version 1.35, 2000/12/02 22:44:49 |
version 1.36, 2001/05/29 21:40:36 |
|
|
#include <string.h> |
#include <string.h> |
#include <syslog.h> |
#include <syslog.h> |
#include <unistd.h> |
#include <unistd.h> |
#include <fcntl.h> |
#ifdef __STDC__ |
|
#include <stdarg.h> |
#ifdef SKEY |
|
#include <skey.h> |
|
#endif |
|
|
|
#ifdef KERBEROS |
|
#include <des.h> |
|
#include <kerberosIV/krb.h> |
|
#include <netdb.h> |
|
|
|
int kerberos __P((char *username, char *user, int uid)); |
|
|
|
#define ARGSTR "-Kc:flm" |
|
|
|
int use_kerberos = 1; |
|
char krbtkfile[MAXPATHLEN]; |
|
char lrealm[REALM_SZ]; |
|
int ksettkfile(char *); |
|
#else |
#else |
#define ARGSTR "-c:flm" |
#include <varargs.h> |
#endif |
#endif |
|
#include <bsd_auth.h> |
|
|
|
#define ARGSTR "-a:c:fKlm" |
|
|
char *ontty __P((void)); |
char *ontty __P((void)); |
int chshell __P((char *)); |
int chshell __P((char *)); |
|
void usage __P((void)); |
|
void auth_err __P((auth_session_t *, int, const char *, ...)); |
|
void auth_errx __P((auth_session_t *, int, const char *, ...)); |
|
|
int |
int |
main(argc, argv) |
main(argc, argv) |
|
|
char **argv; |
char **argv; |
{ |
{ |
extern char **environ; |
extern char **environ; |
register struct passwd *pwd; |
enum { UNSET, YES, NO } iscsh; |
register char *p, **g; |
struct passwd *pwd; |
struct group *gr; |
struct group *gr; |
uid_t ruid; |
uid_t ruid; |
login_cap_t *lc; |
login_cap_t *lc; |
int asme, ch, asthem, fastlogin, prio; |
auth_session_t *as; |
enum { UNSET, YES, NO } iscsh; |
int asme, asthem, authok, ch, fastlogin, prio; |
char *user, *shell, *avshell, *username, *class, **np; |
char *class, *style, *p, **g; |
|
char *user, *shell, *avshell, *username, **np, *fullname; |
char shellbuf[MAXPATHLEN], avshellbuf[MAXPATHLEN]; |
char shellbuf[MAXPATHLEN], avshellbuf[MAXPATHLEN]; |
|
|
iscsh = UNSET; |
iscsh = UNSET; |
shell = class = NULL; |
class = shell = style = NULL; |
asme = asthem = fastlogin = 0; |
asme = asthem = fastlogin = 0; |
while ((ch = getopt(argc, argv, ARGSTR)) != -1) |
while ((ch = getopt(argc, argv, ARGSTR)) != -1) |
switch((char)ch) { |
switch(ch) { |
#ifdef KERBEROS |
case 'a': |
case 'K': |
if (style) |
use_kerberos = 0; |
usage(); |
|
style = optarg; |
break; |
break; |
#endif |
|
case 'c': |
case 'c': |
|
if (class) |
|
usage(); |
class = optarg; |
class = optarg; |
break; |
break; |
case 'f': |
case 'f': |
fastlogin = 1; |
fastlogin = 1; |
break; |
break; |
|
case 'K': |
|
if (style) |
|
usage(); |
|
style = "passwd"; |
|
break; |
case '-': |
case '-': |
case 'l': |
case 'l': |
asme = 0; |
asme = 0; |
|
|
break; |
break; |
case '?': |
case '?': |
default: |
default: |
(void)fprintf(stderr, |
usage(); |
"usage: su [%s] [login [shell arguments]]\n", |
|
ARGSTR); |
|
exit(1); |
|
} |
} |
argv += optind; |
argv += optind; |
|
|
|
|
(void)setpriority(PRIO_PROCESS, 0, -2); |
(void)setpriority(PRIO_PROCESS, 0, -2); |
openlog("su", LOG_CONS, 0); |
openlog("su", LOG_CONS, 0); |
|
|
|
if ((as = auth_open()) == NULL) { |
|
syslog(LOG_ERR, "auth_open: %m"); |
|
err(1, "unable to begin authentication"); |
|
} |
|
|
/* get current login name and shell */ |
/* get current login name and shell */ |
ruid = getuid(); |
ruid = getuid(); |
username = getlogin(); |
username = getlogin(); |
|
|
pwd->pw_uid != ruid) |
pwd->pw_uid != ruid) |
pwd = getpwuid(ruid); |
pwd = getpwuid(ruid); |
if (pwd == NULL) |
if (pwd == NULL) |
errx(1, "who are you?"); |
auth_errx(as, 1, "who are you?"); |
if ((username = strdup(pwd->pw_name)) == NULL) |
if ((username = strdup(pwd->pw_name)) == NULL) |
err(1, "can't allocate memory"); |
auth_err(as, 1, "can't allocate memory"); |
if (asme) { |
if (asme) { |
if (pwd->pw_shell && *pwd->pw_shell) { |
if (pwd->pw_shell && *pwd->pw_shell) { |
shell = strncpy(shellbuf, pwd->pw_shell, sizeof(shellbuf) - 1); |
strlcpy(shellbuf, pwd->pw_shell, sizeof(shellbuf)); |
shellbuf[sizeof(shellbuf) - 1] = '\0'; |
shell = shellbuf; |
} else { |
} else { |
shell = _PATH_BSHELL; |
shell = _PATH_BSHELL; |
iscsh = NO; |
iscsh = NO; |
|
|
|
|
/* get target login information, default to root */ |
/* get target login information, default to root */ |
user = *argv ? *argv : "root"; |
user = *argv ? *argv : "root"; |
np = *argv ? argv : argv-1; |
np = *argv ? argv : argv - 1; |
|
|
if ((pwd = getpwnam(user)) == NULL) |
if ((pwd = getpwnam(user)) == NULL) |
errx(1, "unknown login %s", user); |
auth_errx(as, 1, "unknown login %s", user); |
if ((user = strdup(pwd->pw_name)) == NULL) |
if ((user = strdup(pwd->pw_name)) == NULL) |
err(1, "can't allocate memory"); |
auth_err(as, 1, "can't allocate memory"); |
|
|
/* If the user specified a login class and we are root, use it */ |
/* If the user specified a login class and we are root, use it */ |
if (ruid && class) |
if (ruid && class) |
errx(1, "only the superuser may specify a login class"); |
auth_errx(as, 1, "only the superuser may specify a login class"); |
if (class) |
if (class) |
pwd->pw_class = class; |
pwd->pw_class = class; |
if ((lc = login_getclass(pwd->pw_class)) == NULL) |
if ((lc = login_getclass(pwd->pw_class)) == NULL) |
errx(1, "no such login class: %s", |
auth_errx(as, 1, "no such login class: %s", |
class ? class : LOGIN_DEFCLASS); |
class ? class : LOGIN_DEFCLASS); |
|
|
#if KERBEROS |
|
if (ksettkfile(user)) |
|
use_kerberos = 0; |
|
#endif |
|
|
|
if (ruid) { |
if (ruid) { |
#ifdef KERBEROS |
/* |
if (!use_kerberos || kerberos(username, user, pwd->pw_uid)) |
* If we are trying to become root and the default style |
#endif |
* is being used, don't bother to look it up (we might be |
{ |
* be su'ing up to fix /etc/login.conf) |
/* only allow those in group zero to su to root. */ |
*/ |
|
if ((pwd->pw_uid || !style || strcmp(style, LOGIN_DEFSTYLE)) && |
|
(style = login_getstyle(lc, style, "auth-su")) == NULL) |
|
auth_errx(as, 1, "invalid authentication type"); |
|
if (pwd->pw_uid || strcmp(user, "root") != 0) |
|
fullname = user; |
|
else { |
|
if ((fullname = |
|
malloc(strlen(username) + 6)) == NULL) |
|
auth_err(as, 1, NULL); |
|
(void)sprintf(fullname, "%s.root", username); |
|
} |
|
/* |
|
* Let the authentication program know whether they are |
|
* in group wheel or not (if trying to become super user) |
|
*/ |
if (pwd->pw_uid == 0 && (gr = getgrgid((gid_t)0)) |
if (pwd->pw_uid == 0 && (gr = getgrgid((gid_t)0)) |
&& gr->gr_mem && *(gr->gr_mem)) |
&& gr->gr_mem && *(gr->gr_mem)) { |
for (g = gr->gr_mem;; ++g) { |
for (g = gr->gr_mem; *g; ++g) { |
if (!*g) |
if (strcmp(username, *g) == 0) { |
errx(1, "you are not in the correct group to su %s.", user); |
auth_setoption(as, "wheel", "yes"); |
if (strcmp(username, *g) == 0) |
|
break; |
break; |
} |
} |
/* if target requires a password, verify it */ |
|
if (*pwd->pw_passwd) { |
|
p = getpass("Password:"); |
|
#ifdef SKEY |
|
if (strcasecmp(p, "s/key") == 0) { |
|
if (skey_authenticate(user)) |
|
goto badlogin; |
|
} else |
|
#endif |
|
if (strcmp(pwd->pw_passwd, crypt(p, pwd->pw_passwd))) { |
|
badlogin: |
|
fprintf(stderr, "Sorry\n"); |
|
syslog(LOG_AUTH|LOG_WARNING, |
|
"BAD SU %s to %s%s", username, |
|
user, ontty()); |
|
exit(1); |
|
} |
} |
|
if (!*g) |
|
auth_setoption(as, "wheel", "no"); |
} |
} |
} |
|
if (pwd->pw_expire && time(NULL) >= pwd->pw_expire) { |
auth_verify(as, style, fullname, lc->lc_class, NULL); |
fprintf(stderr, "Sorry - account expired\n"); |
authok = auth_getstate(as); |
syslog(LOG_AUTH|LOG_WARNING, "BAD SU %s to %s%s", username, |
if ((authok & AUTH_ALLOW) == 0) { |
user, ontty()); |
if ((p = auth_getvalue(as, "errormsg")) != NULL) |
exit(1); |
fprintf(stderr, "%s\n", p); |
} |
fprintf(stderr, "Sorry\n"); |
|
syslog(LOG_AUTH|LOG_WARNING, |
|
"BAD SU %s to %s%s", username, user, ontty()); |
|
auth_close(as); |
|
exit(1); |
|
} |
} |
} |
|
|
if (asme) { |
if (asme) { |
/* if asme and non-standard target shell, must be root */ |
/* if asme and non-standard target shell, must be root */ |
if (!chshell(pwd->pw_shell) && ruid) |
if (!chshell(pwd->pw_shell) && ruid) |
errx(1, "permission denied (shell)."); |
auth_errx(as, 1, "permission denied (shell)."); |
} else if (pwd->pw_shell && *pwd->pw_shell) { |
} else if (pwd->pw_shell && *pwd->pw_shell) { |
shell = pwd->pw_shell; |
shell = pwd->pw_shell; |
iscsh = UNSET; |
iscsh = UNSET; |
|
|
if (asthem) { |
if (asthem) { |
p = getenv("TERM"); |
p = getenv("TERM"); |
if ((environ = calloc(1, sizeof (char *))) == NULL) |
if ((environ = calloc(1, sizeof (char *))) == NULL) |
errx(1, "calloc"); |
auth_errx(as, 1, "calloc"); |
if (setusercontext(lc, pwd, pwd->pw_uid, LOGIN_SETPATH)) |
if (setusercontext(lc, pwd, pwd->pw_uid, LOGIN_SETPATH)) |
err(1, "unable to set user context"); |
auth_err(as, 1, "unable to set user context"); |
if (p) { |
if (p && setenv("TERM", p, 1) == -1) |
if (setenv("TERM", p, 1) == -1) |
auth_err(as, 1, "unable to set environment"); |
err(1, "unable to set environment"); |
|
} |
|
|
|
seteuid(pwd->pw_uid); |
seteuid(pwd->pw_uid); |
setegid(pwd->pw_gid); |
setegid(pwd->pw_gid); |
if (chdir(pwd->pw_dir) < 0) |
if (chdir(pwd->pw_dir) < 0) |
err(1, "%s", pwd->pw_dir); |
auth_err(as, 1, "%s", pwd->pw_dir); |
seteuid(0); |
seteuid(0); |
setegid(0); /* XXX use a saved gid instead? */ |
setegid(0); /* XXX use a saved gid instead? */ |
} else if (pwd->pw_uid == 0) { |
} else if (pwd->pw_uid == 0) { |
/* XXX - this seems questionable to me */ |
|
if (setusercontext(lc, |
if (setusercontext(lc, |
pwd, pwd->pw_uid, LOGIN_SETPATH|LOGIN_SETUMASK)) |
pwd, pwd->pw_uid, LOGIN_SETPATH|LOGIN_SETUMASK)) |
err(1, "unable to set user context"); |
auth_err(as, 1, "unable to set user context"); |
} |
} |
if (asthem || pwd->pw_uid) { |
if (asthem || pwd->pw_uid) { |
if (setenv("LOGNAME", pwd->pw_name, 1) == -1 || |
if (setenv("LOGNAME", pwd->pw_name, 1) == -1 || |
setenv("USER", pwd->pw_name, 1) == -1) |
setenv("USER", pwd->pw_name, 1) == -1) |
err(1, "unable to set environment"); |
auth_err(as, 1, "unable to set environment"); |
} |
} |
if (setenv("HOME", pwd->pw_dir, 1) == -1 || |
if (setenv("HOME", pwd->pw_dir, 1) == -1 || |
setenv("SHELL", shell, 1) == -1) |
setenv("SHELL", shell, 1) == -1) |
err(1, "unable to set environment"); |
auth_err(as, 1, "unable to set environment"); |
} |
} |
|
|
#ifdef KERBEROS |
|
if (*krbtkfile) { |
|
if (setenv("KRBTKFILE", krbtkfile, 1) == -1) |
|
err(1, "unable to set environment"); |
|
} |
|
#endif |
|
|
|
if (iscsh == YES) { |
if (iscsh == YES) { |
if (fastlogin) |
if (fastlogin) |
*np-- = "-f"; |
*np-- = "-f"; |
|
|
|
|
if (asthem) { |
if (asthem) { |
avshellbuf[0] = '-'; |
avshellbuf[0] = '-'; |
strncpy(avshellbuf+1, avshell, sizeof(avshellbuf) - 2); |
strlcpy(avshellbuf+1, avshell, sizeof(avshellbuf) - 1); |
avshellbuf[sizeof(avshellbuf) - 1] = '\0'; |
|
avshell = avshellbuf; |
avshell = avshellbuf; |
} else if (iscsh == YES) { |
} else if (iscsh == YES) { |
/* csh strips the first character... */ |
/* csh strips the first character... */ |
avshellbuf[0] = '_'; |
avshellbuf[0] = '_'; |
strncpy(avshellbuf+1, avshell, sizeof(avshellbuf) - 2); |
strlcpy(avshellbuf+1, avshell, sizeof(avshellbuf) - 1); |
avshellbuf[sizeof(avshellbuf) - 1] = '\0'; |
|
avshell = avshellbuf; |
avshell = avshellbuf; |
} |
} |
|
|
|
|
if (setusercontext(lc, pwd, pwd->pw_uid, |
if (setusercontext(lc, pwd, pwd->pw_uid, |
(asthem ? (LOGIN_SETPRIORITY | LOGIN_SETUMASK) : 0) | |
(asthem ? (LOGIN_SETPRIORITY | LOGIN_SETUMASK) : 0) | |
LOGIN_SETRESOURCES | LOGIN_SETGROUP | LOGIN_SETUSER)) |
LOGIN_SETRESOURCES | LOGIN_SETGROUP | LOGIN_SETUSER)) |
err(1, "unable to set user context"); |
auth_err(as, 1, "unable to set user context"); |
|
if (pwd->pw_uid && auth_approval(as, lc, pwd->pw_name, "su") <= 0) |
|
auth_err(as, 1, "approval failure"); |
|
auth_close(as); |
|
|
execv(shell, np); |
execv(shell, np); |
err(1, "%s", shell); |
err(1, "%s", shell); |
|
|
chshell(sh) |
chshell(sh) |
char *sh; |
char *sh; |
{ |
{ |
register char *cp; |
char *cp; |
|
|
while ((cp = getusershell()) != NULL) |
while ((cp = getusershell()) != NULL) |
if (strcmp(cp, sh) == 0) |
if (strcmp(cp, sh) == 0) |
|
|
char * |
char * |
ontty() |
ontty() |
{ |
{ |
char *p, *ttyname(); |
|
static char buf[MAXPATHLEN + 4]; |
static char buf[MAXPATHLEN + 4]; |
|
char *p; |
|
|
buf[0] = 0; |
buf[0] = 0; |
if ((p = ttyname(STDERR_FILENO))) |
if ((p = ttyname(STDERR_FILENO))) |
|
|
return (buf); |
return (buf); |
} |
} |
|
|
#ifdef KERBEROS |
void |
int koktologin __P((char *, char *, char *)); |
usage() |
|
|
int |
|
kerberos(username, user, uid) |
|
char *username, *user; |
|
int uid; |
|
{ |
{ |
KTEXT_ST ticket; |
extern char *__progname; |
AUTH_DAT authdata; |
|
struct hostent *hp; |
|
int kerno, fd; |
|
in_addr_t faddr; |
|
char hostname[MAXHOSTNAMELEN], savehost[MAXHOSTNAMELEN]; |
|
char *ontty(), *krb_get_phost(); |
|
|
|
/* Don't bother with Kerberos if there is no srvtab file */ |
(void)fprintf(stderr, "usage: %s [%s] [-a auth-type] %s ", |
if ((fd = open(KEYFILE, O_RDONLY, 0)) < 0) |
"[-c login-class] [login [argument ...]]\n", __progname, ARGSTR); |
return (1); |
exit(1); |
close(fd); |
|
|
|
if (koktologin(username, lrealm, user) && !uid) { |
|
(void)fprintf(stderr, "kerberos su: not in %s's ACL.\n", user); |
|
return (1); |
|
} |
|
(void)krb_set_tkt_string(krbtkfile); |
|
|
|
/* |
|
* Set real as well as effective ID to 0 for the moment, |
|
* to make the kerberos library do the right thing. |
|
*/ |
|
if (setuid(0) < 0) { |
|
warn("setuid"); |
|
return (1); |
|
} |
|
|
|
/* |
|
* Little trick here -- if we are su'ing to root, |
|
* we need to get a ticket for "xxx.root", where xxx represents |
|
* the name of the person su'ing. Otherwise (non-root case), |
|
* we need to get a ticket for "yyy.", where yyy represents |
|
* the name of the person being su'd to, and the instance is null |
|
*/ |
|
|
|
printf("%s%s@%s's ", (uid == 0 ? username : user), |
|
(uid == 0 ? ".root" : ""), lrealm); |
|
fflush(stdout); |
|
kerno = krb_get_pw_in_tkt((uid == 0 ? username : user), |
|
(uid == 0 ? "root" : ""), lrealm, |
|
"krbtgt", lrealm, DEFAULT_TKT_LIFE, 0); |
|
|
|
if (kerno != KSUCCESS) { |
|
if (kerno == KDC_PR_UNKNOWN) { |
|
warnx("kerberos principal unknown: %s.%s@%s", |
|
(uid == 0 ? username : user), |
|
(uid == 0 ? "root" : ""), lrealm); |
|
return (1); |
|
} |
|
warnx("unable to su: %s", krb_err_txt[kerno]); |
|
syslog(LOG_NOTICE|LOG_AUTH, |
|
"BAD Kerberos SU: %s to %s%s: %s", |
|
username, user, ontty(), krb_err_txt[kerno]); |
|
return (1); |
|
} |
|
|
|
/* |
|
* Set the owner of the ticket file to root but bail if someone |
|
* has nefariously swapped a link in place of the file. |
|
*/ |
|
fd = open(krbtkfile, O_RDWR|O_NOFOLLOW, 0); |
|
if (fd == -1) { |
|
warn("unable to open ticket file"); |
|
(void)unlink(krbtkfile); |
|
return (1); |
|
} |
|
if (fchown(fd, uid, -1) < 0) { |
|
warn("fchown"); |
|
(void)unlink(krbtkfile); |
|
return (1); |
|
} |
|
close(fd); |
|
|
|
(void)setpriority(PRIO_PROCESS, 0, -2); |
|
|
|
if (gethostname(hostname, sizeof(hostname)) == -1) { |
|
warn("gethostname"); |
|
dest_tkt(); |
|
return (1); |
|
} |
|
|
|
(void)strncpy(savehost, krb_get_phost(hostname), sizeof(savehost) - 1); |
|
savehost[sizeof(savehost) - 1] = '\0'; |
|
|
|
kerno = krb_mk_req(&ticket, "rcmd", savehost, lrealm, 33); |
|
|
|
if (kerno == KDC_PR_UNKNOWN) { |
|
warnx("Warning: TGT not verified."); |
|
syslog(LOG_NOTICE|LOG_AUTH, |
|
"%s to %s%s, TGT not verified (%s); %s.%s not registered?", |
|
username, user, ontty(), krb_err_txt[kerno], |
|
"rcmd", savehost); |
|
} else if (kerno != KSUCCESS) { |
|
warnx("Unable to use TGT: %s", krb_err_txt[kerno]); |
|
syslog(LOG_NOTICE|LOG_AUTH, "failed su: %s to %s%s: %s", |
|
username, user, ontty(), krb_err_txt[kerno]); |
|
dest_tkt(); |
|
return (1); |
|
} else { |
|
if (!(hp = gethostbyname(hostname))) { |
|
warnx("can't get addr of %s", hostname); |
|
dest_tkt(); |
|
return (1); |
|
} |
|
(void)memcpy((void *)&faddr, (void *)hp->h_addr, sizeof(faddr)); |
|
|
|
if ((kerno = krb_rd_req(&ticket, "rcmd", savehost, faddr, |
|
&authdata, "")) != KSUCCESS) { |
|
warnx("unable to verify rcmd ticket: %s", |
|
krb_err_txt[kerno]); |
|
syslog(LOG_NOTICE|LOG_AUTH, |
|
"failed su: %s to %s%s: %s", username, |
|
user, ontty(), krb_err_txt[kerno]); |
|
dest_tkt(); |
|
return (1); |
|
} |
|
} |
|
return (0); |
|
} |
} |
|
|
int |
void |
koktologin(name, realm, toname) |
#ifdef __STDC__ |
char *name, *realm, *toname; |
auth_err(auth_session_t *as, int eval, const char *fmt, ...) |
|
#else |
|
auth_err(va_alist) |
|
va_dcl |
|
#endif |
{ |
{ |
register AUTH_DAT *kdata; |
va_list ap; |
AUTH_DAT kdata_st; |
#ifdef __STDC__ |
|
va_start(ap, fmt); |
|
#else |
|
auth_session_t *as; |
|
int eval; |
|
const char *fmt; |
|
|
memset((void *)&kdata_st, 0, sizeof(kdata_st)); |
va_start(ap); |
kdata = &kdata_st; |
as = va_arg(ap, auth_session_t *); |
|
eval = va_arg(ap, int); |
(void)strncpy(kdata->pname, name, sizeof(kdata->pname) - 1); |
fmt = va_arg(ap, const char *); |
kdata->pname[sizeof(kdata->pname) - 1] = '\0'; |
#endif |
|
verr(eval, fmt, ap); |
(void)strncpy(kdata->pinst, |
auth_close(as); |
((strcmp(toname, "root") == 0) ? "root" : ""), sizeof(kdata->pinst) - 1); |
va_end(ap); |
kdata->pinst[sizeof(kdata->pinst) -1] = '\0'; |
|
|
|
(void)strncpy(kdata->prealm, realm, sizeof(kdata->prealm) - 1); |
|
kdata->prealm[sizeof(kdata->prealm) -1] = '\0'; |
|
|
|
return (kuserok(kdata, toname)); |
|
} |
} |
|
|
int |
void |
ksettkfile(user) |
#ifdef __STDC__ |
char *user; |
auth_errx(auth_session_t *as, int eval, const char *fmt, ...) |
|
#else |
|
auth_errx(va_alist) |
|
va_dcl |
|
#endif |
{ |
{ |
if (krb_get_lrealm(lrealm, 1) != KSUCCESS) |
va_list ap; |
return (1); |
#ifdef __STDC__ |
(void)snprintf(krbtkfile, sizeof(krbtkfile), "%s_%s_%u", TKT_ROOT, |
va_start(ap, fmt); |
user, getuid()); |
#else |
return (0); |
auth_session_t *as; |
} |
int eval; |
|
const char *fmt; |
|
|
|
va_start(ap); |
|
as = va_arg(ap, auth_session_t *); |
|
eval = va_arg(ap, int); |
|
fmt = va_arg(ap, const char *); |
#endif |
#endif |
|
verrx(eval, fmt, ap); |
|
auth_close(as); |
|
va_end(ap); |
|
} |