xref: /illumos-gate/usr/src/cmd/smbsrv/smbadm/smbinfo.c (revision 02ac56e010f18fc0c5aafe47377586d8ba8c897c)
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 		fprintf(stderr, _("Missing file id\n"));
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 			fprintf(stderr, _("Invalid file id '%s'"), argv[i]);
301 			return (2);
302 		}
303 #ifdef _LP64
304 		if (ul > UINT32_MAX) {
305 			fprintf(stderr, _("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 			warnx(_("Closing fid %s failed: %s"), argv[i],
322 			    strerror(rc));
323 			errs++;
324 		}
325 	}
326 
327 	if (errs > 0)
328 		return (1);
329 	return (0);
330 }
331 
332 int
333 cmd_close_sess(int argc, char **argv)
334 {
335 	const char *client;
336 	const char *user = NULL;
337 	int rc;
338 
339 	if (argc < 2) {
340 		fprintf(stderr, _("clientname and username missing\n"));
341 		return (2);
342 	}
343 	client = argv[1];
344 	if (argc > 2) {
345 		user = argv[2];
346 	}
347 
348 	/*
349 	 * See SMB_IOC_SESSION_CLOSE (ioc.client, ioc.username)
350 	 * and smb_server_session_close().  The "client" part
351 	 * can be EITHER the "workstation" or the IP address,
352 	 * as shown in the "COMPUTER" and "IP" fields in the
353 	 * output of "list-sessions".   The (optional) "user"
354 	 * part is as shown in "USER" part of that output.
355 	 */
356 	rc = smb_kmod_session_close(client, user);
357 	if (rc != 0) {
358 		rc = 1;
359 	}
360 	return (rc);
361 }
362 
363 static boolean_t
364 fmt_user(ofmt_arg_t *arg, char *buf, uint_t buflen)
365 {
366 	smb_netuserinfo_t	*ui = arg->ofmt_cbarg;
367 	user_field_t		field = (user_field_t)arg->ofmt_id;
368 
369 	switch (field) {
370 	case UF_SESS_ID:
371 		(void) snprintf(buf, buflen, "%" PRIu64, ui->ui_session_id);
372 		break;
373 	case UF_DOMAIN:
374 		print_str(ui->ui_domain, buf, buflen);
375 		break;
376 	case UF_ACCOUNT:
377 		print_str(ui->ui_account, buf, buflen);
378 		break;
379 	case UF_USER:
380 		(void) snprintf(buf, buflen, "%s\\%s", ui->ui_domain,
381 		    ui->ui_account);
382 		break;
383 	case UF_UID:
384 		VERIFY3U(arg->ofmt_width, <, INT_MAX);
385 		(void) snprintf(buf, buflen, "%u", ui->ui_posix_uid);
386 		break;
387 	case UF_WORKSTATION:
388 		print_str(ui->ui_workstation, buf, buflen);
389 		break;
390 	case UF_IP:
391 		(void) smb_inet_ntop(&ui->ui_ipaddr, buf, buflen);
392 		break;
393 	case UF_OS:
394 		/* XXX: Lookup string value */
395 		(void) snprintf(buf, buflen, "%" PRId32, ui->ui_native_os);
396 		break;
397 	case UF_LOGON_TIME:
398 		print_time(ui->ui_logon_time, TIME_FMT, buf, buflen);
399 		break;
400 	case UF_AGE:
401 		print_age(now - ui->ui_logon_time, buf, buflen);
402 		break;
403 	case UF_NUMOPEN:
404 		print_u32(ui->ui_numopens, buf, buflen);
405 		break;
406 	case UF_FLAGS:
407 		print_flags(user_flag_tbl, ARRAY_SIZE(user_flag_tbl),
408 		    ui->ui_flags, buf, buflen);
409 		break;
410 	default:
411 		fatal("%s: invalid field %d", __func__, field);
412 	}
413 
414 	return (B_TRUE);
415 }
416 
417 static boolean_t
418 fmt_tree_type(uint32_t type, char *buf, uint_t buflen)
419 {
420 	switch (type & STYPE_MASK) {
421 	case STYPE_DISKTREE:
422 		(void) strlcpy(buf, "DISK", buflen);
423 		break;
424 	case STYPE_PRINTQ:
425 		(void) strlcpy(buf, "PRINTQ", buflen);
426 		break;
427 	case STYPE_DEVICE:
428 		(void) strlcpy(buf, "DEVICE", buflen);
429 		break;
430 	case STYPE_IPC:
431 		(void) strlcpy(buf, "IPC", buflen);
432 		break;
433 	default:
434 		(void) snprintf(buf, buflen, "%" PRIx32, type & STYPE_MASK);
435 		break;
436 	}
437 
438 	return (B_TRUE);
439 }
440 
441 static boolean_t
442 fmt_tree(ofmt_arg_t *arg, char *buf, uint_t buflen)
443 {
444 	smb_netconnectinfo_t	*nc = arg->ofmt_cbarg;
445 	tree_field_t		field = (tree_field_t)arg->ofmt_id;
446 
447 	switch (field) {
448 	case TF_ID:
449 		(void) snprintf(buf, buflen, "%" PRIu32, nc->ci_id);
450 		break;
451 	case TF_TYPE:
452 		return (fmt_tree_type(nc->ci_type, buf, buflen));
453 	case TF_NUMOPEN:
454 		print_u32(nc->ci_numopens, buf, buflen);
455 		break;
456 	case TF_NUMUSERS:
457 		print_u32(nc->ci_numusers, buf, buflen);
458 		break;
459 	case TF_TIME:
460 		print_time(now - nc->ci_time, TIME_FMT, buf, buflen);
461 		break;
462 	case TF_AGE:
463 		print_age(nc->ci_time, buf, buflen);
464 		break;
465 	case TF_USERNAME:
466 		print_str(nc->ci_username, buf, buflen);
467 		break;
468 	case TF_SHARE:
469 		print_str(nc->ci_share, buf, buflen);
470 		break;
471 	default:
472 		fatal("%s: invalid field %d", __func__, field);
473 	}
474 
475 	return (B_TRUE);
476 }
477 
478 static boolean_t
479 fmt_netfileinfo(ofmt_arg_t *arg, char *buf, uint_t buflen)
480 {
481 	smb_netfileinfo_t	*fi = arg->ofmt_cbarg;
482 	netfileinfo_field_t	field = (netfileinfo_field_t)arg->ofmt_id;
483 
484 	switch (field) {
485 	case NFIF_FID:
486 		(void) snprintf(buf, buflen, "%" PRIu16, fi->fi_fid);
487 		break;
488 	case NFIF_UNIQID:
489 		(void) snprintf(buf, buflen, "%" PRIu32, fi->fi_uniqid);
490 		break;
491 	case NFIF_PERMS:
492 		print_perms(nfi_perm_tbl, ARRAY_SIZE(nfi_perm_tbl),
493 		    fi->fi_permissions, buf, buflen);
494 		break;
495 	case NFIF_NUMLOCKS:
496 		print_u32(fi->fi_numlocks, buf, buflen);
497 		break;
498 	case NFIF_PATH:
499 		print_str(fi->fi_path, buf, buflen);
500 		break;
501 	case NFIF_USERNAME:
502 		print_str(fi->fi_username, buf, buflen);
503 		break;
504 	default:
505 		fatal("%s: invalid field %d", __func__, field);
506 	}
507 
508 	return (B_TRUE);
509 }
510 
511 static int
512 do_enum(smb_svcenum_t *req, ofmt_handle_t hdl)
513 {
514 	smb_netsvc_t		*ns;
515 	smb_netsvcitem_t	*item;
516 	uint32_t		n = 0;
517 	int rc;
518 
519 	if (hdl == NULL)
520 		return (2);	/* exit (2) -- usage */
521 	now = time(NULL);
522 
523 	for (;;) {
524 		req->se_nskip = n;
525 
526 		ns = smb_kmod_enum_init(req);
527 		if (ns == NULL) {
528 			fprintf(stderr, _("SMB enum initialization failure"));
529 			return (1);
530 		}
531 
532 		rc = smb_kmod_enum(ns);
533 		if (rc != 0) {
534 			/*
535 			 * When the SMB service is not running, expect ENXIO.
536 			 */
537 			if (rc == ENXIO) {
538 				fprintf(stderr,
539 				    _("Kernel SMB server not running"));
540 				return (1);
541 			}
542 			fprintf(stderr, _("SMB enumeration call failed: %s"),
543 			    strerror(rc));
544 			return (1);
545 		}
546 
547 		if (list_is_empty(&ns->ns_list))
548 			break;
549 
550 		for (item = list_head(&ns->ns_list); item != NULL;
551 		    item = list_next(&ns->ns_list, item)) {
552 			ofmt_print(hdl, &item->nsi_un);
553 			n++;
554 		}
555 
556 		smb_kmod_enum_fini(ns);
557 	}
558 	return (0);
559 }
560 
561 static void
562 print_str(const char *restrict src, char *restrict buf, uint_t buflen)
563 {
564 	if (src == NULL) {
565 		buf[0] = '\0';
566 		return;
567 	}
568 	(void) strlcpy(buf, src, buflen);
569 }
570 
571 static void
572 print_u32(uint32_t val, char *buf, uint_t buflen)
573 {
574 	const char *fmt = opt_p ? "%" PRIu32 : "%'" PRIu32;
575 
576 	(void) snprintf(buf, buflen, fmt, val);
577 }
578 
579 static void
580 print_age(time_t amt, char *buf, uint_t buflen)
581 {
582 	uint32_t days = 0, hours = 0, mins = 0;
583 
584 	if (opt_p) {
585 		(void) snprintf(buf, buflen, "%" PRId64, (int64_t)amt);
586 		return;
587 	}
588 
589 	if (amt >= DAYS) {
590 		days = amt / DAYS;
591 		amt %= DAYS;
592 	}
593 	if (amt >= HRS) {
594 		hours = amt / HRS;
595 		amt %= HRS;
596 	}
597 	if (amt >= MINS) {
598 		mins = amt / MINS;
599 		amt %= MINS;
600 	}
601 
602 	if (days > 0) {
603 		int n = snprintf(buf, buflen, "%" PRIu32 " days%s",
604 		    days, (hours > 0 || mins > 0 || amt > 0) ? ", " : "");
605 
606 		VERIFY3U(buflen, >, n);
607 
608 		buf += n;
609 		buflen -= n;
610 	}
611 
612 	(void) snprintf(buf, buflen, "%02" PRIu32 ":%02" PRIu32 ":%02" PRIu32,
613 	    hours, mins, amt);
614 }
615 
616 static void
617 print_time(time_t when, const char *fmt, char *buf, uint_t buflen)
618 {
619 	const struct tm *tm;
620 
621 	if (opt_p) {
622 		(void) snprintf(buf, buflen, "%" PRId64, (int64_t)when);
623 		return;
624 	}
625 
626 	tm = localtime(&when);
627 	(void) strftime(buf, buflen - 1, fmt, tm);
628 }
629 
630 static void
631 print_flags(struct flag_tbl *tbl, size_t nent, uint32_t val, char *buf,
632     uint_t buflen)
633 {
634 	uint_t n = 0;
635 	uint_t i;
636 
637 	if (opt_x) {
638 		(void) snprintf(buf, buflen, "%" PRIx32, val);
639 		return;
640 	}
641 
642 	for (i = 0; i < nent; i++) {
643 		if ((val & tbl[i].flag) == 0)
644 			continue;
645 		if (n > 0)
646 			(void) strlcat(buf, ",", buflen);
647 		(void) strlcat(buf, tbl[i].name, buflen);
648 		n++;
649 	}
650 
651 	if (n == 0)
652 		(void) strlcat(buf, "-", buflen);
653 }
654 
655 static void
656 print_perms(struct flag_tbl *tbl, size_t nent, uint32_t val, char *buf,
657     uint_t buflen)
658 {
659 	uint_t n = 0;
660 	uint_t i;
661 
662 	if (opt_x) {
663 		(void) snprintf(buf, buflen, "%" PRIx32, val);
664 		return;
665 	}
666 
667 	for (i = 0; i < nent; i++) {
668 		if ((val & tbl[i].flag) == 0) {
669 			(void) strlcat(buf, "-", buflen);
670 		} else {
671 			(void) strlcat(buf, tbl[i].name, buflen);
672 		}
673 		n++;
674 	}
675 }
676 
677 __NORETURN static void
678 ofmt_fatal(ofmt_handle_t hdl, ofmt_field_t *templ, ofmt_status_t status)
679 {
680 	char buf[OFMT_BUFSIZE];
681 	char *msg = ofmt_strerror(hdl, status, buf, sizeof (buf));
682 
683 	fprintf(stderr, _("ofmt error: %s\n"), msg);
684 
685 	if (status == OFMT_EBADFIELDS ||
686 	    status == OFMT_ENOFIELDS) {
687 		ofmt_field_t *f = templ;
688 		fprintf(stderr, _("Valid fields are: "));
689 		while (f->of_name != NULL) {
690 			fprintf(stderr, "%s", f->of_name);
691 			f++;
692 			if (f->of_name != NULL)
693 				fprintf(stderr, ",");
694 		}
695 		fprintf(stderr, "\n");
696 	}
697 
698 	exit(EXIT_FAILURE);
699 }
700 
701 __NORETURN static void
702 fatal(const char *msg, ...)
703 {
704 	char buf[128];
705 	va_list ap;
706 	size_t len;
707 
708 	va_start(ap, msg);
709 	(void) vsnprintf(buf, sizeof (buf), msg, ap);
710 	va_end(ap);
711 
712 	len = strlen(buf);
713 	upanic(buf, len);
714 }
715