xref: /freebsd/usr.sbin/mpsutil/mps_cmd.c (revision 2830819497fb2deae3dd71574592ace55f2fbdba)
1 /*-
2  * Copyright (c) 2015 Netflix, Inc.
3  * All rights reserved.
4  * Written by: Scott Long <scottl@freebsd.org>
5  *
6  * Copyright (c) 2008 Yahoo!, Inc.
7  * All rights reserved.
8  * Written by: John Baldwin <jhb@FreeBSD.org>
9  *
10  * Redistribution and use in source and binary forms, with or without
11  * modification, are permitted provided that the following conditions
12  * are met:
13  * 1. Redistributions of source code must retain the above copyright
14  *    notice, this list of conditions and the following disclaimer.
15  * 2. Redistributions in binary form must reproduce the above copyright
16  *    notice, this list of conditions and the following disclaimer in the
17  *    documentation and/or other materials provided with the distribution.
18  * 3. Neither the name of the author nor the names of any co-contributors
19  *    may be used to endorse or promote products derived from this software
20  *    without specific prior written permission.
21  *
22  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
23  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
26  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
28  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
29  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
31  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
32  * SUCH DAMAGE.
33  */
34 
35 #include <sys/cdefs.h>
36 __RCSID("$FreeBSD$");
37 
38 #include <sys/param.h>
39 #include <sys/errno.h>
40 #include <sys/ioctl.h>
41 #if 0
42 #include <sys/mps_ioctl.h>
43 #else
44 #include "mps_ioctl.h"
45 #include "mpr_ioctl.h"
46 #endif
47 #include <sys/sysctl.h>
48 #include <sys/uio.h>
49 
50 #include <err.h>
51 #include <fcntl.h>
52 #include <stdio.h>
53 #include <stdlib.h>
54 #include <string.h>
55 #include <unistd.h>
56 
57 #include "mpsutil.h"
58 
59 #ifndef USE_MPT_IOCTLS
60 #define USE_MPT_IOCTLS
61 #endif
62 
63 static const char *mps_ioc_status_codes[] = {
64 	"Success",				/* 0x0000 */
65 	"Invalid function",
66 	"Busy",
67 	"Invalid scatter-gather list",
68 	"Internal error",
69 	"Reserved",
70 	"Insufficient resources",
71 	"Invalid field",
72 	"Invalid state",			/* 0x0008 */
73 	"Operation state not supported",
74 	NULL,
75 	NULL,
76 	NULL,
77 	NULL,
78 	NULL,
79 	NULL,
80 	NULL,					/* 0x0010 */
81 	NULL,
82 	NULL,
83 	NULL,
84 	NULL,
85 	NULL,
86 	NULL,
87 	NULL,
88 	NULL,					/* 0x0018 */
89 	NULL,
90 	NULL,
91 	NULL,
92 	NULL,
93 	NULL,
94 	NULL,
95 	NULL,
96 	"Invalid configuration action",		/* 0x0020 */
97 	"Invalid configuration type",
98 	"Invalid configuration page",
99 	"Invalid configuration data",
100 	"No configuration defaults",
101 	"Unable to commit configuration change",
102 	NULL,
103 	NULL,
104 	NULL,					/* 0x0028 */
105 	NULL,
106 	NULL,
107 	NULL,
108 	NULL,
109 	NULL,
110 	NULL,
111 	NULL,
112 	NULL,					/* 0x0030 */
113 	NULL,
114 	NULL,
115 	NULL,
116 	NULL,
117 	NULL,
118 	NULL,
119 	NULL,
120 	NULL,					/* 0x0038 */
121 	NULL,
122 	NULL,
123 	NULL,
124 	NULL,
125 	NULL,
126 	NULL,
127 	NULL,
128 	"Recovered SCSI error",			/* 0x0040 */
129 	"Invalid SCSI bus",
130 	"Invalid SCSI target ID",
131 	"SCSI device not there",
132 	"SCSI data overrun",
133 	"SCSI data underrun",
134 	"SCSI I/O error",
135 	"SCSI protocol error",
136 	"SCSI task terminated",			/* 0x0048 */
137 	"SCSI residual mismatch",
138 	"SCSI task management failed",
139 	"SCSI I/O controller terminated",
140 	"SCSI external controller terminated",
141 	"EEDP guard error",
142 	"EEDP reference tag error",
143 	"EEDP application tag error",
144 	NULL,					/* 0x0050 */
145 	NULL,
146 	NULL,
147 	NULL,
148 	NULL,
149 	NULL,
150 	NULL,
151 	NULL,
152 	NULL,					/* 0x0058 */
153 	NULL,
154 	NULL,
155 	NULL,
156 	NULL,
157 	NULL,
158 	NULL,
159 	NULL,
160 	"SCSI target priority I/O",		/* 0x0060 */
161 	"Invalid SCSI target port",
162 	"Invalid SCSI target I/O index",
163 	"SCSI target aborted",
164 	"No connection retryable",
165 	"No connection",
166 	"FC aborted",
167 	"Invalid FC receive ID",
168 	"FC did invalid",			/* 0x0068 */
169 	"FC node logged out",
170 	"Transfer count mismatch",
171 	"STS data not set",
172 	"FC exchange canceled",
173 	"Data offset error",
174 	"Too much write data",
175 	"IU too short",
176 	"ACK NAK timeout",			/* 0x0070 */
177 	"NAK received",
178 	NULL,
179 	NULL,
180 	NULL,
181 	NULL,
182 	NULL,
183 	NULL,
184 	NULL,					/* 0x0078 */
185 	NULL,
186 	NULL,
187 	NULL,
188 	NULL,
189 	NULL,
190 	NULL,
191 	NULL,
192 	"LAN device not found",			/* 0x0080 */
193 	"LAN device failure",
194 	"LAN transmit error",
195 	"LAN transmit aborted",
196 	"LAN receive error",
197 	"LAN receive aborted",
198 	"LAN partial packet",
199 	"LAN canceled",
200 	NULL,					/* 0x0088 */
201 	NULL,
202 	NULL,
203 	NULL,
204 	NULL,
205 	NULL,
206 	NULL,
207 	NULL,
208 	"SAS SMP request failed",		/* 0x0090 */
209 	"SAS SMP data overrun",
210 	NULL,
211 	NULL,
212 	NULL,
213 	NULL,
214 	NULL,
215 	NULL,
216 	"Inband aborted",			/* 0x0098 */
217 	"No inband connection",
218 	NULL,
219 	NULL,
220 	NULL,
221 	NULL,
222 	NULL,
223 	NULL,
224 	"Diagnostic released",			/* 0x00A0 */
225 };
226 
227 struct mprs_pass_thru {
228         uint64_t        PtrRequest;
229         uint64_t        PtrReply;
230         uint64_t        PtrData;
231         uint32_t        RequestSize;
232         uint32_t        ReplySize;
233         uint32_t        DataSize;
234         uint32_t        DataDirection;
235         uint64_t        PtrDataOut;
236         uint32_t        DataOutSize;
237         uint32_t        Timeout;
238 };
239 
240 struct mprs_btdh_mapping {
241         uint16_t        TargetID;
242         uint16_t        Bus;
243         uint16_t        DevHandle;
244         uint16_t        Reserved;
245 };
246 
247 const char *
248 mps_ioc_status(U16 IOCStatus)
249 {
250 	static char buffer[16];
251 
252 	IOCStatus &= MPI2_IOCSTATUS_MASK;
253 	if (IOCStatus < sizeof(mps_ioc_status_codes) / sizeof(char *) &&
254 	    mps_ioc_status_codes[IOCStatus] != NULL)
255 		return (mps_ioc_status_codes[IOCStatus]);
256 	snprintf(buffer, sizeof(buffer), "Status: 0x%04x", IOCStatus);
257 	return (buffer);
258 }
259 
260 #ifdef USE_MPT_IOCTLS
261 int
262 mps_map_btdh(int fd, uint16_t *devhandle, uint16_t *bus, uint16_t *target)
263 {
264 	int error;
265 	struct mprs_btdh_mapping map;
266 
267 	map.Bus = *bus;
268 	map.TargetID = *target;
269 	map.DevHandle = *devhandle;
270 
271 	if ((error = ioctl(fd, MPTIOCTL_BTDH_MAPPING, &map)) != 0) {
272 		error = errno;
273 		warn("Failed to map bus/target/device");
274 		return (error);
275 	}
276 
277 	*bus = map.Bus;
278 	*target = map.TargetID;
279 	*devhandle = map.DevHandle;
280 
281 	return (0);
282 }
283 
284 int
285 mps_read_config_page_header(int fd, U8 PageType, U8 PageNumber, U32 PageAddress,
286     MPI2_CONFIG_PAGE_HEADER *header, U16 *IOCStatus)
287 {
288 	MPI2_CONFIG_REQUEST req;
289 	MPI2_CONFIG_REPLY reply;
290 
291 	bzero(&req, sizeof(req));
292 	req.Function = MPI2_FUNCTION_CONFIG;
293 	req.Action = MPI2_CONFIG_ACTION_PAGE_HEADER;
294 	req.Header.PageType = PageType;
295 	req.Header.PageNumber = PageNumber;
296 	req.PageAddress = PageAddress;
297 
298 	if (mps_pass_command(fd, &req, sizeof(req), &reply, sizeof(reply),
299 	    NULL, 0, NULL, 0, 30))
300 		return (errno);
301 
302 	if (!IOC_STATUS_SUCCESS(reply.IOCStatus)) {
303 		if (IOCStatus != NULL)
304 			*IOCStatus = reply.IOCStatus;
305 		return (EIO);
306 	}
307 	if (header == NULL)
308 		return (EINVAL);
309 	*header = reply.Header;
310 	return (0);
311 }
312 
313 int
314 mps_read_ext_config_page_header(int fd, U8 ExtPageType, U8 PageNumber, U32 PageAddress, MPI2_CONFIG_PAGE_HEADER *header, U16 *ExtPageLength, U16 *IOCStatus)
315 {
316 	MPI2_CONFIG_REQUEST req;
317 	MPI2_CONFIG_REPLY reply;
318 
319 	bzero(&req, sizeof(req));
320 	req.Function = MPI2_FUNCTION_CONFIG;
321 	req.Action = MPI2_CONFIG_ACTION_PAGE_HEADER;
322 	req.Header.PageType = MPI2_CONFIG_PAGETYPE_EXTENDED;
323 	req.ExtPageType = ExtPageType;
324 	req.Header.PageNumber = PageNumber;
325 	req.PageAddress = PageAddress;
326 
327 	if (mps_pass_command(fd, &req, sizeof(req), &reply, sizeof(reply),
328 	    NULL, 0, NULL, 0, 30))
329 		return (errno);
330 
331 	if (!IOC_STATUS_SUCCESS(reply.IOCStatus)) {
332 		if (IOCStatus != NULL)
333 			*IOCStatus = reply.IOCStatus;
334 		return (EIO);
335 	}
336 	if ((header == NULL) || (ExtPageLength == NULL))
337 		return (EINVAL);
338 	*header = reply.Header;
339 	*ExtPageLength = reply.ExtPageLength;
340 	return (0);
341 }
342 
343 void *
344 mps_read_config_page(int fd, U8 PageType, U8 PageNumber, U32 PageAddress,
345     U16 *IOCStatus)
346 {
347 	MPI2_CONFIG_REQUEST req;
348 	MPI2_CONFIG_PAGE_HEADER header;
349 	MPI2_CONFIG_REPLY reply;
350 	void *buf;
351 	int error, len;
352 
353 	bzero(&header, sizeof(header));
354 	error = mps_read_config_page_header(fd, PageType, PageNumber,
355 	    PageAddress, &header, IOCStatus);
356 	if (error) {
357 		errno = error;
358 		return (NULL);
359 	}
360 
361 	bzero(&req, sizeof(req));
362 	req.Function = MPI2_FUNCTION_CONFIG;
363 	req.Action = MPI2_CONFIG_ACTION_PAGE_READ_CURRENT;
364 	req.PageAddress = PageAddress;
365 	req.Header = header;
366 	req.Header.PageLength = reply.Header.PageLength;
367 	if (reply.Header.PageLength == 0)
368 		req.Header.PageLength = 4;
369 
370 	len = req.Header.PageLength * 4;
371 	buf = malloc(len);
372 	if (mps_pass_command(fd, &req, sizeof(req), &reply, sizeof(reply),
373 	    buf, len, NULL, 0, 30)) {
374 		error = errno;
375 		free(buf);
376 		errno = error;
377 		return (NULL);
378 	}
379 	if (!IOC_STATUS_SUCCESS(reply.IOCStatus)) {
380 		if (IOCStatus != NULL)
381 			*IOCStatus = reply.IOCStatus;
382 		else
383 			warnx("Reading config page failed: 0x%x %s",
384 			    reply.IOCStatus, mps_ioc_status(reply.IOCStatus));
385 		free(buf);
386 		errno = EIO;
387 		return (NULL);
388 	}
389 	return (buf);
390 }
391 
392 void *
393 mps_read_extended_config_page(int fd, U8 ExtPageType, U8 PageVersion,
394     U8 PageNumber, U32 PageAddress, U16 *IOCStatus)
395 {
396 	MPI2_CONFIG_REQUEST req;
397 	MPI2_CONFIG_PAGE_HEADER header;
398 	MPI2_CONFIG_REPLY reply;
399 	U16 pagelen;
400 	void *buf;
401 	int error, len;
402 
403 	if (IOCStatus != NULL)
404 		*IOCStatus = MPI2_IOCSTATUS_SUCCESS;
405 	bzero(&header, sizeof(header));
406 	error = mps_read_ext_config_page_header(fd, ExtPageType, PageNumber,
407 	    PageAddress, &header, &pagelen, IOCStatus);
408 	if (error) {
409 		errno = error;
410 		return (NULL);
411 	}
412 
413 	bzero(&req, sizeof(req));
414 	req.Function = MPI2_FUNCTION_CONFIG;
415 	req.Action = MPI2_CONFIG_ACTION_PAGE_READ_CURRENT;
416 	req.PageAddress = PageAddress;
417 	req.Header = header;
418 	if (pagelen == 0)
419 		pagelen = 4;
420 	req.ExtPageLength = pagelen;
421 	req.ExtPageType = ExtPageType;
422 
423 	len = pagelen * 4;
424 	buf = malloc(len);
425 	if (mps_pass_command(fd, &req, sizeof(req), &reply, sizeof(reply),
426 	    buf, len, NULL, 0, 30)) {
427 		error = errno;
428 		free(buf);
429 		errno = error;
430 		return (NULL);
431 	}
432 	if (!IOC_STATUS_SUCCESS(reply.IOCStatus)) {
433 		if (IOCStatus != NULL)
434 			*IOCStatus = reply.IOCStatus;
435 		else
436 			warnx("Reading extended config page failed: %s",
437 			    mps_ioc_status(reply.IOCStatus));
438 		free(buf);
439 		errno = EIO;
440 		return (NULL);
441 	}
442 	return (buf);
443 }
444 
445 #else
446 
447 int
448 mps_read_config_page_header(int fd, U8 PageType, U8 PageNumber, U32 PageAddress,
449     MPI2_CONFIG_PAGE_HEADER *header, U16 *IOCStatus)
450 {
451 	struct mps_cfg_page_req req;
452 
453 	if (IOCStatus != NULL)
454 		*IOCStatus = MPI2_IOCSTATUS_SUCCESS;
455 	if (header == NULL)
456 		return (EINVAL);
457 	bzero(&req, sizeof(req));
458 	req.header.PageType = PageType;
459 	req.header.PageNumber = PageNumber;
460 	req.page_address = PageAddress;
461 	if (ioctl(fd, MPSIO_READ_CFG_HEADER, &req) < 0)
462 		return (errno);
463 	if (!IOC_STATUS_SUCCESS(req.ioc_status)) {
464 		if (IOCStatus != NULL)
465 			*IOCStatus = req.ioc_status;
466 		return (EIO);
467 	}
468 	bcopy(&req.header, header, sizeof(*header));
469 	return (0);
470 }
471 
472 void *
473 mps_read_config_page(int fd, U8 PageType, U8 PageNumber, U32 PageAddress,
474     U16 *IOCStatus)
475 {
476 	struct mps_cfg_page_req req;
477 	void *buf;
478 	int error;
479 
480 	error = mps_read_config_page_header(fd, PageType, PageNumber,
481 	    PageAddress, &req.header, IOCStatus);
482 	if (error) {
483 		errno = error;
484 		return (NULL);
485 	}
486 
487 	if (req.header.PageLength == 0)
488 		req.header.PageLength = 4;
489 	req.len = req.header.PageLength * 4;
490 	buf = malloc(req.len);
491 	req.buf = buf;
492 	bcopy(&req.header, buf, sizeof(req.header));
493 	if (ioctl(fd, MPSIO_READ_CFG_PAGE, &req) < 0) {
494 		error = errno;
495 		free(buf);
496 		errno = error;
497 		return (NULL);
498 	}
499 	if (!IOC_STATUS_SUCCESS(req.ioc_status)) {
500 		if (IOCStatus != NULL)
501 			*IOCStatus = req.ioc_status;
502 		else
503 			warnx("Reading config page failed: 0x%x %s",
504 			    req.ioc_status, mps_ioc_status(req.ioc_status));
505 		free(buf);
506 		errno = EIO;
507 		return (NULL);
508 	}
509 	return (buf);
510 }
511 
512 void *
513 mps_read_extended_config_page(int fd, U8 ExtPageType, U8 PageVersion,
514     U8 PageNumber, U32 PageAddress, U16 *IOCStatus)
515 {
516 	struct mps_ext_cfg_page_req req;
517 	void *buf;
518 	int error;
519 
520 	if (IOCStatus != NULL)
521 		*IOCStatus = MPI2_IOCSTATUS_SUCCESS;
522 	bzero(&req, sizeof(req));
523 	req.header.PageVersion = PageVersion;
524 	req.header.PageNumber = PageNumber;
525 	req.header.ExtPageType = ExtPageType;
526 	req.page_address = PageAddress;
527 	if (ioctl(fd, MPSIO_READ_EXT_CFG_HEADER, &req) < 0)
528 		return (NULL);
529 	if (!IOC_STATUS_SUCCESS(req.ioc_status)) {
530 		if (IOCStatus != NULL)
531 			*IOCStatus = req.ioc_status;
532 		else
533 			warnx("Reading extended config page header failed: %s",
534 			    mps_ioc_status(req.ioc_status));
535 		errno = EIO;
536 		return (NULL);
537 	}
538 	req.len = req.header.ExtPageLength * 4;
539 	buf = malloc(req.len);
540 	req.buf = buf;
541 	bcopy(&req.header, buf, sizeof(req.header));
542 	if (ioctl(fd, MPSIO_READ_EXT_CFG_PAGE, &req) < 0) {
543 		error = errno;
544 		free(buf);
545 		errno = error;
546 		return (NULL);
547 	}
548 	if (!IOC_STATUS_SUCCESS(req.ioc_status)) {
549 		if (IOCStatus != NULL)
550 			*IOCStatus = req.ioc_status;
551 		else
552 			warnx("Reading extended config page failed: %s",
553 			    mps_ioc_status(req.ioc_status));
554 		free(buf);
555 		errno = EIO;
556 		return (NULL);
557 	}
558 	return (buf);
559 }
560 #endif
561 
562 int
563 mps_open(int unit)
564 {
565 	char path[MAXPATHLEN];
566 
567 	snprintf(path, sizeof(path), "/dev/mp%s%d", is_mps ? "s": "r", unit);
568 	return (open(path, O_RDWR));
569 }
570 
571 int
572 mps_user_command(int fd, void *req, uint32_t req_len, void *reply,
573         uint32_t reply_len, void *buffer, int len, uint32_t flags)
574 {
575 	struct mps_usr_command cmd;
576 
577 	bzero(&cmd, sizeof(struct mps_usr_command));
578 	cmd.req = req;
579 	cmd.req_len = req_len;
580 	cmd.rpl = reply;
581 	cmd.rpl_len = reply_len;
582 	cmd.buf = buffer;
583 	cmd.len = len;
584 	cmd.flags = flags;
585 
586 	if (ioctl(fd, is_mps ? MPSIO_MPS_COMMAND : MPRIO_MPR_COMMAND, &cmd) < 0)
587 		return (errno);
588 	return (0);
589 }
590 
591 int
592 mps_pass_command(int fd, void *req, uint32_t req_len, void *reply,
593 	uint32_t reply_len, void *data_in, uint32_t datain_len, void *data_out,
594 	uint32_t dataout_len, uint32_t timeout)
595 {
596 	struct mprs_pass_thru pass;
597 
598 	pass.PtrRequest = (uint64_t)(uintptr_t)req;
599 	pass.PtrReply = (uint64_t)(uintptr_t)reply;
600 	pass.PtrData = (uint64_t)(uintptr_t)data_in;
601 	pass.PtrDataOut = (uint64_t)(uintptr_t)data_out;
602 	pass.RequestSize = req_len;
603 	pass.ReplySize = reply_len;
604 	pass.DataSize = datain_len;
605 	pass.DataOutSize = dataout_len;
606 	if (datain_len && dataout_len) {
607 		if (is_mps) {
608 			pass.DataDirection = MPS_PASS_THRU_DIRECTION_BOTH;
609 		} else {
610 			pass.DataDirection = MPR_PASS_THRU_DIRECTION_BOTH;
611 		}
612 	} else if (datain_len) {
613 		if (is_mps) {
614 			pass.DataDirection = MPS_PASS_THRU_DIRECTION_READ;
615 		} else {
616 			pass.DataDirection = MPR_PASS_THRU_DIRECTION_READ;
617 		}
618 	} else if (dataout_len) {
619 		if (is_mps) {
620 			pass.DataDirection = MPS_PASS_THRU_DIRECTION_WRITE;
621 		} else {
622 			pass.DataDirection = MPR_PASS_THRU_DIRECTION_WRITE;
623 		}
624 	} else {
625 		if (is_mps) {
626 			pass.DataDirection = MPS_PASS_THRU_DIRECTION_NONE;
627 		} else {
628 			pass.DataDirection = MPR_PASS_THRU_DIRECTION_NONE;
629 		}
630 	}
631 	pass.Timeout = timeout;
632 
633 	if (ioctl(fd, MPTIOCTL_PASS_THRU, &pass) < 0)
634 		return (errno);
635 	return (0);
636 }
637 
638 MPI2_IOC_FACTS_REPLY *
639 mps_get_iocfacts(int fd)
640 {
641 	MPI2_IOC_FACTS_REPLY *facts;
642 	MPI2_IOC_FACTS_REQUEST req;
643 	int error;
644 
645 	facts = malloc(sizeof(MPI2_IOC_FACTS_REPLY));
646 	if (facts == NULL) {
647 		errno = ENOMEM;
648 		return (NULL);
649 	}
650 
651 	bzero(&req, sizeof(MPI2_IOC_FACTS_REQUEST));
652 	req.Function = MPI2_FUNCTION_IOC_FACTS;
653 
654 #if 1
655 	error = mps_pass_command(fd, &req, sizeof(MPI2_IOC_FACTS_REQUEST),
656 	    facts, sizeof(MPI2_IOC_FACTS_REPLY), NULL, 0, NULL, 0, 10);
657 #else
658 	error = mps_user_command(fd, &req, sizeof(MPI2_IOC_FACTS_REQUEST),
659 	    facts, sizeof(MPI2_IOC_FACTS_REPLY), NULL, 0, 0);
660 #endif
661 	if (error) {
662 		free(facts);
663 		return (NULL);
664 	}
665 
666 	if (!IOC_STATUS_SUCCESS(facts->IOCStatus)) {
667 		free(facts);
668 		errno = EINVAL;
669 		return (NULL);
670 	}
671 	return (facts);
672 }
673 
674