xref: /netbsd/external/bsd/cron/dist/do_command.c (revision bd4a2c22)
1 /*	$NetBSD: do_command.c,v 1.15 2020/04/18 19:32:19 christos Exp $	*/
2 
3 /* Copyright 1988,1990,1993,1994 by Paul Vixie
4  * All rights reserved
5  */
6 
7 /*
8  * Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC")
9  * Copyright (c) 1997,2000 by Internet Software Consortium, Inc.
10  *
11  * Permission to use, copy, modify, and distribute this software for any
12  * purpose with or without fee is hereby granted, provided that the above
13  * copyright notice and this permission notice appear in all copies.
14  *
15  * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES
16  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
17  * MERCHANTABILITY AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR
18  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
19  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
20  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
21  * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22  */
23 #include <sys/cdefs.h>
24 #if !defined(lint) && !defined(LINT)
25 #if 0
26 static char rcsid[] = "Id: do_command.c,v 1.9 2004/01/23 18:56:42 vixie Exp";
27 #else
28 __RCSID("$NetBSD: do_command.c,v 1.15 2020/04/18 19:32:19 christos Exp $");
29 #endif
30 #endif
31 
32 #include "cron.h"
33 #include <unistd.h>
34 
35 static int		child_process(entry *);
36 static int		safe_p(const char *, const char *);
37 
38 pid_t
do_command(entry * e,user * u)39 do_command(entry *e, user *u) {
40 	int retval;
41 
42 	Debug(DPROC, ("[%ld] do_command(%s, (%s,%ld,%ld))\n",
43 		      (long)getpid(), e->cmd, u->name,
44 		      (long)e->pwd->pw_uid, (long)e->pwd->pw_gid));
45 
46 	/* fork to become asynchronous -- parent process is done immediately,
47 	 * and continues to run the normal cron code, which means return to
48 	 * tick().  the child and grandchild don't leave this function, alive.
49 	 *
50 	 * vfork() is unsuitable, since we have much to do, and the parent
51 	 * needs to be able to run off and fork other processes.
52 	 */
53 
54 	pid_t	jobpid;
55 	switch (jobpid = fork()) {
56 	case -1:
57 		log_it("CRON", getpid(), "error", "can't fork");
58 		break;
59 	case 0:
60 		/* child process */
61 		acquire_daemonlock(1);
62 		retval = child_process(e);
63 		Debug(DPROC, ("[%ld] child process done (rc=%d), exiting\n",
64 			      (long)getpid(), retval));
65 		_exit(retval);
66 		break;
67 	default:
68 		/* parent process */
69 		if ((e->flags & SINGLE_JOB) == 0)
70 			jobpid = -1;
71 		break;
72 	}
73 	Debug(DPROC, ("[%ld] main process returning to work\n",(long)getpid()));
74 
75 	/* only return pid if a singleton */
76 	return jobpid;
77 }
78 
79 static void
sigchld_handler(int signo)80 sigchld_handler(int signo) {
81 	for (;;) {
82 		WAIT_T waiter;
83 		PID_T pid = waitpid(-1, &waiter, WNOHANG);
84 
85 		switch (pid) {
86 		case -1:
87 			if (errno == EINTR)
88 				continue;
89 		case 0:
90 			return;
91 		default:
92 			break;
93 		}
94 	}
95 }
96 
97 static void
write_data(char * volatile input_data,int * stdin_pipe,int * stdout_pipe)98 write_data(char *volatile input_data, int *stdin_pipe, int *stdout_pipe)
99 {
100 	FILE *out = fdopen(stdin_pipe[WRITE_PIPE], "w");
101 	int need_newline = FALSE;
102 	int escaped = FALSE;
103 	int ch;
104 
105 	Debug(DPROC, ("[%ld] child2 sending data to grandchild\n",
106 		      (long)getpid()));
107 
108 #ifdef USE_PAM
109 	cron_pam_child_close();
110 #else
111 	log_close();
112 #endif
113 
114 	/* close the pipe we don't use, since we inherited it and
115 	 * are part of its reference count now.
116 	 */
117 	(void)close(stdout_pipe[READ_PIPE]);
118 
119 	/* translation:
120 	 *	\% -> %
121 	 *	%  -> \n
122 	 *	\x -> \x	for all x != %
123 	 */
124 	while ((ch = *input_data++) != '\0') {
125 		if (escaped) {
126 			if (ch != '%')
127 				(void)putc('\\', out);
128 		} else {
129 			if (ch == '%')
130 				ch = '\n';
131 		}
132 
133 		if (!(escaped = (ch == '\\'))) {
134 			(void)putc(ch, out);
135 			need_newline = (ch != '\n');
136 		}
137 	}
138 	if (escaped)
139 		(void)putc('\\', out);
140 	if (need_newline)
141 		(void)putc('\n', out);
142 
143 	/* close the pipe, causing an EOF condition.  fclose causes
144 	 * stdin_pipe[WRITE_PIPE] to be closed, too.
145 	 */
146 	(void)fclose(out);
147 
148 	Debug(DPROC, ("[%ld] child2 done sending to grandchild\n",
149 		      (long)getpid()));
150 }
151 
152 static int
read_data(entry * e,const char * mailto,const char * usernm,char ** envp,int * stdout_pipe,pid_t jobpid)153 read_data(entry *e, const char *mailto, const char *usernm, char **envp,
154     int *stdout_pipe, pid_t jobpid)
155 {
156 	FILE	*in = fdopen(stdout_pipe[READ_PIPE], "r");
157 	FILE	*mail = NULL;
158 	int	bytes = 1;
159 	int	status = 0;
160 	int	ch = getc(in);
161 	int	retval = 0;
162 	sig_t	oldchld = NULL;
163 
164 	if (ch == EOF)
165 		goto out;
166 
167 	Debug(DPROC|DEXT, ("[%ld] got data (%x:%c) from grandchild\n",
168 	    (long)getpid(), ch, ch));
169 
170 	/* get name of recipient.  this is MAILTO if set to a
171 	 * valid local username; USER otherwise.
172 	 */
173 	if (mailto) {
174 		/* MAILTO was present in the environment
175 		 */
176 		if (!*mailto) {
177 			/* ... but it's empty. set to NULL
178 			 */
179 			mailto = NULL;
180 		}
181 	} else {
182 		/* MAILTO not present, set to USER.
183 		 */
184 		mailto = usernm;
185 	}
186 
187 	/*
188 	 * Unsafe, disable mailing.
189 	 */
190 	if (mailto && !safe_p(usernm, mailto))
191 		mailto = NULL;
192 
193 	/* if we are supposed to be mailing, MAILTO will
194 	 * be non-NULL.  only in this case should we set
195 	 * up the mail command and subjects and stuff...
196 	 */
197 
198 	if (mailto) {
199 		char	**env;
200 		char	mailcmd[MAX_COMMAND];
201 		char	hostname[MAXHOSTNAMELEN + 1];
202 
203 		(void)gethostname(hostname, MAXHOSTNAMELEN);
204 		if (strlens(MAILFMT, MAILARG, NULL) + 1 >= sizeof mailcmd) {
205 			log_it(usernm, getpid(), "MAIL", "mailcmd too long");
206 			retval = ERROR_EXIT;
207 			goto out;
208 		}
209 		(void)snprintf(mailcmd, sizeof(mailcmd), MAILFMT, MAILARG);
210 		oldchld = signal(SIGCHLD, SIG_DFL);
211 		if (!(mail = cron_popen(mailcmd, "w", e->pwd))) {
212 			log_itx(usernm, getpid(), "MAIL",
213 			    "cannot run `%s'", mailcmd);
214 			(void) signal(SIGCHLD, oldchld);
215 			retval = ERROR_EXIT;
216 			goto out;
217 		}
218 		(void)fprintf(mail, "From: root (Cron Daemon)\n");
219 		(void)fprintf(mail, "To: %s\n", mailto);
220 		(void)fprintf(mail, "Subject: Cron <%s@%s> %s\n",
221 		    usernm, hostname, e->cmd);
222 		(void)fprintf(mail, "Auto-Submitted: auto-generated\n");
223 #ifdef MAIL_DATE
224 		(void)fprintf(mail, "Date: %s\n", arpadate(&StartTime));
225 #endif /*MAIL_DATE*/
226 		for (env = envp;  *env;  env++)
227 			(void)fprintf(mail, "X-Cron-Env: <%s>\n", *env);
228 		(void)fprintf(mail, "\n");
229 
230 		/* this was the first char from the pipe
231 		 */
232 		(void)putc(ch, mail);
233 	}
234 
235 	/* we have to read the input pipe no matter whether
236 	 * we mail or not, but obviously we only write to
237 	 * mail pipe if we ARE mailing.
238 	 */
239 
240 	while (EOF != (ch = getc(in))) {
241 		bytes++;
242 		if (mailto)
243 			(void)putc(ch, mail);
244 	}
245 
246 	/* only close pipe if we opened it -- i.e., we're
247 	 * mailing...
248 	 */
249 
250 	if (mailto) {
251 		if (e->flags & MAIL_WHEN_ERR) {
252 			int jstatus = -1;
253 			if (jobpid <= 0)
254 				log_it("CRON", getpid(), "error",
255 				    "no job pid");
256 			else {
257 				while (waitpid(jobpid, &jstatus, WNOHANG) == -1)
258 					if (errno != EINTR) {
259 						log_it("CRON", getpid(),
260 						    "error", "no job pid");
261 						break;
262 					}
263 			}
264 			/* If everything went well, and -n was set, _and_ we
265 			 * have mail, we won't be mailing... so shoot the
266 			 * messenger!
267 			 */
268 			if (WIFEXITED(jstatus) && WEXITSTATUS(jstatus) == 0) {
269 				Debug(DPROC, ("[%ld] aborting pipe to mail\n",
270 				    (long)getpid()));
271 				status = cron_pabort(mail);
272 				mailto = NULL;
273 			}
274 		}
275 
276 		if (mailto) {
277 			Debug(DPROC, ("[%ld] closing pipe to mail\n",
278 			    (long)getpid()));
279 			/* Note: the pclose will probably see
280 			 * the termination of the grandchild
281 			 * in addition to the mail process, since
282 			 * it (the grandchild) is likely to exit
283 			 * after closing its stdout.
284 			 */
285 			status = cron_pclose(mail);
286 			mail = NULL;
287 		}
288 		(void) signal(SIGCHLD, oldchld);
289 	}
290 
291 	/* if there was output and we could not mail it,
292 	 * log the facts so the poor user can figure out
293 	 * what's going on.
294 	 */
295 	if (mailto && status) {
296 		log_itx(usernm, getpid(), "MAIL",
297 		    "mailed %d byte%s of output to `%s' but"
298 		    " got status %#04x", bytes,
299 		    bytes == 1 ? "" : "s", mailto, status);
300 	}
301 
302 out:
303 	Debug(DPROC, ("[%ld] got EOF from grandchild\n", (long)getpid()));
304 
305 	(void)fclose(in);	/* also closes stdout_pipe[READ_PIPE] */
306 	return retval;
307 }
308 
309 extern char **environ;
310 static int
exec_user_command(entry * e,char ** envp,char * usernm,int * stdin_pipe,int * stdout_pipe,pid_t * jobpid)311 exec_user_command(entry *e, char **envp, char *usernm, int *stdin_pipe,
312     int *stdout_pipe, pid_t *jobpid)
313 {
314 	char *homedir;
315 	char * volatile *ep;
316 
317 	switch (*jobpid = vfork()) {
318 	case -1:
319 		return -1;
320 	case 0:
321 		ep = envp;
322 		Debug(DPROC, ("[%ld] grandchild process vfork()'ed\n",
323 			      (long)getpid()));
324 
325 		/* write a log message.  we've waited this long to do it
326 		 * because it was not until now that we knew the PID that
327 		 * the actual user command shell was going to get and the
328 		 * PID is part of the log message.
329 		 */
330 		if ((e->flags & DONT_LOG) == 0) {
331 			char *x = mkprints(e->cmd, strlen(e->cmd));
332 
333 			log_it(usernm, getpid(), "CMD START", x);
334 			free(x);
335 		}
336 
337 		/* that's the last thing we'll log.  close the log files.
338 		 */
339 		log_close();
340 
341 		/* get new pgrp, void tty, etc.
342 		 */
343 		if (setsid() == -1)
344 			syslog(LOG_ERR, "setsid() failure: %m");
345 
346 		/* close the pipe ends that we won't use.  this doesn't affect
347 		 * the parent, who has to read and write them; it keeps the
348 		 * kernel from recording us as a potential client TWICE --
349 		 * which would keep it from sending SIGPIPE in otherwise
350 		 * appropriate circumstances.
351 		 */
352 		(void)close(stdin_pipe[WRITE_PIPE]);
353 		(void)close(stdout_pipe[READ_PIPE]);
354 
355 		/* grandchild process.  make std{in,out} be the ends of
356 		 * pipes opened by our daddy; make stderr go to stdout.
357 		 */
358 		if (stdin_pipe[READ_PIPE] != STDIN) {
359 			(void)dup2(stdin_pipe[READ_PIPE], STDIN);
360 			(void)close(stdin_pipe[READ_PIPE]);
361 		}
362 		if (stdout_pipe[WRITE_PIPE] != STDOUT) {
363 			(void)dup2(stdout_pipe[WRITE_PIPE], STDOUT);
364 			(void)close(stdout_pipe[WRITE_PIPE]);
365 		}
366 		(void)dup2(STDOUT, STDERR);
367 
368 		/* set our directory, uid and gid.  Set gid first, since once
369 		 * we set uid, we've lost root privledges.
370 		 */
371 #ifdef LOGIN_CAP
372 		{
373 #ifdef BSD_AUTH
374 			auth_session_t *as;
375 #endif
376 			login_cap_t *lc;
377 			char *p;
378 
379 			if ((lc = login_getclass(e->pwd->pw_class)) == NULL) {
380 				warnx("unable to get login class for `%s'",
381 				    e->pwd->pw_name);
382 				_exit(ERROR_EXIT);
383 			}
384 			if (setusercontext(lc, e->pwd, e->pwd->pw_uid, LOGIN_SETALL) < 0) {
385 				warnx("setusercontext failed for `%s'",
386 				    e->pwd->pw_name);
387 				_exit(ERROR_EXIT);
388 			}
389 #ifdef BSD_AUTH
390 			as = auth_open();
391 			if (as == NULL || auth_setpwd(as, e->pwd) != 0) {
392 				warn("can't malloc");
393 				_exit(ERROR_EXIT);
394 			}
395 			if (auth_approval(as, lc, usernm, "cron") <= 0) {
396 				warnx("approval failed for `%s'",
397 				    e->pwd->pw_name);
398 				_exit(ERROR_EXIT);
399 			}
400 			auth_close(as);
401 #endif /* BSD_AUTH */
402 			login_close(lc);
403 
404 			/* If no PATH specified in crontab file but
405 			 * we just added one via login.conf, add it to
406 			 * the crontab environment.
407 			 */
408 			if (env_get("PATH", envp) == NULL && environ != NULL) {
409 				if ((p = getenv("PATH")) != NULL)
410 					ep = env_set(envp, p);
411 			}
412 		}
413 #else
414 		if (setgid(e->pwd->pw_gid) != 0) {
415 			syslog(LOG_ERR, "setgid(%d) failed for %s: %m",
416 			    e->pwd->pw_gid, e->pwd->pw_name);
417 			_exit(ERROR_EXIT);
418 		}
419 		if (initgroups(usernm, e->pwd->pw_gid) != 0) {
420 			syslog(LOG_ERR, "initgroups(%s, %d) failed for %s: %m",
421 			    usernm, e->pwd->pw_gid, e->pwd->pw_name);
422 			_exit(ERROR_EXIT);
423 		}
424 #if (defined(BSD)) && (BSD >= 199103)
425 		if (setlogin(usernm) < 0) {
426 			syslog(LOG_ERR, "setlogin(%s) failure for %s: %m",
427 			    usernm, e->pwd->pw_name);
428 			_exit(ERROR_EXIT);
429 		}
430 #endif /* BSD */
431 #ifdef USE_PAM
432 		if (!cron_pam_setcred())
433 			_exit(ERROR_EXIT);
434 		cron_pam_child_close();
435 #endif
436 		if (setuid(e->pwd->pw_uid) != 0) {
437 			syslog(LOG_ERR, "setuid(%d) failed for %s: %m",
438 			    e->pwd->pw_uid, e->pwd->pw_name);
439 			_exit(ERROR_EXIT);
440 		}
441 		/* we aren't root after this... */
442 #endif /* LOGIN_CAP */
443 		homedir = env_get("HOME", __UNVOLATILE(ep));
444 		if (chdir(homedir) != 0) {
445 			syslog(LOG_ERR, "chdir(%s) $HOME failed for %s: %m",
446 			    homedir, e->pwd->pw_name);
447 			_exit(ERROR_EXIT);
448 		}
449 
450 #ifdef USE_SIGCHLD
451 		/* our grandparent is watching for our death by catching
452 		 * SIGCHLD.  the parent is ignoring SIGCHLD's; we want
453 		 * to restore default behaviour.
454 		 */
455 		(void) signal(SIGCHLD, SIG_DFL);
456 #endif
457 		(void) signal(SIGPIPE, SIG_DFL);
458 		(void) signal(SIGUSR1, SIG_DFL);
459 		(void) signal(SIGHUP, SIG_DFL);
460 
461 		/*
462 		 * Exec the command.
463 		 */
464 		{
465 			char	*shell = env_get("SHELL", __UNVOLATILE(ep));
466 
467 # if DEBUGGING
468 			if (DebugFlags & DTEST) {
469 				(void)fprintf(stderr,
470 				"debug DTEST is on, not exec'ing command.\n");
471 				(void)fprintf(stderr,
472 				"\tcmd='%s' shell='%s'\n", e->cmd, shell);
473 				_exit(OK_EXIT);
474 			}
475 # endif /*DEBUGGING*/
476 			(void)execle(shell, shell, "-c", e->cmd, NULL, envp);
477 			warn("execl: couldn't exec `%s'", shell);
478 			_exit(ERROR_EXIT);
479 		}
480 		return 0;
481 	default:
482 		/* parent process */
483 		return 0;
484 	}
485 }
486 
487 static int
child_process(entry * e)488 child_process(entry *e) {
489 	int stdin_pipe[2], stdout_pipe[2];
490 	char * volatile input_data;
491 	char *usernm, * volatile mailto;
492 	struct sigaction sact;
493 	char **envp = e->envp;
494 	int retval = OK_EXIT;
495 	pid_t jobpid = 0;
496 
497 	Debug(DPROC, ("[%ld] child_process('%s')\n", (long)getpid(), e->cmd));
498 
499 	setproctitle("running job");
500 
501 	/* discover some useful and important environment settings
502 	 */
503 	usernm = e->pwd->pw_name;
504 	mailto = env_get("MAILTO", envp);
505 
506 	memset(&sact, 0, sizeof(sact));
507 	sigemptyset(&sact.sa_mask);
508 	sact.sa_flags = 0;
509 #ifdef SA_RESTART
510 	sact.sa_flags |= SA_RESTART;
511 #endif
512 	sact.sa_handler = sigchld_handler;
513 	(void) sigaction(SIGCHLD, &sact, NULL);
514 
515 	/* create some pipes to talk to our future child
516 	 */
517 	if (pipe(stdin_pipe) == -1) 	/* child's stdin */
518 		log_it("CRON", getpid(), "error", "create child stdin pipe");
519 	if (pipe(stdout_pipe) == -1)	/* child's stdout */
520 		log_it("CRON", getpid(), "error", "create child stdout pipe");
521 
522 	/* since we are a forked process, we can diddle the command string
523 	 * we were passed -- nobody else is going to use it again, right?
524 	 *
525 	 * if a % is present in the command, previous characters are the
526 	 * command, and subsequent characters are the additional input to
527 	 * the command.  An escaped % will have the escape character stripped
528 	 * from it.  Subsequent %'s will be transformed into newlines,
529 	 * but that happens later.
530 	 */
531 	/*local*/{
532 		int escaped = FALSE;
533 		int ch;
534 		char *p;
535 
536 		/* translation:
537 		 *	\% -> %
538 		 *	%  -> end of command, following is command input.
539 		 *	\x -> \x	for all x != %
540 		 */
541 		input_data = p = e->cmd;
542 		while ((ch = *input_data++) != '\0') {
543  			if (escaped) {
544 				if (ch != '%')
545 					*p++ = '\\';
546 			} else {
547 				if (ch == '%') {
548 					break;
549 				}
550 			}
551 
552 			if (!(escaped = (ch == '\\'))) {
553 				*p++ = (char)ch;
554 			}
555 		}
556 		if (ch == '\0') {
557 			/* move pointer back, so that code below
558 			 * won't think we encountered % sequence */
559 			input_data--;
560 		}
561 		if (escaped)
562 			*p++ = '\\';
563 
564 		*p = '\0';
565 	}
566 
567 #ifdef USE_PAM
568 	if (!cron_pam_start(usernm))
569 		return ERROR_EXIT;
570 
571 	if (!(envp = cron_pam_getenvlist(envp))) {
572 		retval = ERROR_EXIT;
573 		goto child_process_end;
574 	}
575 #endif
576 
577 	/* fork again, this time so we can exec the user's command.
578 	 */
579 	if (exec_user_command(e, envp, usernm, stdin_pipe, stdout_pipe,
580 	    &jobpid) == -1) {
581 		retval = ERROR_EXIT;
582 		goto child_process_end;
583 	}
584 
585 
586 	/* middle process, child of original cron, parent of process running
587 	 * the user's command.
588 	 */
589 
590 	Debug(DPROC, ("[%ld] child continues, closing pipes\n",(long)getpid()));
591 
592 	/* close the ends of the pipe that will only be referenced in the
593 	 * grandchild process...
594 	 */
595 	(void)close(stdin_pipe[READ_PIPE]);
596 	(void)close(stdout_pipe[WRITE_PIPE]);
597 
598 	/*
599 	 * write, to the pipe connected to child's stdin, any input specified
600 	 * after a % in the crontab entry.  while we copy, convert any
601 	 * additional %'s to newlines.  when done, if some characters were
602 	 * written and the last one wasn't a newline, write a newline.
603 	 *
604 	 * Note that if the input data won't fit into one pipe buffer (2K
605 	 * or 4K on most BSD systems), and the child doesn't read its stdin,
606 	 * we would block here.  thus we must fork again.
607 	 */
608 
609 	if (*input_data) {
610 		switch (fork()) {
611 		case 0:
612 			write_data(input_data, stdin_pipe, stdout_pipe);
613 			exit(EXIT_SUCCESS);
614 		case -1:
615 			retval = ERROR_EXIT;
616 			goto child_process_end;
617 		default:
618 			break;
619 		}
620 	}
621 
622 	/* close the pipe to the grandkiddie's stdin, since its wicked uncle
623 	 * ernie back there has it open and will close it when he's done.
624 	 */
625 	(void)close(stdin_pipe[WRITE_PIPE]);
626 
627 	/*
628 	 * read output from the grandchild.  it's stderr has been redirected to
629 	 * it's stdout, which has been redirected to our pipe.  if there is any
630 	 * output, we'll be mailing it to the user whose crontab this is...
631 	 * when the grandchild exits, we'll get EOF.
632 	 */
633 
634 	Debug(DPROC, ("[%ld] child reading output from grandchild\n",
635 		      (long)getpid()));
636 
637 	retval = read_data(e, mailto, usernm, envp, stdout_pipe, jobpid);
638 	if (retval)
639 		goto child_process_end;
640 
641 
642 	/* wait for children to die.
643 	 */
644 	sigchld_handler(0);
645 
646 	/* Log the time when we finished deadling with the job */
647 	/*local*/{
648 		char *x = mkprints(e->cmd, strlen(e->cmd));
649 
650 		log_it(usernm, getpid(), "CMD FINISH", x);
651 		free(x);
652 	}
653 
654 child_process_end:
655 #ifdef USE_PAM
656 	cron_pam_finish();
657 #endif
658 	return retval;
659 }
660 
661 static int
safe_p(const char * usernm,const char * s)662 safe_p(const char *usernm, const char *s) {
663 	static const char safe_delim[] = "@!:%-.,";     /* conservative! */
664 	const char *t;
665 	int ch, first;
666 
667 	for (t = s, first = 1; (ch = *t++) != '\0'; first = 0) {
668 		if (isascii(ch) && isprint(ch) &&
669 		    (isalnum(ch) || (!first && strchr(safe_delim, ch))))
670 			continue;
671 		log_it(usernm, getpid(), "UNSAFE", s);
672 		return (FALSE);
673 	}
674 	return (TRUE);
675 }
676