xref: /illumos-gate/usr/src/cmd/smbsrv/smbadm/smbinfo.c (revision aed162cfceb2959d973ec638d3d3cf5a730e3a03)
1 /*
2  * This file and its contents are supplied under the terms of the
3  * Common Development and Distribution License ("CDDL"), version 1.0.
4  * You may only use this file in accordance with the terms of version
5  * 1.0 of the CDDL.
6  *
7  * A full copy of the text of the CDDL should have accompanied this
8  * source.  A copy of the CDDL is also available via the Internet at
9  * http://www.illumos.org/license/CDDL.
10  */
11 
12 /*
13  * Copyright 2022 RackTop Systems, Inc.
14  */
15 
16 #include <err.h>
17 #include <errno.h>
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <string.h>
21 #include <sys/debug.h>
22 #include <sys/list.h>
23 #include <sys/types.h>
24 #include <sys/sysmacros.h>
25 #include <smbsrv/libsmb.h>
26 #include <ofmt.h>
27 #include <libintl.h>
28 #include <limits.h>
29 #include <locale.h>
30 #include <time.h>
31 #include <upanic.h>
32 #include "smbadm.h"
33 
34 /*
35  * Share types for shiX_type fields - duplicated from smb.h
36  * Don't want to pull that in, and these are "carved in stone"
37  * (from the SMB protocol definitions)
38  */
39 #ifndef _SHARE_TYPES_DEFINED_
40 #define	_SHARE_TYPES_DEFINED_
41 #define	STYPE_DISKTREE		0x00000000
42 #define	STYPE_PRINTQ		0x00000001
43 #define	STYPE_DEVICE		0x00000002
44 #define	STYPE_IPC		0x00000003
45 #define	STYPE_MASK		0x0000000F
46 #endif /* _SHARE_TYPES_DEFINED_ */
47 
48 #define	MINS		(60U)
49 #define	HRS		(60 * MINS)
50 #define	DAYS		(24 * HRS)
51 #define	TIME_FMT	"%F %T %Z"
52 
53 #define	_(x) gettext(x)
54 
55 struct flag_tbl {
56 	uint32_t	flag;
57 	const char	*name;
58 };
59 
60 typedef enum user_field {
61 	UF_SESS_ID,
62 	UF_DOMAIN,
63 	UF_ACCOUNT,
64 	UF_USER,
65 	UF_UID,
66 	UF_WORKSTATION,
67 	UF_IP,
68 	UF_OS,
69 	UF_LOGON_TIME,
70 	UF_AGE,
71 	UF_NUMOPEN,
72 	UF_FLAGS,
73 } user_field_t;
74 
75 typedef enum tree_field {
76 	TF_ID,
77 	TF_TYPE,
78 	TF_NUMOPEN,
79 	TF_NUMUSERS,
80 	TF_TIME,
81 	TF_AGE,
82 	TF_USERNAME,
83 	TF_SHARE,
84 } tree_field_t;
85 
86 typedef enum netfileinfo_field {
87 	NFIF_FID,
88 	NFIF_UNIQID,
89 	NFIF_PERMS,
90 	NFIF_NUMLOCKS,
91 	NFIF_PATH,
92 	NFIF_USERNAME,
93 } netfileinfo_field_t;
94 
95 static ofmt_handle_t cmd_create_handle(int, char **, const char *,
96     ofmt_field_t *);
97 
98 static boolean_t fmt_user(ofmt_arg_t *, char *, uint_t);
99 static boolean_t fmt_tree(ofmt_arg_t *, char *, uint_t);
100 static boolean_t fmt_netfileinfo(ofmt_arg_t *, char *, uint_t);
101 
102 static void print_str(const char *restrict, char *restrict, uint_t);
103 static void print_u32(uint32_t, char *, uint_t);
104 static void print_age(time_t, char *, uint_t);
105 static void print_time(time_t, const char *, char *, uint_t);
106 static void print_flags(struct flag_tbl *, size_t, uint32_t, char *, uint_t);
107 static void print_perms(struct flag_tbl *, size_t, uint32_t, char *, uint_t);
108 
109 static ofmt_field_t smb_user_fields[] = {
110 	{ "ID",		4,	UF_SESS_ID,	fmt_user },
111 	{ "DOMAIN",	32,	UF_DOMAIN,	fmt_user },
112 	{ "ACCT",	16,	UF_ACCOUNT,	fmt_user },
113 	{ "USER",	32,	UF_USER,	fmt_user },
114 	{ "UID",	12,	UF_UID,		fmt_user },
115 	{ "COMPUTER",	16,	UF_WORKSTATION,	fmt_user },
116 	{ "IP",		15,	UF_IP,		fmt_user },
117 	{ "OS",		8,	UF_OS,		fmt_user },
118 	{ "LOGON",	24,	UF_LOGON_TIME,	fmt_user },
119 	{ "AGE",	16,	UF_AGE,		fmt_user },
120 	{ "NOPEN",	5,	UF_NUMOPEN,	fmt_user },
121 	{ "FLAGS",	12,	UF_FLAGS,	fmt_user },
122 	{ NULL,		0,	0,		NULL }
123 };
124 
125 static const char default_user_fields[] = "IP,USER,NOPEN,AGE,FLAGS";
126 
127 struct flag_tbl user_flag_tbl[] = {
128 	{ SMB_ATF_GUEST, "GUEST" },
129 	{ SMB_ATF_ANON, "ANON" },
130 	{ SMB_ATF_ADMIN, "ADMIN" },
131 	{ SMB_ATF_POWERUSER, "POWERUSER" },
132 	{ SMB_ATF_BACKUPOP, "BACKUPOP" },
133 };
134 
135 static ofmt_field_t smb_tree_fields[] = {
136 	{ "ID",		4,	TF_ID,		fmt_tree },
137 	{ "TYPE",	6,	TF_TYPE,	fmt_tree },
138 	{ "NOPEN",	6,	TF_NUMOPEN,	fmt_tree },
139 	{ "NUSER",	6,	TF_NUMUSERS,	fmt_tree },
140 	{ "TIME",	24,	TF_TIME,	fmt_tree },
141 	{ "AGE",	12,	TF_AGE,		fmt_tree },
142 	{ "USER",	32,	TF_USERNAME,	fmt_tree },
143 	{ "SHARE",	16,	TF_SHARE,	fmt_tree },
144 	{ NULL,		0,	0,		NULL }
145 };
146 
147 static const char default_tree_fields[] = "TYPE,SHARE,USER,NOPEN,AGE";
148 
149 static ofmt_field_t smb_netfileinfo_fields[] = {
150 	{ "ID",		4,	NFIF_FID,	fmt_netfileinfo },
151 	{ "UNIQID",	8,	NFIF_UNIQID,	fmt_netfileinfo },
152 	{ "PERM",	15,	NFIF_PERMS,	fmt_netfileinfo },
153 	{ "NLOCK",	6,	NFIF_NUMLOCKS,	fmt_netfileinfo },
154 	{ "PATH",	32,	NFIF_PATH,	fmt_netfileinfo },
155 	{ "USER",	16,	NFIF_USERNAME,	fmt_netfileinfo },
156 	{ NULL,		0,	0,		NULL }
157 };
158 
159 static const char default_netfileinfo_fields[] = "UNIQID,PATH,USER,NLOCK,PERM";
160 
161 /*
162  * Flags are the same as "ls -V" and chmod ACLs:
163  * eg:  everyone@:rwxpdDaARWcCos:fd----I:allow
164  * See libsec:acltext.c
165  */
166 static struct flag_tbl nfi_perm_tbl[] = {
167 	{ ACE_READ_DATA,		"r" },
168 	{ ACE_WRITE_DATA,		"w" },
169 	{ ACE_EXECUTE,			"x" },
170 	{ ACE_APPEND_DATA,		"p" },
171 	{ ACE_DELETE,			"d" },
172 	{ ACE_DELETE_CHILD,		"D" },
173 	{ ACE_READ_ATTRIBUTES,		"a" },
174 	{ ACE_WRITE_ATTRIBUTES,		"A" },
175 	{ ACE_READ_NAMED_ATTRS,		"R" },
176 	{ ACE_WRITE_NAMED_ATTRS,	"W" },
177 	{ ACE_READ_ACL,			"c" },
178 	{ ACE_WRITE_ACL,		"C" },
179 	{ ACE_WRITE_OWNER,		"o" },
180 	{ ACE_SYNCHRONIZE,		"s" },
181 };
182 
183 static int do_enum(smb_svcenum_t *, ofmt_handle_t);
184 static void ofmt_fatal(ofmt_handle_t, ofmt_field_t *, ofmt_status_t)
185     __NORETURN;
186 static void fatal(const char *, ...) __NORETURN;
187 
188 time_t		now;
189 boolean_t	opt_p;
190 boolean_t	opt_x;
191 
192 int
193 cmd_list_sess(int argc, char **argv)
194 {
195 	ofmt_handle_t	hdl;
196 	smb_svcenum_t	req = {
197 		.se_type = SMB_SVCENUM_TYPE_USER,
198 		.se_level = 1,
199 		.se_nlimit = UINT32_MAX,
200 	};
201 	int rc;
202 
203 	hdl = cmd_create_handle(argc, argv, default_user_fields,
204 	    smb_user_fields);
205 	rc = do_enum(&req, hdl);
206 	ofmt_close(hdl);
207 	return (rc);
208 }
209 
210 int
211 cmd_list_trees(int argc, char **argv)
212 {
213 	ofmt_handle_t	hdl;
214 	smb_svcenum_t	req = {
215 		.se_type = SMB_SVCENUM_TYPE_TREE,
216 		.se_level = 1,
217 		.se_nlimit = UINT32_MAX,
218 	};
219 	int rc;
220 
221 	hdl = cmd_create_handle(argc, argv, default_tree_fields,
222 	    smb_tree_fields);
223 	rc = do_enum(&req, hdl);
224 	ofmt_close(hdl);
225 	return (rc);
226 }
227 
228 int
229 cmd_list_ofiles(int argc, char **argv)
230 {
231 	ofmt_handle_t	hdl;
232 	smb_svcenum_t	req = {
233 		.se_type = SMB_SVCENUM_TYPE_FILE,
234 		.se_level = 1,
235 		.se_nlimit = UINT32_MAX,
236 	};
237 	int rc;
238 
239 	hdl = cmd_create_handle(argc, argv, default_netfileinfo_fields,
240 	    smb_netfileinfo_fields);
241 	rc = do_enum(&req, hdl);
242 	ofmt_close(hdl);
243 	return (rc);
244 }
245 
246 static ofmt_handle_t
247 cmd_create_handle(int argc, char **argv, const char *def, ofmt_field_t *templ)
248 {
249 	const char	*fields = def;
250 	ofmt_handle_t	hdl;
251 	ofmt_status_t	status;
252 	uint_t		flags = 0;
253 	int		c;
254 
255 	while ((c = getopt(argc, argv, "Ho:px")) != -1) {
256 		switch (c) {
257 		case 'H':
258 			flags |= OFMT_NOHEADER;
259 			break;
260 		case 'o':
261 			fields = optarg;
262 			break;
263 		case 'p':
264 			opt_p = B_TRUE;
265 			flags |= OFMT_PARSABLE;
266 			break;
267 		case 'x':
268 			opt_x = B_TRUE;
269 			break;
270 		case '?':
271 			/* Note: getopt prints an error for us. */
272 			return (NULL);
273 		}
274 	}
275 
276 	status = ofmt_open(fields, templ, flags, 0, &hdl);
277 	if (status != OFMT_SUCCESS)
278 		ofmt_fatal(hdl, templ, status);
279 
280 	return (hdl);
281 }
282 
283 int
284 cmd_close_ofile(int argc, char **argv)
285 {
286 	uint_t errs = 0;
287 
288 	if (argc < 2) {
289 		warnx(_("Missing file id"));
290 		return (2);
291 	}
292 
293 	for (int i = 1; i < argc; i++) {
294 		unsigned long ul;
295 		int rc;
296 
297 		errno = 0;
298 		ul = strtoul(argv[i], NULL, 0);
299 		if (errno != 0) {
300 			warnx(_("Invalid file id '%s'"), argv[i]);
301 			return (2);
302 		}
303 #ifdef _LP64
304 		if (ul > UINT32_MAX) {
305 			warnx(_("File id %lu too large"), ul);
306 			return (2);
307 		}
308 #endif
309 
310 		/*
311 		 * See SMB_IOC_FILE_CLOSE (ioc.uniqid)
312 		 * and smb_server_file_close()
313 		 */
314 		rc = smb_kmod_file_close((uint32_t)ul);
315 		if (rc != 0) {
316 			/*
317 			 * Since the user can specify the fid as a decimal
318 			 * or hex value, we use the string they gave us so
319 			 * the value displayed matches what we were given.
320 			 */
321 			warnc(rc, _("Closing fid %s failed"), argv[i]);
322 			errs++;
323 		}
324 	}
325 
326 	if (errs > 0)
327 		return (1);
328 	return (0);
329 }
330 
331 int
332 cmd_close_sess(int argc, char **argv)
333 {
334 	const char *client;
335 	const char *user = NULL;
336 	int rc;
337 
338 	if (argc < 2) {
339 		warnx(_("clientname and username missing"));
340 		return (2);
341 	}
342 	client = argv[1];
343 	if (argc > 2) {
344 		user = argv[2];
345 	}
346 
347 	/*
348 	 * See SMB_IOC_SESSION_CLOSE (ioc.client, ioc.username)
349 	 * and smb_server_session_close().  The "client" part
350 	 * can be EITHER the "workstation" or the IP address,
351 	 * as shown in the "COMPUTER" and "IP" fields in the
352 	 * output of "list-sessions".   The (optional) "user"
353 	 * part is as shown in "USER" part of that output.
354 	 */
355 	rc = smb_kmod_session_close(client, user);
356 	if (rc != 0) {
357 		rc = 1;
358 	}
359 	return (rc);
360 }
361 
362 static boolean_t
363 fmt_user(ofmt_arg_t *arg, char *buf, uint_t buflen)
364 {
365 	smb_netuserinfo_t	*ui = arg->ofmt_cbarg;
366 	user_field_t		field = (user_field_t)arg->ofmt_id;
367 
368 	switch (field) {
369 	case UF_SESS_ID:
370 		(void) snprintf(buf, buflen, "%" PRIu64, ui->ui_session_id);
371 		break;
372 	case UF_DOMAIN:
373 		print_str(ui->ui_domain, buf, buflen);
374 		break;
375 	case UF_ACCOUNT:
376 		print_str(ui->ui_account, buf, buflen);
377 		break;
378 	case UF_USER:
379 		(void) snprintf(buf, buflen, "%s\\%s", ui->ui_domain,
380 		    ui->ui_account);
381 		break;
382 	case UF_UID:
383 		VERIFY3U(arg->ofmt_width, <, INT_MAX);
384 		(void) snprintf(buf, buflen, "%u", ui->ui_posix_uid);
385 		break;
386 	case UF_WORKSTATION:
387 		print_str(ui->ui_workstation, buf, buflen);
388 		break;
389 	case UF_IP:
390 		(void) smb_inet_ntop(&ui->ui_ipaddr, buf, buflen);
391 		break;
392 	case UF_OS:
393 		/* XXX: Lookup string value */
394 		(void) snprintf(buf, buflen, "%" PRId32, ui->ui_native_os);
395 		break;
396 	case UF_LOGON_TIME:
397 		print_time(ui->ui_logon_time, TIME_FMT, buf, buflen);
398 		break;
399 	case UF_AGE:
400 		print_age(now - ui->ui_logon_time, buf, buflen);
401 		break;
402 	case UF_NUMOPEN:
403 		print_u32(ui->ui_numopens, buf, buflen);
404 		break;
405 	case UF_FLAGS:
406 		print_flags(user_flag_tbl, ARRAY_SIZE(user_flag_tbl),
407 		    ui->ui_flags, buf, buflen);
408 		break;
409 	default:
410 		fatal("%s: invalid field %d", __func__, field);
411 	}
412 
413 	return (B_TRUE);
414 }
415 
416 static boolean_t
417 fmt_tree_type(uint32_t type, char *buf, uint_t buflen)
418 {
419 	switch (type & STYPE_MASK) {
420 	case STYPE_DISKTREE:
421 		(void) strlcpy(buf, "DISK", buflen);
422 		break;
423 	case STYPE_PRINTQ:
424 		(void) strlcpy(buf, "PRINTQ", buflen);
425 		break;
426 	case STYPE_DEVICE:
427 		(void) strlcpy(buf, "DEVICE", buflen);
428 		break;
429 	case STYPE_IPC:
430 		(void) strlcpy(buf, "IPC", buflen);
431 		break;
432 	default:
433 		(void) snprintf(buf, buflen, "%" PRIx32, type & STYPE_MASK);
434 		break;
435 	}
436 
437 	return (B_TRUE);
438 }
439 
440 static boolean_t
441 fmt_tree(ofmt_arg_t *arg, char *buf, uint_t buflen)
442 {
443 	smb_netconnectinfo_t	*nc = arg->ofmt_cbarg;
444 	tree_field_t		field = (tree_field_t)arg->ofmt_id;
445 
446 	switch (field) {
447 	case TF_ID:
448 		(void) snprintf(buf, buflen, "%" PRIu32, nc->ci_id);
449 		break;
450 	case TF_TYPE:
451 		return (fmt_tree_type(nc->ci_type, buf, buflen));
452 	case TF_NUMOPEN:
453 		print_u32(nc->ci_numopens, buf, buflen);
454 		break;
455 	case TF_NUMUSERS:
456 		print_u32(nc->ci_numusers, buf, buflen);
457 		break;
458 	case TF_TIME:
459 		print_time(now - nc->ci_time, TIME_FMT, buf, buflen);
460 		break;
461 	case TF_AGE:
462 		print_age(nc->ci_time, buf, buflen);
463 		break;
464 	case TF_USERNAME:
465 		print_str(nc->ci_username, buf, buflen);
466 		break;
467 	case TF_SHARE:
468 		print_str(nc->ci_share, buf, buflen);
469 		break;
470 	default:
471 		fatal("%s: invalid field %d", __func__, field);
472 	}
473 
474 	return (B_TRUE);
475 }
476 
477 static boolean_t
478 fmt_netfileinfo(ofmt_arg_t *arg, char *buf, uint_t buflen)
479 {
480 	smb_netfileinfo_t	*fi = arg->ofmt_cbarg;
481 	netfileinfo_field_t	field = (netfileinfo_field_t)arg->ofmt_id;
482 
483 	switch (field) {
484 	case NFIF_FID:
485 		(void) snprintf(buf, buflen, "%" PRIu16, fi->fi_fid);
486 		break;
487 	case NFIF_UNIQID:
488 		(void) snprintf(buf, buflen, "%" PRIu32, fi->fi_uniqid);
489 		break;
490 	case NFIF_PERMS:
491 		print_perms(nfi_perm_tbl, ARRAY_SIZE(nfi_perm_tbl),
492 		    fi->fi_permissions, buf, buflen);
493 		break;
494 	case NFIF_NUMLOCKS:
495 		print_u32(fi->fi_numlocks, buf, buflen);
496 		break;
497 	case NFIF_PATH:
498 		print_str(fi->fi_path, buf, buflen);
499 		break;
500 	case NFIF_USERNAME:
501 		print_str(fi->fi_username, buf, buflen);
502 		break;
503 	default:
504 		fatal("%s: invalid field %d", __func__, field);
505 	}
506 
507 	return (B_TRUE);
508 }
509 
510 static int
511 do_enum(smb_svcenum_t *req, ofmt_handle_t hdl)
512 {
513 	smb_netsvc_t		*ns;
514 	smb_netsvcitem_t	*item;
515 	uint32_t		n = 0;
516 	int rc;
517 
518 	if (hdl == NULL)
519 		return (2);	/* exit (2) -- usage */
520 	now = time(NULL);
521 
522 	for (;;) {
523 		req->se_nskip = n;
524 
525 		ns = smb_kmod_enum_init(req);
526 		if (ns == NULL) {
527 			warnx(_("SMB enum initialization failure"));
528 			return (1);
529 		}
530 
531 		rc = smb_kmod_enum(ns);
532 		if (rc != 0) {
533 			/*
534 			 * When the SMB service is not running, expect ENXIO.
535 			 */
536 			if (rc == ENXIO) {
537 				warnx(_("Kernel SMB server not running"));
538 				return (1);
539 			}
540 			warnc(rc, _("SMB enumeration call failed"));
541 			return (1);
542 		}
543 
544 		if (list_is_empty(&ns->ns_list))
545 			break;
546 
547 		for (item = list_head(&ns->ns_list); item != NULL;
548 		    item = list_next(&ns->ns_list, item)) {
549 			ofmt_print(hdl, &item->nsi_un);
550 			n++;
551 		}
552 
553 		smb_kmod_enum_fini(ns);
554 	}
555 	return (0);
556 }
557 
558 static void
559 print_str(const char *restrict src, char *restrict buf, uint_t buflen)
560 {
561 	if (src == NULL) {
562 		buf[0] = '\0';
563 		return;
564 	}
565 	(void) strlcpy(buf, src, buflen);
566 }
567 
568 static void
569 print_u32(uint32_t val, char *buf, uint_t buflen)
570 {
571 	const char *fmt = opt_p ? "%" PRIu32 : "%'" PRIu32;
572 
573 	(void) snprintf(buf, buflen, fmt, val);
574 }
575 
576 static void
577 print_age(time_t amt, char *buf, uint_t buflen)
578 {
579 	uint32_t days = 0, hours = 0, mins = 0;
580 
581 	if (opt_p) {
582 		(void) snprintf(buf, buflen, "%" PRId64, (int64_t)amt);
583 		return;
584 	}
585 
586 	if (amt >= DAYS) {
587 		days = amt / DAYS;
588 		amt %= DAYS;
589 	}
590 	if (amt >= HRS) {
591 		hours = amt / HRS;
592 		amt %= HRS;
593 	}
594 	if (amt >= MINS) {
595 		mins = amt / MINS;
596 		amt %= MINS;
597 	}
598 
599 	if (days > 0) {
600 		int n = snprintf(buf, buflen, "%" PRIu32 " days%s",
601 		    days, (hours > 0 || mins > 0 || amt > 0) ? ", " : "");
602 
603 		VERIFY3U(buflen, >, n);
604 
605 		buf += n;
606 		buflen -= n;
607 	}
608 
609 	(void) snprintf(buf, buflen, "%02" PRIu32 ":%02" PRIu32 ":%02" PRIu32,
610 	    hours, mins, amt);
611 }
612 
613 static void
614 print_time(time_t when, const char *fmt, char *buf, uint_t buflen)
615 {
616 	const struct tm *tm;
617 
618 	if (opt_p) {
619 		(void) snprintf(buf, buflen, "%" PRId64, (int64_t)when);
620 		return;
621 	}
622 
623 	tm = localtime(&when);
624 	(void) strftime(buf, buflen - 1, fmt, tm);
625 }
626 
627 static void
628 print_flags(struct flag_tbl *tbl, size_t nent, uint32_t val, char *buf,
629     uint_t buflen)
630 {
631 	uint_t n = 0;
632 	uint_t i;
633 
634 	if (opt_x) {
635 		(void) snprintf(buf, buflen, "%" PRIx32, val);
636 		return;
637 	}
638 
639 	for (i = 0; i < nent; i++) {
640 		if ((val & tbl[i].flag) == 0)
641 			continue;
642 		if (n > 0)
643 			(void) strlcat(buf, ",", buflen);
644 		(void) strlcat(buf, tbl[i].name, buflen);
645 		n++;
646 	}
647 
648 	if (n == 0)
649 		(void) strlcat(buf, "-", buflen);
650 }
651 
652 static void
653 print_perms(struct flag_tbl *tbl, size_t nent, uint32_t val, char *buf,
654     uint_t buflen)
655 {
656 	uint_t n = 0;
657 	uint_t i;
658 
659 	if (opt_x) {
660 		(void) snprintf(buf, buflen, "%" PRIx32, val);
661 		return;
662 	}
663 
664 	for (i = 0; i < nent; i++) {
665 		if ((val & tbl[i].flag) == 0) {
666 			(void) strlcat(buf, "-", buflen);
667 		} else {
668 			(void) strlcat(buf, tbl[i].name, buflen);
669 		}
670 		n++;
671 	}
672 }
673 
674 __NORETURN static void
675 ofmt_fatal(ofmt_handle_t hdl, ofmt_field_t *templ, ofmt_status_t status)
676 {
677 	char buf[OFMT_BUFSIZE];
678 	char *msg = ofmt_strerror(hdl, status, buf, sizeof (buf));
679 
680 	warnx(_("ofmt error: %s"), msg);
681 
682 	if (status == OFMT_EBADFIELDS ||
683 	    status == OFMT_ENOFIELDS) {
684 		ofmt_field_t *f = templ;
685 		fprintf(stderr, _("Valid fields are: "));
686 		while (f->of_name != NULL) {
687 			fprintf(stderr, "%s", f->of_name);
688 			f++;
689 			if (f->of_name != NULL)
690 				fprintf(stderr, ",");
691 		}
692 		fprintf(stderr, "\n");
693 	}
694 
695 	exit(EXIT_FAILURE);
696 }
697 
698 __NORETURN static void
699 fatal(const char *msg, ...)
700 {
701 	char buf[128];
702 	va_list ap;
703 	size_t len;
704 
705 	va_start(ap, msg);
706 	(void) vsnprintf(buf, sizeof (buf), msg, ap);
707 	va_end(ap);
708 
709 	len = strlen(buf);
710 	upanic(buf, len);
711 }
712