remove xen support
[unleashed/tickless.git] / usr / src / cmd / su / su.c
blobc4c71c98c1b74971892bda9debdfa45eba9b5198
1 /*
2 * CDDL HEADER START
4 * The contents of this file are subject to the terms of the
5 * Common Development and Distribution License (the "License").
6 * You may not use this file except in compliance with the License.
8 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9 * or http://www.opensolaris.org/os/licensing.
10 * See the License for the specific language governing permissions
11 * and limitations under the License.
13 * When distributing Covered Code, include this CDDL HEADER in each
14 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15 * If applicable, add the following below this CDDL HEADER, with the
16 * fields enclosed by brackets "[]" replaced with your own identifying
17 * information: Portions Copyright [yyyy] [name of copyright owner]
19 * CDDL HEADER END
22 * Copyright (c) 1988, 2010, Oracle and/or its affiliates. All rights reserved.
23 * Copyright 2012 Milan Jurik. All rights reserved.
24 * Copyright 2014 Nexenta Systems, Inc.
26 /* Copyright (c) 1984, 1986, 1987, 1988, 1989 AT&T */
27 /* All Rights Reserved */
29 /* Copyright (c) 1987, 1988 Microsoft Corporation */
30 /* All Rights Reserved */
33 * su [-] [name [arg ...]] change userid, `-' changes environment.
34 * If SULOG is defined, all attempts to su to another user are
35 * logged there.
36 * If CONSOLE is defined, all successful attempts to su to uid 0
37 * are also logged there.
39 * If su cannot create, open, or write entries into SULOG,
40 * (or on the CONSOLE, if defined), the entry will not
41 * be logged -- thus losing a record of the su's attempted
42 * during this period.
45 #include <stdio.h>
46 #include <sys/types.h>
47 #include <sys/stat.h>
48 #include <sys/param.h>
49 #include <unistd.h>
50 #include <stdlib.h>
51 #include <crypt.h>
52 #include <pwd.h>
53 #include <shadow.h>
54 #include <time.h>
55 #include <signal.h>
56 #include <fcntl.h>
57 #include <string.h>
58 #include <locale.h>
59 #include <syslog.h>
60 #include <sys/utsname.h>
61 #include <sys/wait.h>
62 #include <grp.h>
63 #include <deflt.h>
64 #include <limits.h>
65 #include <errno.h>
66 #include <stdarg.h>
67 #include <user_attr.h>
68 #include <priv.h>
70 #include <bsm/adt.h>
71 #include <bsm/adt_event.h>
73 #include <security/pam_appl.h>
75 #define PATH "/usr/bin:" /* path for users other than root */
76 #define SUPATH "/usr/sbin:/usr/bin" /* path for root */
77 #define SUPRMT "PS1=# " /* primary prompt for root */
78 #define ELIM 128
79 #define ROOT 0
80 #ifdef DYNAMIC_SU
81 #define EMBEDDED_NAME "embedded_su"
82 #define DEF_ATTEMPTS 3 /* attempts to change password */
83 #endif /* DYNAMIC_SU */
85 #define PW_FALSE 1 /* no password change */
86 #define PW_TRUE 2 /* successful password change */
87 #define PW_FAILED 3 /* failed password change */
90 * Intervals to sleep after failed su
92 #ifndef SLEEPTIME
93 #define SLEEPTIME 4
94 #endif
96 #define DEFAULT_LOGIN "/etc/default/login"
97 #define DEFFILE "/etc/default/su"
100 char *Sulog, *Console;
101 char *Path, *Supath;
104 * Locale variables to be propagated to "su -" environment
106 static char *initvar;
107 static char *initenv[] = {
108 "TZ", "LANG", "LC_CTYPE",
109 "LC_NUMERIC", "LC_TIME", "LC_COLLATE",
110 "LC_MONETARY", "LC_MESSAGES", "LC_ALL", 0};
111 static char mail[30] = { "MAIL=/var/mail/" };
113 static void envalt(void);
114 static void log(char *, char *, int);
115 static void to(int);
117 enum messagemode { USAGE, ERR, WARN };
118 static void message(enum messagemode, char *, ...);
120 static char *alloc_vsprintf(const char *, va_list);
121 static char *tail(char *);
123 static void audit_success(int, struct passwd *);
124 static void audit_logout(adt_session_data_t *, au_event_t);
125 static void audit_failure(int, struct passwd *, char *, int);
127 #ifdef DYNAMIC_SU
128 static void validate(char *, int *);
129 static int legalenvvar(char *);
130 static int su_conv(int, struct pam_message **, struct pam_response **, void *);
131 static int emb_su_conv(int, struct pam_message **, struct pam_response **,
132 void *);
133 static void freeresponse(int, struct pam_response **response);
134 static struct pam_conv pam_conv = {su_conv, NULL};
135 static struct pam_conv emb_pam_conv = {emb_su_conv, NULL};
136 static void quotemsg(char *, ...);
137 static void readinitblock(void);
138 #else /* !DYNAMIC_SU */
139 static void update_audit(struct passwd *pwd);
140 #endif /* DYNAMIC_SU */
142 static pam_handle_t *pamh = NULL; /* Authentication handle */
143 struct passwd pwd;
144 char pwdbuf[1024]; /* buffer for getpwnam_r() */
145 char shell[] = "/usr/bin/sh"; /* default shell */
146 char safe_shell[] = "/sbin/sh"; /* "fallback" shell */
147 char su[PATH_MAX] = "su"; /* arg0 for exec of shprog */
148 char homedir[PATH_MAX] = "HOME=";
149 char logname[20] = "LOGNAME=";
150 char *suprmt = SUPRMT;
151 char termtyp[PATH_MAX] = "TERM=";
152 char *term;
153 char shelltyp[PATH_MAX] = "SHELL=";
154 char *hz;
155 char tznam[PATH_MAX];
156 char hzname[10] = "HZ=";
157 char path[PATH_MAX] = "PATH=";
158 char supath[PATH_MAX] = "PATH=";
159 char *envinit[ELIM];
160 extern char **environ;
161 char *ttyn;
162 char *username; /* the invoker */
163 static int dosyslog = 0; /* use syslog? */
164 char *myname;
165 #ifdef DYNAMIC_SU
166 int pam_flags = 0;
167 boolean_t embedded = B_FALSE;
168 #endif /* DYNAMIC_SU */
171 main(int argc, char **argv)
173 #ifndef DYNAMIC_SU
174 struct spwd sp;
175 char spbuf[1024]; /* buffer for getspnam_r() */
176 char *password;
177 #endif /* !DYNAMIC_SU */
178 char *nptr;
179 char *pshell;
180 int eflag = 0;
181 int envidx = 0;
182 uid_t uid;
183 gid_t gid;
184 char *dir, *shprog, *name;
185 char *ptr;
186 char *prog = argv[0];
187 #ifdef DYNAMIC_SU
188 int sleeptime = SLEEPTIME;
189 char **pam_env = 0;
190 int flags = 0;
191 int retcode;
192 int idx = 0;
193 #endif /* DYNAMIC_SU */
194 int pw_change = PW_FALSE;
195 struct passwd *result;
197 (void) setlocale(LC_ALL, "");
198 #if !defined(TEXT_DOMAIN) /* Should be defined by cc -D */
199 #define TEXT_DOMAIN "SYS_TEST" /* Use this only if it wasn't */
200 #endif
201 (void) textdomain(TEXT_DOMAIN);
203 myname = tail(argv[0]);
205 #ifdef DYNAMIC_SU
206 if (strcmp(myname, EMBEDDED_NAME) == 0) {
207 embedded = B_TRUE;
208 setbuf(stdin, NULL);
209 setbuf(stdout, NULL);
210 readinitblock();
212 #endif /* DYNAMIC_SU */
214 if (argc > 1 && *argv[1] == '-') {
215 /* Explicitly check for just `-' (no trailing chars) */
216 if (strlen(argv[1]) == 1) {
217 eflag++; /* set eflag if `-' is specified */
218 argv++;
219 argc--;
220 } else {
221 message(USAGE,
222 gettext("Usage: %s [-] [ username [ arg ... ] ]"),
223 prog);
224 exit(1);
229 * Determine specified userid, get their password file entry,
230 * and set variables to values in password file entry fields.
232 if (argc > 1) {
234 * Usernames can't start with a `-', so we check for that to
235 * catch bad usage (like "su - -c ls").
237 if (*argv[1] == '-') {
238 message(USAGE,
239 gettext("Usage: %s [-] [ username [ arg ... ] ]"),
240 prog);
241 exit(1);
242 } else
243 nptr = argv[1]; /* use valid command-line username */
244 } else
245 nptr = "root"; /* use default "root" username */
247 if (defopen(DEFFILE) == 0) {
249 if (Sulog = defread("SULOG="))
250 Sulog = strdup(Sulog);
251 if (Console = defread("CONSOLE="))
252 Console = strdup(Console);
253 if (Path = defread("PATH="))
254 Path = strdup(Path);
255 if (Supath = defread("SUPATH="))
256 Supath = strdup(Supath);
257 if ((ptr = defread("SYSLOG=")) != NULL)
258 dosyslog = strcmp(ptr, "YES") == 0;
260 (void) defopen(NULL);
262 (void) strlcat(path, (Path) ? Path : PATH, sizeof (path));
263 (void) strlcat(supath, (Supath) ? Supath : SUPATH, sizeof (supath));
265 if ((ttyn = ttyname(0)) == NULL)
266 if ((ttyn = ttyname(1)) == NULL)
267 if ((ttyn = ttyname(2)) == NULL)
268 ttyn = "/dev/???";
269 if ((username = cuserid(NULL)) == NULL)
270 username = "(null)";
273 * if Sulog defined, create SULOG, if it does not exist, with
274 * mode read/write user. Change owner and group to root
276 if (Sulog != NULL) {
277 (void) close(open(Sulog, O_WRONLY | O_APPEND | O_CREAT,
278 (S_IRUSR|S_IWUSR)));
279 (void) chown(Sulog, (uid_t)ROOT, (gid_t)ROOT);
282 #ifdef DYNAMIC_SU
283 if (pam_start(embedded ? EMBEDDED_NAME : "su", nptr,
284 embedded ? &emb_pam_conv : &pam_conv, &pamh) != PAM_SUCCESS)
285 exit(1);
286 if (pam_set_item(pamh, PAM_TTY, ttyn) != PAM_SUCCESS)
287 exit(1);
288 getpwuid_r(getuid(), &pwd, pwdbuf, sizeof (pwdbuf), &result);
289 if (!result ||
290 pam_set_item(pamh, PAM_AUSER, pwd.pw_name) != PAM_SUCCESS)
291 exit(1);
292 #endif /* DYNAMIC_SU */
294 openlog("su", LOG_CONS, LOG_AUTH);
296 #ifdef DYNAMIC_SU
299 * Use the same value of sleeptime and password required that
300 * login(1) uses.
301 * This is obtained by reading the file /etc/default/login
302 * using the def*() functions
304 if (defopen(DEFAULT_LOGIN) == 0) {
305 if ((ptr = defread("SLEEPTIME=")) != NULL) {
306 sleeptime = atoi(ptr);
307 if (sleeptime < 0 || sleeptime > 5)
308 sleeptime = SLEEPTIME;
311 if ((ptr = defread("PASSREQ=")) != NULL &&
312 strcasecmp("YES", ptr) == 0)
313 pam_flags |= PAM_DISALLOW_NULL_AUTHTOK;
315 (void) defopen(NULL);
318 * Ignore SIGQUIT and SIGINT
320 (void) signal(SIGQUIT, SIG_IGN);
321 (void) signal(SIGINT, SIG_IGN);
323 /* call pam authenticate() to authenticate the user through PAM */
324 getpwnam_r(nptr, &pwd, pwdbuf, sizeof (pwdbuf), &result);
325 if (!result)
326 retcode = PAM_USER_UNKNOWN;
327 else if ((flags = (getuid() != (uid_t)ROOT)) != 0) {
328 retcode = pam_authenticate(pamh, pam_flags);
329 } else /* root user does not need to authenticate */
330 retcode = PAM_SUCCESS;
332 if (retcode != PAM_SUCCESS) {
334 * 1st step: audit and log the error.
335 * 2nd step: sleep.
336 * 3rd step: print out message to user.
338 /* don't let audit_failure distinguish a role here */
339 audit_failure(PW_FALSE, NULL, nptr, retcode);
340 switch (retcode) {
341 case PAM_USER_UNKNOWN:
342 closelog();
343 (void) sleep(sleeptime);
344 message(ERR, gettext("Unknown id: %s"), nptr);
345 break;
347 case PAM_AUTH_ERR:
348 if (Sulog != NULL)
349 log(Sulog, nptr, 0); /* log entry */
350 if (dosyslog)
351 syslog(LOG_CRIT, "'su %s' failed for %s on %s",
352 pwd.pw_name, username, ttyn);
353 closelog();
354 (void) sleep(sleeptime);
355 message(ERR, gettext("Sorry"));
356 break;
358 case PAM_CONV_ERR:
359 default:
360 if (dosyslog)
361 syslog(LOG_CRIT, "'su %s' failed for %s on %s",
362 pwd.pw_name, username, ttyn);
363 closelog();
364 (void) sleep(sleeptime);
365 message(ERR, gettext("Sorry"));
366 break;
369 (void) signal(SIGQUIT, SIG_DFL);
370 (void) signal(SIGINT, SIG_DFL);
371 exit(1);
373 if (flags)
374 validate(username, &pw_change);
375 if (pam_setcred(pamh, PAM_REINITIALIZE_CRED) != PAM_SUCCESS) {
376 message(ERR, gettext("unable to set credentials"));
377 exit(2);
379 if (dosyslog)
380 syslog(pwd.pw_uid == 0 ? LOG_NOTICE : LOG_INFO,
381 "'su %s' succeeded for %s on %s",
382 pwd.pw_name, username, ttyn);
383 closelog();
384 (void) signal(SIGQUIT, SIG_DFL);
385 (void) signal(SIGINT, SIG_DFL);
386 #else /* !DYNAMIC_SU */
387 getpwnam_r(nptr, &pwd, pwdbuf, sizeof (pwdbuf), &result);
388 if (!result ||
389 (getspnam_r(nptr, &sp, spbuf, sizeof (spbuf)) == NULL)) {
390 message(ERR, gettext("Unknown id: %s"), nptr);
391 audit_failure(PW_FALSE, NULL, nptr, PAM_USER_UNKNOWN);
392 closelog();
393 exit(1);
397 * Prompt for password if invoking user is not root or
398 * if specified(new) user requires a password
400 if (sp.sp_pwdp[0] == '\0' || getuid() == (uid_t)ROOT)
401 goto ok;
402 password = getpass(gettext("Password:"));
404 if ((strcmp(sp.sp_pwdp, crypt(password, sp.sp_pwdp)) != 0)) {
405 /* clear password file entry */
406 (void) memset(spbuf, 0, sizeof (spbuf));
407 if (Sulog != NULL)
408 log(Sulog, nptr, 0); /* log entry */
409 message(ERR, gettext("Sorry"));
410 audit_failure(PW_FALSE, NULL, nptr, PAM_AUTH_ERR);
411 if (dosyslog)
412 syslog(LOG_CRIT, "'su %s' failed for %s on %s",
413 pwd.pw_name, username, ttyn);
414 closelog();
415 exit(2);
417 /* clear password file entry */
418 (void) memset(spbuf, 0, sizeof (spbuf));
420 /* update audit session in a non-pam environment */
421 update_audit(&pwd);
422 if (dosyslog)
423 syslog(pwd.pw_uid == 0 ? LOG_NOTICE : LOG_INFO,
424 "'su %s' succeeded for %s on %s",
425 pwd.pw_name, username, ttyn);
426 #endif /* DYNAMIC_SU */
428 audit_success(pw_change, &pwd);
429 uid = pwd.pw_uid;
430 gid = pwd.pw_gid;
431 dir = strdup(pwd.pw_dir);
432 shprog = strdup(pwd.pw_shell);
433 name = strdup(pwd.pw_name);
435 if (Sulog != NULL)
436 log(Sulog, nptr, 1); /* log entry */
438 /* set user and group ids to specified user */
440 /* set the real (and effective) GID */
441 if (setgid(gid) == -1) {
442 message(ERR, gettext("Invalid GID"));
443 exit(2);
445 /* Initialize the supplementary group access list. */
446 if (!nptr)
447 exit(2);
448 if (initgroups(nptr, gid) == -1) {
449 exit(2);
451 /* set the real (and effective) UID */
452 if (setuid(uid) == -1) {
453 message(ERR, gettext("Invalid UID"));
454 exit(2);
458 * If new user's shell field is neither NULL nor equal to /usr/bin/sh,
459 * set:
461 * pshell = their shell
462 * su = [-]last component of shell's pathname
464 * Otherwise, set the shell to /usr/bin/sh and set argv[0] to '[-]su'.
466 if (shprog[0] != '\0' && strcmp(shell, shprog) != 0) {
467 char *p;
469 pshell = shprog;
470 (void) strcpy(su, eflag ? "-" : "");
472 if ((p = strrchr(pshell, '/')) != NULL)
473 (void) strlcat(su, p + 1, sizeof (su));
474 else
475 (void) strlcat(su, pshell, sizeof (su));
476 } else {
477 pshell = shell;
478 (void) strcpy(su, eflag ? "-su" : "su");
482 * set environment variables for new user;
483 * arg0 for exec of shprog must now contain `-'
484 * so that environment of new user is given
486 if (eflag) {
487 int j;
488 char *var;
490 if (strlen(dir) == 0) {
491 (void) strcpy(dir, "/");
492 message(WARN, gettext("No directory! Using home=/"));
494 (void) strlcat(homedir, dir, sizeof (homedir));
495 (void) strlcat(logname, name, sizeof (logname));
496 if (hz = getenv("HZ"))
497 (void) strlcat(hzname, hz, sizeof (hzname));
499 (void) strlcat(shelltyp, pshell, sizeof (shelltyp));
501 if (chdir(dir) < 0) {
502 message(ERR, gettext("No directory!"));
503 exit(1);
505 envinit[envidx = 0] = homedir;
506 envinit[++envidx] = ((uid == (uid_t)ROOT) ? supath : path);
507 envinit[++envidx] = logname;
508 envinit[++envidx] = hzname;
509 if ((term = getenv("TERM")) != NULL) {
510 (void) strlcat(termtyp, term, sizeof (termtyp));
511 envinit[++envidx] = termtyp;
513 envinit[++envidx] = shelltyp;
515 (void) strlcat(mail, name, sizeof (mail));
516 envinit[++envidx] = mail;
519 * Fetch the relevant locale/TZ environment variables from
520 * the inherited environment.
522 * We have a priority here for setting TZ. If TZ is set in
523 * in the inherited environment, that value remains top
524 * priority. If the file /etc/default/login has TIMEZONE set,
525 * that has second highest priority.
527 tznam[0] = '\0';
528 for (j = 0; initenv[j] != 0; j++) {
529 if (initvar = getenv(initenv[j])) {
532 * Skip over values beginning with '/' for
533 * security.
535 if (initvar[0] == '/') continue;
537 if (strcmp(initenv[j], "TZ") == 0) {
538 (void) strcpy(tznam, "TZ=");
539 (void) strlcat(tznam, initvar,
540 sizeof (tznam));
542 } else {
543 var = (char *)
544 malloc(strlen(initenv[j])
545 + strlen(initvar)
546 + 2);
547 if (var == NULL) {
548 perror("malloc");
549 exit(4);
551 (void) strcpy(var, initenv[j]);
552 (void) strcat(var, "=");
553 (void) strcat(var, initvar);
554 envinit[++envidx] = var;
560 * Check if TZ was found. If not then try to read it from
561 * /etc/default/login.
563 if (tznam[0] == '\0') {
564 if (defopen(DEFAULT_LOGIN) == 0) {
565 if (initvar = defread("TIMEZONE=")) {
566 (void) strcpy(tznam, "TZ=");
567 (void) strlcat(tznam, initvar,
568 sizeof (tznam));
570 (void) defopen(NULL);
574 if (tznam[0] != '\0')
575 envinit[++envidx] = tznam;
577 #ifdef DYNAMIC_SU
579 * set the PAM environment variables -
580 * check for legal environment variables
582 if ((pam_env = pam_getenvlist(pamh)) != 0) {
583 while (pam_env[idx] != 0) {
584 if (envidx + 2 < ELIM &&
585 legalenvvar(pam_env[idx])) {
586 envinit[++envidx] = pam_env[idx];
588 idx++;
591 #endif /* DYNAMIC_SU */
592 envinit[++envidx] = NULL;
593 environ = envinit;
594 } else {
595 char **pp = environ, **qq, *p;
597 while ((p = *pp) != NULL) {
598 if (*p == 'L' && p[1] == 'D' && p[2] == '_') {
599 for (qq = pp; (*qq = qq[1]) != NULL; qq++)
601 /* pp is not advanced */
602 } else {
603 pp++;
608 #ifdef DYNAMIC_SU
609 if (pamh)
610 (void) pam_end(pamh, PAM_SUCCESS);
611 #endif /* DYNAMIC_SU */
614 * if new user is root:
615 * if CONSOLE defined, log entry there;
616 * if eflag not set, change environment to that of root.
618 if (uid == (uid_t)ROOT) {
619 if (Console != NULL)
620 if (strcmp(ttyn, Console) != 0) {
621 (void) signal(SIGALRM, to);
622 (void) alarm(30);
623 log(Console, nptr, 1);
624 (void) alarm(0);
626 if (!eflag)
627 envalt();
631 * Default for SIGCPU and SIGXFSZ. Shells inherit
632 * signal disposition from parent. And the
633 * shells should have default dispositions for these
634 * signals.
636 (void) signal(SIGXCPU, SIG_DFL);
637 (void) signal(SIGXFSZ, SIG_DFL);
639 #ifdef DYNAMIC_SU
640 if (embedded) {
641 (void) puts("SUCCESS");
643 * After this point, we're no longer talking the
644 * embedded_su protocol, so turn it off.
646 embedded = B_FALSE;
648 #endif /* DYNAMIC_SU */
651 * if additional arguments, exec shell program with array
652 * of pointers to arguments:
653 * -> if shell = default, then su = [-]su
654 * -> if shell != default, then su = [-]last component of
655 * shell's pathname
657 * if no additional arguments, exec shell with arg0 of su
658 * where:
659 * -> if shell = default, then su = [-]su
660 * -> if shell != default, then su = [-]last component of
661 * shell's pathname
663 if (argc > 2) {
664 argv[1] = su;
665 (void) execv(pshell, &argv[1]);
666 } else
667 (void) execl(pshell, su, 0);
671 * Try to clean up after an administrator who has made a mistake
672 * configuring root's shell; if root's shell is other than /sbin/sh,
673 * try exec'ing /sbin/sh instead.
675 if ((uid == (uid_t)ROOT) && (strcmp(name, "root") == 0) &&
676 (strcmp(safe_shell, pshell) != 0)) {
677 message(WARN,
678 gettext("No shell %s. Trying fallback shell %s."),
679 pshell, safe_shell);
681 if (eflag) {
682 (void) strcpy(su, "-sh");
683 (void) strlcpy(shelltyp + strlen("SHELL="),
684 safe_shell, sizeof (shelltyp) - strlen("SHELL="));
685 } else {
686 (void) strcpy(su, "sh");
689 if (argc > 2) {
690 argv[1] = su;
691 (void) execv(safe_shell, &argv[1]);
692 } else {
693 (void) execl(safe_shell, su, 0);
695 message(ERR, gettext("Couldn't exec fallback shell %s: %s"),
696 safe_shell, strerror(errno));
697 } else {
698 message(ERR, gettext("No shell"));
700 return (3);
704 * Environment altering routine -
705 * This routine is called when a user is su'ing to root
706 * without specifying the - flag.
707 * The user's PATH and PS1 variables are reset
708 * to the correct value for root.
709 * All of the user's other environment variables retain
710 * their current values after the su (if they are exported).
712 static void
713 envalt(void)
716 * If user has PATH variable in their environment, change its value
717 * to /bin:/etc:/usr/bin ;
718 * if user does not have PATH variable, add it to the user's
719 * environment;
720 * if either of the above fail, an error message is printed.
722 if (putenv(supath) != 0) {
723 message(ERR,
724 gettext("unable to obtain memory to expand environment"));
725 exit(4);
729 * If user has PROMPT variable in their environment, change its value
730 * to # ;
731 * if user does not have PROMPT variable, add it to the user's
732 * environment;
733 * if either of the above fail, an error message is printed.
735 if (putenv(suprmt) != 0) {
736 message(ERR,
737 gettext("unable to obtain memory to expand environment"));
738 exit(4);
743 * Logging routine -
744 * where = SULOG or CONSOLE
745 * towho = specified user ( user being su'ed to )
746 * how = 0 if su attempt failed; 1 if su attempt succeeded
748 static void
749 log(char *where, char *towho, int how)
751 FILE *logf;
752 time_t now;
753 struct tm *tmp;
756 * open SULOG or CONSOLE - if open fails, return
758 if ((logf = fopen(where, "a")) == NULL)
759 return;
761 now = time(0);
762 tmp = localtime(&now);
765 * write entry into SULOG or onto CONSOLE - if write fails, return
767 (void) fprintf(logf, "SU %.2d/%.2d %.2d:%.2d %c %s %s-%s\n",
768 tmp->tm_mon + 1, tmp->tm_mday, tmp->tm_hour, tmp->tm_min,
769 how ? '+' : '-', ttyn + sizeof ("/dev/") - 1, username, towho);
771 (void) fclose(logf); /* close SULOG or CONSOLE */
774 /*ARGSUSED*/
775 static void
776 to(int sig)
780 * audit_success - audit successful su
782 * Entry process audit context established -- i.e., pam_setcred()
783 * or equivalent called.
784 * pw_change = PW_TRUE, if successful password change audit
785 * required.
786 * pwd = passwd entry for new user.
789 static void
790 audit_success(int pw_change, struct passwd *pwd)
792 adt_session_data_t *ah = NULL;
793 adt_event_data_t *event;
794 au_event_t event_id = ADT_su;
795 userattr_t *user_entry;
796 char *kva_value;
798 if (adt_start_session(&ah, NULL, ADT_USE_PROC_DATA) != 0) {
799 syslog(LOG_AUTH | LOG_ALERT,
800 "adt_start_session(ADT_su): %m");
801 return;
803 if (((user_entry = getusernam(pwd->pw_name)) != NULL) &&
804 ((kva_value = kva_match((kva_t *)user_entry->attr,
805 USERATTR_TYPE_KW)) != NULL) &&
806 ((strcmp(kva_value, USERATTR_TYPE_NONADMIN_KW) == 0) ||
807 (strcmp(kva_value, USERATTR_TYPE_ADMIN_KW) == 0))) {
808 event_id = ADT_role_login;
810 free_userattr(user_entry); /* OK to use, checks for NULL */
812 /* since proc uid/gid not yet updated */
813 if (adt_set_user(ah, pwd->pw_uid, pwd->pw_gid, pwd->pw_uid,
814 pwd->pw_gid, NULL, ADT_USER) != 0) {
815 syslog(LOG_AUTH | LOG_ERR,
816 "adt_set_user(ADT_su, ADT_FAILURE): %m");
818 if ((event = adt_alloc_event(ah, event_id)) == NULL) {
819 syslog(LOG_AUTH | LOG_ALERT, "adt_alloc_event(ADT_su): %m");
820 } else if (adt_put_event(event, ADT_SUCCESS, ADT_SUCCESS) != 0) {
821 syslog(LOG_AUTH | LOG_ALERT,
822 "adt_put_event(ADT_su, ADT_SUCCESS): %m");
825 if (pw_change == PW_TRUE) {
826 /* Also audit password change */
827 adt_free_event(event);
828 if ((event = adt_alloc_event(ah, ADT_passwd)) == NULL) {
829 syslog(LOG_AUTH | LOG_ALERT,
830 "adt_alloc_event(ADT_passwd): %m");
831 } else if (adt_put_event(event, ADT_SUCCESS,
832 ADT_SUCCESS) != 0) {
833 syslog(LOG_AUTH | LOG_ALERT,
834 "adt_put_event(ADT_passwd, ADT_SUCCESS): %m");
837 adt_free_event(event);
839 * The preceeding code is a noop if audit isn't enabled,
840 * but, let's not make a new process when it's not necessary.
842 if (adt_audit_state(AUC_AUDITING)) {
843 audit_logout(ah, event_id); /* fork to catch logout */
845 (void) adt_end_session(ah);
850 * audit_logout - audit successful su logout
852 * Entry ah = Successful su audit handle
853 * event_id = su event ID: ADT_su, ADT_role_login
855 * Exit Errors are just ignored and we go on.
856 * su logout event written.
858 static void
859 audit_logout(adt_session_data_t *ah, au_event_t event_id)
861 adt_event_data_t *event;
862 int status; /* wait status */
863 pid_t pid;
864 priv_set_t *priv; /* waiting process privs */
866 if (event_id == ADT_su) {
867 event_id = ADT_su_logout;
868 } else {
869 event_id = ADT_role_logout;
871 if ((event = adt_alloc_event(ah, event_id)) == NULL) {
872 syslog(LOG_AUTH | LOG_ALERT,
873 "adt_alloc_event(ADT_su_logout): %m");
874 return;
876 if ((priv = priv_allocset()) == NULL) {
877 syslog(LOG_AUTH | LOG_ALERT,
878 "su audit_logout: could not alloc basic privs: %m");
879 adt_free_event(event);
880 return;
884 * The child returns and continues su processing.
885 * The parent's sole job is to wait for child exit, write the
886 * logout audit record, and replay the child's exit code.
888 if ((pid = fork()) == 0) {
889 /* child */
891 adt_free_event(event);
892 priv_freeset(priv);
893 return;
895 if (pid == -1) {
896 /* failure */
898 syslog(LOG_AUTH | LOG_ALERT,
899 "su audit_logout: could not fork: %m");
900 adt_free_event(event);
901 priv_freeset(priv);
902 return;
905 /* parent process */
908 * When this routine is called, the current working
909 * directory is the unknown and there are unknown open
910 * files. For the waiting process, change the current
911 * directory to root and close open files so that
912 * directories can be unmounted if necessary.
914 if (chdir("/") != 0) {
915 syslog(LOG_AUTH | LOG_ALERT,
916 "su audit_logout: could not chdir /: %m");
919 * Reduce privileges to just those needed.
921 priv_basicset(priv);
922 (void) priv_delset(priv, PRIV_PROC_EXEC);
923 (void) priv_delset(priv, PRIV_PROC_FORK);
924 (void) priv_delset(priv, PRIV_PROC_INFO);
925 (void) priv_delset(priv, PRIV_PROC_SESSION);
926 (void) priv_delset(priv, PRIV_FILE_LINK_ANY);
927 if ((priv_addset(priv, PRIV_PROC_AUDIT) != 0) ||
928 (setppriv(PRIV_SET, PRIV_PERMITTED, priv) != 0)) {
929 syslog(LOG_AUTH | LOG_ALERT,
930 "su audit_logout: could not reduce privs: %m");
932 closefrom(0);
933 priv_freeset(priv);
935 for (;;) {
936 if (pid != waitpid(pid, &status, WUNTRACED)) {
937 if (errno == ECHILD) {
939 * No existing child with the given pid. Lets
940 * audit the logout.
942 break;
944 continue;
947 if (WIFEXITED(status) || WIFSIGNALED(status)) {
949 * The child shell exited or was terminated by
950 * a signal. Lets audit logout.
952 break;
953 } else if (WIFSTOPPED(status)) {
954 pid_t pgid;
955 int fd;
956 void (*sg_handler)();
958 * The child shell has been stopped/suspended.
959 * We need to suspend here as well and pass down
960 * the control to the parent process.
962 sg_handler = signal(WSTOPSIG(status), SIG_DFL);
963 (void) sigsend(P_PGID, getpgrp(), WSTOPSIG(status));
965 * We stop here. When resumed, mark the child
966 * shell group as foreground process group
967 * which gives the child shell a control over
968 * the controlling terminal.
970 (void) signal(WSTOPSIG(status), sg_handler);
972 pgid = getpgid(pid);
973 if ((fd = open("/dev/tty", O_RDWR)) != -1) {
975 * Pass down the control over the controlling
976 * terminal iff we are in a foreground process
977 * group. Otherwise, we are in a background
978 * process group and the kernel will send
979 * SIGTTOU signal to stop us (by default).
981 if (tcgetpgrp(fd) == getpgrp()) {
982 (void) tcsetpgrp(fd, pgid);
984 (void) close(fd);
986 /* Wake up the child shell */
987 (void) sigsend(P_PGID, pgid, SIGCONT);
991 (void) adt_put_event(event, ADT_SUCCESS, ADT_SUCCESS);
992 adt_free_event(event);
993 (void) adt_end_session(ah);
994 exit(WEXITSTATUS(status));
999 * audit_failure - audit failed su
1001 * Entry New audit context not set.
1002 * pw_change == PW_FALSE, if no password change requested.
1003 * PW_FAILED, if failed password change audit
1004 * required.
1005 * pwd = NULL, or password entry to use.
1006 * user = username entered. Add to record if pwd == NULL.
1007 * pamerr = PAM error code; reason for failure.
1010 static void
1011 audit_failure(int pw_change, struct passwd *pwd, char *user, int pamerr)
1013 adt_session_data_t *ah; /* audit session handle */
1014 adt_event_data_t *event; /* event to generate */
1015 au_event_t event_id = ADT_su;
1016 userattr_t *user_entry;
1017 char *kva_value;
1019 if (adt_start_session(&ah, NULL, ADT_USE_PROC_DATA) != 0) {
1020 syslog(LOG_AUTH | LOG_ALERT,
1021 "adt_start_session(ADT_su, ADT_FAILURE): %m");
1022 return;
1025 if (pwd != NULL) {
1026 /* target user authenticated, merge audit state */
1027 if (adt_set_user(ah, pwd->pw_uid, pwd->pw_gid, pwd->pw_uid,
1028 pwd->pw_gid, NULL, ADT_UPDATE) != 0) {
1029 syslog(LOG_AUTH | LOG_ERR,
1030 "adt_set_user(ADT_su, ADT_FAILURE): %m");
1032 if (((user_entry = getusernam(pwd->pw_name)) != NULL) &&
1033 ((kva_value = kva_match((kva_t *)user_entry->attr,
1034 USERATTR_TYPE_KW)) != NULL) &&
1035 ((strcmp(kva_value, USERATTR_TYPE_NONADMIN_KW) == 0) ||
1036 (strcmp(kva_value, USERATTR_TYPE_ADMIN_KW) == 0))) {
1037 event_id = ADT_role_login;
1039 free_userattr(user_entry); /* OK to use, checks for NULL */
1041 if ((event = adt_alloc_event(ah, event_id)) == NULL) {
1042 syslog(LOG_AUTH | LOG_ALERT,
1043 "adt_alloc_event(ADT_su, ADT_FAILURE): %m");
1044 return;
1047 * can't tell if user not found is a role, so always use su
1048 * If we do pass in pwd when the JNI is fixed, then can
1049 * distinguish and set name in both su and role_login
1051 if (pwd == NULL) {
1053 * this should be "fail_user" rather than "message"
1054 * see adt_xml. The JNI breaks, so for now we leave
1055 * this alone.
1057 event->adt_su.message = user;
1059 if (adt_put_event(event, ADT_FAILURE,
1060 ADT_FAIL_PAM + pamerr) != 0) {
1061 syslog(LOG_AUTH | LOG_ALERT,
1062 "adt_put_event(ADT_su(ADT_FAIL, %s): %m",
1063 pam_strerror(pamh, pamerr));
1065 if (pw_change != PW_FALSE) {
1066 /* Also audit password change failed */
1067 adt_free_event(event);
1068 if ((event = adt_alloc_event(ah, ADT_passwd)) == NULL) {
1069 syslog(LOG_AUTH | LOG_ALERT,
1070 "su: adt_alloc_event(ADT_passwd): %m");
1071 } else if (adt_put_event(event, ADT_FAILURE,
1072 ADT_FAIL_PAM + pamerr) != 0) {
1073 syslog(LOG_AUTH | LOG_ALERT,
1074 "su: adt_put_event(ADT_passwd, ADT_FAILURE): %m");
1077 adt_free_event(event);
1078 (void) adt_end_session(ah);
1081 #ifdef DYNAMIC_SU
1083 * su_conv():
1084 * This is the conv (conversation) function called from
1085 * a PAM authentication module to print error messages
1086 * or garner information from the user.
1088 /*ARGSUSED*/
1089 static int
1090 su_conv(int num_msg, struct pam_message **msg, struct pam_response **response,
1091 void *appdata_ptr)
1093 struct pam_message *m;
1094 struct pam_response *r;
1095 char *temp;
1096 int k;
1097 char respbuf[PAM_MAX_RESP_SIZE];
1099 if (num_msg <= 0)
1100 return (PAM_CONV_ERR);
1102 *response = (struct pam_response *)calloc(num_msg,
1103 sizeof (struct pam_response));
1104 if (*response == NULL)
1105 return (PAM_BUF_ERR);
1107 k = num_msg;
1108 m = *msg;
1109 r = *response;
1110 while (k--) {
1112 switch (m->msg_style) {
1114 case PAM_PROMPT_ECHO_OFF:
1115 errno = 0;
1116 temp = getpassphrase(m->msg);
1117 if (errno == EINTR)
1118 return (PAM_CONV_ERR);
1119 if (temp != NULL) {
1120 r->resp = strdup(temp);
1121 if (r->resp == NULL) {
1122 freeresponse(num_msg, response);
1123 return (PAM_BUF_ERR);
1126 break;
1128 case PAM_PROMPT_ECHO_ON:
1129 if (m->msg != NULL) {
1130 (void) fputs(m->msg, stdout);
1133 (void) fgets(respbuf, sizeof (respbuf), stdin);
1134 temp = strchr(respbuf, '\n');
1135 if (temp != NULL)
1136 *temp = '\0';
1138 r->resp = strdup(respbuf);
1139 if (r->resp == NULL) {
1140 freeresponse(num_msg, response);
1141 return (PAM_BUF_ERR);
1143 break;
1145 case PAM_ERROR_MSG:
1146 if (m->msg != NULL) {
1147 (void) fputs(m->msg, stderr);
1148 (void) fputs("\n", stderr);
1150 break;
1152 case PAM_TEXT_INFO:
1153 if (m->msg != NULL) {
1154 (void) fputs(m->msg, stdout);
1155 (void) fputs("\n", stdout);
1157 break;
1159 default:
1160 break;
1162 m++;
1163 r++;
1165 return (PAM_SUCCESS);
1169 * emb_su_conv():
1170 * This is the conv (conversation) function called from
1171 * a PAM authentication module to print error messages
1172 * or garner information from the user.
1173 * This version is used for embedded_su.
1175 /*ARGSUSED*/
1176 static int
1177 emb_su_conv(int num_msg, struct pam_message **msg,
1178 struct pam_response **response, void *appdata_ptr)
1180 struct pam_message *m;
1181 struct pam_response *r;
1182 char *temp;
1183 int k;
1184 char respbuf[PAM_MAX_RESP_SIZE];
1186 if (num_msg <= 0)
1187 return (PAM_CONV_ERR);
1189 *response = (struct pam_response *)calloc(num_msg,
1190 sizeof (struct pam_response));
1191 if (*response == NULL)
1192 return (PAM_BUF_ERR);
1194 /* First, send the prompts */
1195 (void) printf("CONV %d\n", num_msg);
1196 k = num_msg;
1197 m = *msg;
1198 while (k--) {
1199 switch (m->msg_style) {
1201 case PAM_PROMPT_ECHO_OFF:
1202 (void) puts("PAM_PROMPT_ECHO_OFF");
1203 goto msg_common;
1205 case PAM_PROMPT_ECHO_ON:
1206 (void) puts("PAM_PROMPT_ECHO_ON");
1207 goto msg_common;
1209 case PAM_ERROR_MSG:
1210 (void) puts("PAM_ERROR_MSG");
1211 goto msg_common;
1213 case PAM_TEXT_INFO:
1214 (void) puts("PAM_TEXT_INFO");
1215 /* fall through to msg_common */
1216 msg_common:
1217 if (m->msg == NULL)
1218 quotemsg(NULL);
1219 else
1220 quotemsg("%s", m->msg);
1221 break;
1223 default:
1224 break;
1226 m++;
1229 /* Next, collect the responses */
1230 k = num_msg;
1231 m = *msg;
1232 r = *response;
1233 while (k--) {
1235 switch (m->msg_style) {
1237 case PAM_PROMPT_ECHO_OFF:
1238 case PAM_PROMPT_ECHO_ON:
1239 (void) fgets(respbuf, sizeof (respbuf), stdin);
1241 temp = strchr(respbuf, '\n');
1242 if (temp != NULL)
1243 *temp = '\0';
1245 r->resp = strdup(respbuf);
1246 if (r->resp == NULL) {
1247 freeresponse(num_msg, response);
1248 return (PAM_BUF_ERR);
1251 break;
1253 case PAM_ERROR_MSG:
1254 case PAM_TEXT_INFO:
1255 break;
1257 default:
1258 break;
1260 m++;
1261 r++;
1263 return (PAM_SUCCESS);
1266 static void
1267 freeresponse(int num_msg, struct pam_response **response)
1269 struct pam_response *r;
1270 int i;
1272 /* free responses */
1273 r = *response;
1274 for (i = 0; i < num_msg; i++, r++) {
1275 if (r->resp != NULL) {
1276 /* Zap it in case it's a password */
1277 (void) memset(r->resp, '\0', strlen(r->resp));
1278 free(r->resp);
1281 free(*response);
1282 *response = NULL;
1286 * Print a message, applying quoting for lines starting with '.'.
1288 * I18n note: \n is "safe" in all locales, and all locales use
1289 * a high-bit-set character to start multibyte sequences, so
1290 * scanning for a \n followed by a '.' is safe.
1292 static void
1293 quotemsg(char *fmt, ...)
1295 if (fmt != NULL) {
1296 char *msg;
1297 char *p;
1298 boolean_t bol;
1299 va_list v;
1301 va_start(v, fmt);
1302 msg = alloc_vsprintf(fmt, v);
1303 va_end(v);
1305 bol = B_TRUE;
1306 for (p = msg; *p != '\0'; p++) {
1307 if (bol) {
1308 if (*p == '.')
1309 (void) putchar('.');
1310 bol = B_FALSE;
1312 (void) putchar(*p);
1313 if (*p == '\n')
1314 bol = B_TRUE;
1316 (void) putchar('\n');
1317 free(msg);
1319 (void) putchar('.');
1320 (void) putchar('\n');
1324 * validate - Check that the account is valid for switching to.
1326 static void
1327 validate(char *usernam, int *pw_change)
1329 int error;
1330 int tries;
1332 if ((error = pam_acct_mgmt(pamh, pam_flags)) != PAM_SUCCESS) {
1333 if (Sulog != NULL)
1334 log(Sulog, pwd.pw_name, 0); /* log entry */
1335 if (error == PAM_NEW_AUTHTOK_REQD) {
1336 tries = 0;
1337 message(ERR, gettext("Password for user "
1338 "'%s' has expired"), pwd.pw_name);
1339 while ((error = pam_chauthtok(pamh,
1340 PAM_CHANGE_EXPIRED_AUTHTOK)) != PAM_SUCCESS) {
1341 if ((error == PAM_AUTHTOK_ERR ||
1342 error == PAM_TRY_AGAIN) &&
1343 (tries++ < DEF_ATTEMPTS)) {
1344 continue;
1346 message(ERR, gettext("Sorry"));
1347 audit_failure(PW_FAILED, &pwd, NULL, error);
1348 if (dosyslog)
1349 syslog(LOG_CRIT,
1350 "'su %s' failed for %s on %s",
1351 pwd.pw_name, usernam, ttyn);
1352 closelog();
1353 exit(1);
1355 *pw_change = PW_TRUE;
1356 return;
1357 } else {
1358 message(ERR, gettext("Sorry"));
1359 audit_failure(PW_FALSE, &pwd, NULL, error);
1360 if (dosyslog)
1361 syslog(LOG_CRIT, "'su %s' failed for %s on %s",
1362 pwd.pw_name, usernam, ttyn);
1363 closelog();
1364 exit(3);
1369 static char *illegal[] = {
1370 "SHELL=",
1371 "HOME=",
1372 "LOGNAME=",
1373 #ifndef NO_MAIL
1374 "MAIL=",
1375 #endif
1376 "CDPATH=",
1377 "IFS=",
1378 "PATH=",
1379 "TZ=",
1380 "HZ=",
1381 "TERM=",
1386 * legalenvvar - can PAM modules insert this environmental variable?
1389 static int
1390 legalenvvar(char *s)
1392 register char **p;
1394 for (p = illegal; *p; p++)
1395 if (strncmp(s, *p, strlen(*p)) == 0)
1396 return (0);
1398 if (s[0] == 'L' && s[1] == 'D' && s[2] == '_')
1399 return (0);
1401 return (1);
1405 * The embedded_su protocol allows the client application to supply
1406 * an initialization block terminated by a line with just a "." on it.
1408 * This initialization block is currently unused, reserved for future
1409 * expansion. Ignore it. This is made very slightly more complex by
1410 * the desire to cleanly ignore input lines of any length, while still
1411 * correctly detecting a line with just a "." on it.
1413 * I18n note: It appears that none of the Solaris-supported locales
1414 * use 0x0a for any purpose other than newline, so looking for '\n'
1415 * seems safe.
1416 * All locales use high-bit-set leadin characters for their multi-byte
1417 * sequences, so a line consisting solely of ".\n" is what it appears
1418 * to be.
1420 static void
1421 readinitblock(void)
1423 char buf[100];
1424 boolean_t bol;
1426 bol = B_TRUE;
1427 for (;;) {
1428 if (fgets(buf, sizeof (buf), stdin) == NULL)
1429 return;
1430 if (bol && strcmp(buf, ".\n") == 0)
1431 return;
1432 bol = (strchr(buf, '\n') != NULL);
1435 #else /* !DYNAMIC_SU */
1436 static void
1437 update_audit(struct passwd *pwd)
1439 adt_session_data_t *ah; /* audit session handle */
1441 if (adt_start_session(&ah, NULL, ADT_USE_PROC_DATA) != 0) {
1442 message(ERR, gettext("Sorry"));
1443 if (dosyslog)
1444 syslog(LOG_CRIT, "'su %s' failed for %s "
1445 "cannot start audit session %m",
1446 pwd->pw_name, username);
1447 closelog();
1448 exit(2);
1450 if (adt_set_user(ah, pwd->pw_uid, pwd->pw_gid, pwd->pw_uid,
1451 pwd->pw_gid, NULL, ADT_UPDATE) != 0) {
1452 if (dosyslog)
1453 syslog(LOG_CRIT, "'su %s' failed for %s "
1454 "cannot update audit session %m",
1455 pwd->pw_name, username);
1456 closelog();
1457 exit(2);
1460 #endif /* DYNAMIC_SU */
1463 * Report an error, either a fatal one, a warning, or a usage message,
1464 * depending on the mode parameter.
1466 /*ARGSUSED*/
1467 static void
1468 message(enum messagemode mode, char *fmt, ...)
1470 char *s;
1471 va_list v;
1473 va_start(v, fmt);
1474 s = alloc_vsprintf(fmt, v);
1475 va_end(v);
1477 #ifdef DYNAMIC_SU
1478 if (embedded) {
1479 if (mode == WARN) {
1480 (void) printf("CONV 1\n");
1481 (void) printf("PAM_ERROR_MSG\n");
1482 } else { /* ERR, USAGE */
1483 (void) printf("ERROR\n");
1485 if (mode == USAGE) {
1486 quotemsg("%s", s);
1487 } else { /* ERR, WARN */
1488 quotemsg("%s: %s", myname, s);
1490 } else {
1491 #endif /* DYNAMIC_SU */
1492 if (mode == USAGE) {
1493 (void) fprintf(stderr, "%s\n", s);
1494 } else { /* ERR, WARN */
1495 (void) fprintf(stderr, "%s: %s\n", myname, s);
1497 #ifdef DYNAMIC_SU
1499 #endif /* DYNAMIC_SU */
1501 free(s);
1505 * Return a pointer to the last path component of a.
1507 static char *
1508 tail(char *a)
1510 char *p;
1512 p = strrchr(a, '/');
1513 if (p == NULL)
1514 p = a;
1515 else
1516 p++; /* step over the '/' */
1518 return (p);
1521 static char *
1522 alloc_vsprintf(const char *fmt, va_list ap1)
1524 va_list ap2;
1525 int n;
1526 char buf[1];
1527 char *s;
1530 * We need to scan the argument list twice. Save off a copy
1531 * of the argument list pointer(s) for the second pass. Note that
1532 * we are responsible for va_end'ing our copy.
1534 va_copy(ap2, ap1);
1537 * vsnprintf into a dummy to get a length. One might
1538 * think that passing 0 as the length to snprintf would
1539 * do what we want, but it's defined not to.
1541 * Perhaps we should sprintf into a 100 character buffer
1542 * or something like that, to avoid two calls to snprintf
1543 * in most cases.
1545 n = vsnprintf(buf, sizeof (buf), fmt, ap2);
1546 va_end(ap2);
1549 * Allocate an appropriately-sized buffer.
1551 s = malloc(n + 1);
1552 if (s == NULL) {
1553 perror("malloc");
1554 exit(4);
1557 (void) vsnprintf(s, n+1, fmt, ap1);
1559 return (s);