#!mkcmd #* +--------------------------------------------------------------------+ */ #* | Copyright 1991, David Koblas. | */ #* | Permission to use, copy, modify, and distribute this software | */ #* | and its documentation for any purpose and without fee is hereby | */ #* | granted, provided that the above copyright notice appear in all | */ #* | copies and that both that copyright notice and this permission | */ #* | notice appear in supporting documentation. This software is | */ #* | provided "as is" without express or implied warranty. | */ #* +--------------------------------------------------------------------+ */ # This version has been modified substantially by KS Braunsdorf, for # example it is coded in mkcmd rather than pure C, and it know about lots # more mnemonic attributes, and -u, -g, -f options. # $Info: sed -n -e "/UnIqueS[t]art/,/UNIqu[e]End/s!.*\tcase '\\(.*\\)': /\\* \\(.*\\)[\t ]\\*/!\\1 is \\2!p" %f from '' from '' from '' from '' from '' from '' from '' from '' from '' from '' from '' from '' from '' from '' from '' from '' from '' from '' from '' from '' from '' from '' from '' from '"machine.h"' from '"main.h"' require "util_getlogingr.m" "util_cache.m" require "std_help.m" "std_version.m" basename "op" "" %i static const char acDefPam[] = "op"; #if USE_PAM #include #endif static void CheckProg(const cmd_t *, const char *, int *, const struct stat *); static char *FullPath(char *, const char *); %% named "Progname" char* named 'FullProgname' { hidden help "" } init 1 'FullProgname = (char *)0 == argv[0] ? Progname : argv[0];' init 3 'openlog(acDefPam, 0, LOG_AUTH);' init 4 'CheckIndirect(%#, %@);' after 'if (fReadExisting) ReadFile(acAccess); else pcAccess = acAccess;' char* 'u' { named "pcUid" track "fUid" init "(char *)0" user "CheckColon(& %n, & %(respect)gn);" param "login" help "specify the login for the process, as allowed" } char* 'g' { named "pcGid" track "fGid" init "(char *)0" param "group" help "specify the group for the process, as allowed" } char* 'f' { named "pcSecFile" track "fSecFile" init "(char *)0" param "file" help "specify direct file for the $f macro, as allowed" } boolean 'S' { exclude "fguHl" named "fSanity" help "sanity check rule database" boolean 'n' { named "fReadExisting" init '1' help "do not read the existing rule-base" } list { named "SanityCheck" param "files" help "optional configuration files to check" } } integer named "fWhichList" { init "'_'" help "" } action 'lrw' { exclude "fguHS" help "list rules available to this session (list, run, why)" update "fWhichList = %w;" list { named "RulesAllowed" update "%n(fWhichList, %#, %@);" param "login" help "the superuser may request a list for any login" } } augment action 'V' { user "Version();" } action 'H' { update "ConfHelp(stdout);" aborts "exit(EX_OK);" help "show only help on the configuration of this program" } list { param "mnemonic" named "OldMain" update "%n(%#, (const char **)%@);" help "specify the mnemonic and any arguments" } zero { named "" update "/* none */" aborts 'fatal("Provide mnemonic, or use -h for help");' } %h /* Each configuration stanza is represented by an `op definition' structure. */ typedef struct ODnode { const char *pcname; /* mnemonic name */ int nargs, nopts; /* current counts */ int margs, mopts; /* max allocated currently */ int iline; /* defining line and file */ const char *pcfile; const char **args, **opts; /* vectors of args and attrs */ struct ODnode *pODnext; /* next command in order */ struct ODnode *pODdefscope; /* the last DEFAULT we saw */ int fcarp; /* flag use in sanity checks */ const char *pcscript; /* { in-line script } text */ } cmd_t; %% %i static char rcsid[] = "$Id: op.m,v 2.155 2012/10/05 16:52:00 ksb Exp $"; #if !defined(HAVE_SETFIB) #define HAVE_SETFIB defined(SO_SETFIB) #endif #if !defined(OP_OPTIONS) /* $^ support */ #define OP_OPTIONS 1 #endif #if HAVE_SETFIB #include #endif #if VARARGS #include #else /* VARARGS */ #include #endif /* VARARGS */ #if !defined(ACCESS_FILE) #define ACCESS_FILE "/usr/local/lib/op/access.cf" #endif #if USE_GETSPNAM #include #endif #if !defined(OP_GIDLIST_MAX) #if defined(NGROUPS) #define OP_GIDLIST_MAX NGROUPS #else #if defined(NGROUPS_MAX) #define OP_GIDLIST_MAX NGROUPS_MAX #else #define OP_GIDLIST_MAX 256 #endif #endif #endif #if !defined(IOV_MAX) #define IOV_MAX 16 /* min writev supports, that ksb has seen */ #endif #if !defined(OP_MAX_LDWIDTH) #define OP_MAX_LDWIDTH 48 /* length of a printf("__%ld", max_long) */ #endif /* 128*l(2)/l(10) => 38.53, we round up */ #if !defined(PATH_SUDO) #define PATH_SUDO "sudo" #endif /* When an option looks like an environment definition return true */ #if !defined(ENV_OPEN) #define ENV_OPEN(Mpc) ('$' == (Mpc)[0] && (isalpha((Mpc)[1]) || '_' == (Mpc)[1] || '$' == (Mpc)[1])) #endif static const char *pcPerp = "", *pcPerpHome = (const char *)0, *pcPerpShell = (const char *)0, *pcCmd = (const char *)0, *pcAccess = (const char *)0, acAccess[] = ACCESS_FILE, acDefault[] = "DEFAULT", acDefNotEnv[] = "[%/]", /* from sudo's env_check */ acDefShell[] = "/bin/sh", #if USE_PAM || defined(TIOCNOTTY) acDevTty[] = "/dev/tty", acMyHost[] = "localhost", /* pam session source */ #endif acDevNull[] = "/dev/null", /* the null device */ acShell[] = "$SHELL", /* the name $S reads */ acMagicShell[] = "MAGIC_SHELL", /* emulate su, mostly */ acEcho[] = "echo", /* builtin echo command */ acOptNetgroups[] = "netgroups", /* allow netgroup members */ acOptGroups[] = "groups", /* allow by membership/gid */ acOptUsers[] = "users", /* allow by login name/uid */ acOptPass[] = "password", /* must type a password */ acOptPam[] = "pam", /* like password, with pam */ acOptSession[] = "session", /* let pam build a session */ acOptCleanup[] = "cleanup", /* close the session */ acOptDaemon[] = "daemon", /* run in background */ acOptDir[] = "dir", /* change pwd */ acOptChroot[] = "chroot", /* change root */ acOptUid[] = "uid", /* new real uid */ acOptEuid[] = "euid", /* different effective uid */ acOptGid[] = "gid", /* new real gid */ acOptEgid[] = "egid", /* different effective gid */ acOptInitgrps[] = "initgroups", /* set group list */ acOptNice[] = "nice", /* set priority */ acOptJacket[] = "jacket", /* manager process */ acOptHelmet[] = "helmet", /* authorization process */ acOptNolog[] = "nolog", /* e.g. run from cron */ acOptMask[] = "umask", /* set file creation mask */ acOptStdin[] = "stdin", /* change fd 0 */ acOptStdout[] = "stdout", /* change fd 1 */ acOptStderr[] = "stderr", /* change fd 2 */ acOptMac[] = "mac", /* set process label */ acOptFib[] = "fib", /* set network context */ acOptEnv[] = "environment", /* pass matching $ENVs */ acOptArgv0[] = "basename", /* set/mask argv[0] */ acOptPATH[] = "$PATH", /* set search path */ acPatPerms[] = "fperms", /* compat with Linux op */ acPatFowners[] = "fowners", /* compat with Linux op */ acOptCount[] = "$#", /* count params */ acSpecF[] = "%f", /* ask for file */ acSpecD[] = "%d", /* ask for dirname(file) */ acSpecU[] = "%u", /* ask for -u user */ acSpecG[] = "%g", /* ask for -g group */ acSpecMe[] = "%l", /* ask for Perp */ acDupU[] = "initgroups,session,cleanup,gid,egid,uid,euid,chroot,dir", acKeyInitgroup[] = "%i", /* copy initgroups */ acOK[] = "Access granted", /* little information */ acSure[] = ", are you sure?", /* grep -v this when you are */ acBrokenCode[] = "Internal invariant failure", #if OP_MAC_SUPPORT acMacDeny[] = "-m policy restricted", #endif acBadAllow[] = "Permission denied", acBadConfig[] = "Configuration error", /* help the Admin */ acBadAlloc[] = "Out of memory", /* out of resources */ acBadList[] = "No matching parameter list", /* radiate some info */ acBadParam[] = "Argument rejected", acBadNumber[] = "Wrong number of arguments", acBadFew[] = "Too few arguments", acBadMnemonic[] = "No such mnemonic"; static cmd_t *NewCmd(const char *pcName); #if defined(DEBUG) /* Dump out the given command about the way we read it. (ksb) */ static const cmd_t * CmdOutput(const cmd_t *cmd) { register int i; fprintf(stderr, "%s\t", cmd->pcname); for (i = 0; i < cmd->nargs; ++i) fprintf(stderr, "%s ", cmd->args[i]); if ((cmd_t *)0 != cmd->pODdefscope) { fprintf(stderr, "[defaults from %s:%d] ", cmd->pODdefscope->pcfile, cmd->pODdefscope->iline); } fprintf(stderr, ";\n"); for (i = 0; i < cmd->nopts; ++i) fprintf(stderr, "\t%s\n", cmd->opts[i]); fprintf(stderr, "\n"); return cmd; } #else #define CmdOutput(Mcmd) (Mcmd) #endif /* DEBUG */ /* Varargs function to output a message to stderr and exit failure. (orig) * Also syslogs the access failure when pcCmd is set. */ #if VARARGS static void fatal(va_alist) va_dcl { auto va_list ap; register char *s; va_start(ap); if ((const char *)0 != pcCmd) { syslog(LOG_NOTICE, "user %s FAILED to execute %s", pcPerp, pcCmd); } s = va_arg(ap, char *); fprintf(stderr, "%s: ", Progname); vfprintf(stderr, s, ap); fputc('\n', stderr); va_end(ap); exit(EX_DATAERR); /*NOTREACHED*/ } #else /* VARARGS */ static void fatal(const char *format, ...) { auto va_list ap; va_start(ap, format); if ((const char *)0 != pcCmd) { syslog(LOG_NOTICE, "user %s FAILED to execute %s", pcPerp, pcCmd); } fprintf(stderr, "%s: ", Progname); vfprintf(stderr, format, ap); fputc('\n', stderr); va_end(ap); exit(EX_DATAERR); /*NOTREACHED*/ } #endif /* VARARGS */ #if !defined(RUN_AS) #define RUN_AS "/usr/local/bin/op" #endif /* If op is installed non-setuid, indirect through sudo to get root (ksb) * access, that way we can run on a host that doesn't let us install setuid * but does allow a policy to delegate details to op. How strange. * Naming a mnemonic "op" might confuse the loop check below. * * In sudoers allow "op" to run us with any args, euid as root, uid as * themselves, I'd leave the groups as-is. I don't use this feature. */ static void CheckIndirect(int argc, char **argv) { #if HAVE_issetugid /* We have a system call the check, use it */ if (issetugid()) return; #endif #if RUN_AS_MORTAL /* we are setuid to a mortal login, and we can't see that * because we don't have issetugid(2) */ return; #else /* Try to indirect via sudo, this almost never works because the * rule you need diables sudo for any other purpose. --ksb */ { register int i; register char **ppcNew; if (0 == getuid() || 0 == geteuid() || getegid() != getgid()) return; if ((char *)0 != argv[1] && 0 == strcmp(Progname, argv[1])) fprintf(stderr, "%s: possible sudo/op loop\n", Progname); /* Building: "sudo" Progname argv[0] argv[1]... (char *)0 */ if ((char **)0 == (ppcNew = calloc((argc|3)+5, sizeof(char *)))) fatal(acBadAlloc); i = 0; ppcNew[i++] = PATH_SUDO; ppcNew[i++] = "-u"; ppcNew[i++] = "root"; for (ppcNew[i++] = RUN_AS; 0 != argc--; /* move along */) ppcNew[i++] = *argv++; ppcNew[i] = (char *)0; (void)execvp(*ppcNew, ppcNew); fatal("execvp: %s: %s", *ppcNew, strerror(errno)); /*NOTREACHED*/ } #endif } /* Keep track of a gid list, only add the unique ones. (ksb) */ static void AddGid(const gid_t gNew, int *pnCount, gid_t *pwSet) { register int i; for (i = 0; i < *pnCount; ++i) { if (*pwSet == gNew) return; ++pwSet; } if (OP_GIDLIST_MAX == *pnCount) { fatal("%s: out of space in grouplist", acOptGid); } *pwSet = gNew; ++*pnCount; } /* Search for keywords, like "uid" or "uid=pat" (orig) */ static char * FindOpt(const cmd_t *cmd, const char *pcAttr) { register int i; register char *cp; register int iLook; static char acEmpty[2] = ""; iLook = strlen(pcAttr); for (i = 0; i < cmd->nopts; ++i) { if ((char *)0 == (cp = strchr(cmd->opts[i], '='))) { /* uid w/o an = is the same as the empty string */ if (0 == strcmp(cmd->opts[i], pcAttr)) return acEmpty; } else if (iLook != (cp - cmd->opts[i])) { continue; } else if (0 == strncmp(cmd->opts[i], pcAttr, iLook)) { return cp+1; } } return (char *)0; } /* Copy out each of N comma separated values in order, a comma before (ksb) * a comma removes the separator meaning -- better than a backslash. * Empty fields don't help op any. * Always EndField() to reset the sanity checks after you copy or don't * need the returned buffer. N.B. you can't have more than 1 active loop. * Call with pcCur as NULL to free and reset the memory buffer. */ static const char * GetField(const char *pcCur, char **ppcProcess) { register char *pcOut; register size_t iLen; register int c; static char *pcKeep = (char *)0; static size_t iKeep = 0; #if defined(DEBUG) static char **ppcLast = (char **)0; #endif if ((char **)0 == ppcProcess) { if ((char *)0 != pcKeep) { free((void *)pcKeep); pcKeep = (char *)0; iKeep = 0; } } if ((const char *)0 == pcCur) { #if defined(DEBUG) ppcLast = (char **)0; #endif return (const char *)0; } #if defined(DEBUG) if ((char **)0 == ppcLast) ppcLast = ppcProcess; else if (ppcProcess != ppcLast) fatal("GetField snark"); #endif if ('\000' == *pcCur) { *ppcProcess = (char *)0; return (char *)0; } if (((iLen = strlen(pcCur)) > iKeep) || (char *)0 == pcKeep) { /* Since we use the same buffer for all calls make * it big enough we might not need to resize it. */ iKeep = (iLen|4095)+1; if ((char *)0 == pcKeep) pcKeep = malloc(iKeep); else pcKeep = realloc((void *)pcKeep, iKeep); } if ((char *)0 == pcKeep) { fatal(acBadAlloc); } if ((char **)0 != ppcProcess) { *ppcProcess = pcKeep; } for (pcOut = pcKeep; '\000' != (c = *pcCur); ++pcCur) { if (',' == c && ',' != pcCur[1]) break; *pcOut++ = c; if (',' == c) ++pcCur; } *pcOut = '\000'; return ('\000' == *pcCur) ? pcCur : (pcCur+1); } #if !defined(EndField) #if defined(DEBUG) #define EndField() GetField((char *)0, (char **)0) #else #define EndField() /* nada, saving 544 bytes of text segment */ #endif #endif /* already defined */ /* Since we only ever have two REs compiled at a time, and we want to (ksb) * diddle the RE for some tags we focus all the RE compiles here. * When (char **)0 != ppcCarp, let the caller decide how to proceed, * in Check code we print errros, in run code we might syslog the issue * without telling the Customer. */ static regex_t * ReComp(const char *pcRE, char **ppcCarp) { static int fAlt = 0; static regex_t aRE[2]; register int iErr; static char acMesg[1024]; if ((const char *)0 == pcRE) return & aRE[fAlt]; fAlt ^= 1; if (0 == (iErr = regcomp(& aRE[fAlt], pcRE, REG_EXTENDED|REG_NOSUB))) return & aRE[fAlt]; (void)regerror(iErr, &aRE[fAlt], acMesg, sizeof(acMesg)); if ((char **)0 == ppcCarp) fatal("regcomp: %s: %s", pcRE, acMesg); *ppcCarp = acMesg; return (regex_t *)0; } /* Match against the last compiled RE. (ksb) */ static int ReMatch(regex_t *pRE, const char *pcValue) { register int iErr; auto regmatch_t aRM[1]; auto char acMesg[1024]; if (pRE != ReComp((const char *)0, (char **)0)) { syslog(LOG_ERR, "please submit a bug report, RE sync bug"); fatal("Please report an internal RE sync bug!"); } aRM[0].rm_so = 0; aRM[0].rm_eo = strlen(pcValue); switch (iErr = regexec(pRE, pcValue, 1, aRM, 0)) { case 0: return 1; case REG_NOMATCH: return 0; default: break; } (void)regerror(iErr, pRE, acMesg, sizeof(acMesg)); fatal("regexec: %s", acMesg); /*NOTREACHED*/ return 0; } /* Return 1 when any field from any listed attribute contains token. (ksb) * List is a concatenation of strings terminated by a double-\000 (constr). */ static int AnyField(const cmd_t *cmd, const char *pcToken, const char *psList) { register const char *pcLook; register regex_t *reg; register int iRet; auto char *pcField, *pcCarp; if ((regex_t *)0 == (reg = ReComp(pcToken, &pcCarp))) { fatal("%s: %s:%d: regcomp: %s: %s", cmd->pcname, cmd->pcfile, cmd->iline, pcToken, pcCarp); } iRet = 0; for (/* param */; 0 == iRet && '\000' != *psList; psList += strlen(psList)+1) { if ((char *)0 == (pcLook = FindOpt(cmd, psList))) continue; while ((char *)0 != (pcLook = GetField(pcLook, &pcField))) if (0 != (iRet = ReMatch(reg, pcField))) break; EndField(); } regfree(reg); return iRet; } /* Remember what a mnemonic `needs' on the command-line usage */ typedef struct NEnode { int fUid, /* saw %u, $u, $U */ fGid, /* saw %g, $g, $G */ fSecFile, /* saw %f, $f */ fSecFd, /* saw $F */ fSecDir, /* saw $d */ fDirFd, /* saw $D */ fMacs, /* saw $m */ fDStar; /* saw $*, $@: makes argc variable */ long lCmdParam, /* highest $[0-9]+ in command template */ lHelpParam, /* highest $[0-9]+ in spec for -l */ lTotalParam; /* $# count or lCmdParam or -1 */ } NEEDS; /* Which indirect objects (-g, -u, -f, -m) we need from the command-line? * SecFd is only tripped by $F. Look for the highest $n mentioned, and * limit argc to include that many words if no $* is mentioned. */ static int Craving(const cmd_t *cmd, NEEDS *pNE) { register int c, iArgx, iOptx, iRet; register const char *pcScan; auto char *pcEnd; auto long ul; pNE->fSecDir = AnyField(cmd, "^%d$", "uid\000euid\000gid\000egid\000initgroups\000session\000cleanup\000password\000dir\000chroot\000\000"); pNE->fSecFile = pNE->fSecDir || AnyField(cmd, "^%f$", "uid\000euid\000gid\000egid\000initgroups\000session\000cleanup\000password\000dir\000chroot\000\000") || AnyField(cmd, "^[<>]*%f$", "stdin\000stdout\000stderr\000\000"); pNE->fUid = AnyField(cmd, "^%u$", "dir\000chroot\000uid\000euid\000gid\000egid\000initgroups\000session\000cleanup\000password\000\000") || AnyField(cmd, "^$|^%u$", "!g@u\000%g@u\000\000"); pNE->fGid = AnyField(cmd, "^%g$", "gid\000egid\000\000"); pNE->fSecFd = pNE->fDirFd = 0; pNE->fMacs = 0; pNE->fDStar = 0; pNE->lCmdParam = 0; pNE->lTotalParam = pNE->lHelpParam = -1; iRet = 0; for (iOptx = iArgx = 0; (iOptx < cmd->nopts && (char *)0 != (pcScan = cmd->opts[iOptx++])) || (iArgx < cmd->nargs && (char *)0 != (pcScan = cmd->args[iArgx++])); /* double cond */) { /* if looking for an environment variable */ if (0 == iArgx) { register int fCheck = 0; if (0 == strncmp(pcScan, "mac=", 4)) { fCheck = 1; } else if (ENV_OPEN(pcScan)) { ++pcScan; fCheck = 1; } else if ('!' == *pcScan || '$' == *pcScan) { ++pcScan; } else { continue; } if ('@' == *pcScan || '*' == *pcScan) continue; if (isdigit(*pcScan)) { ul = strtol(pcScan, (char **)0, 10); /* we carp about junk on the end in Sanity */ if (-1 == pNE->lHelpParam || ul > pNE->lHelpParam) pNE->lHelpParam = ul; continue; } if ('#' == *pcScan) { if ('=' != pcScan[1]) { if (fSanity) fprintf(stderr, "%s: %s: %s:%d: %c# has no default value\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcScan[-1]); iRet = 4; continue; } pNE->lTotalParam = strtol(pcScan+2, (char **)0, 10); if (0 <= pNE->lTotalParam) continue; pNE->lTotalParam = -1; iRet = 3; if (fSanity) fprintf(stderr, "%s: %s: %s:%d: %c# may not be negative\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcScan[-1]); else syslog(LOG_ERR, "%s: %s: %s:%d: %c# may not be negative (%s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcScan[-1], pcScan+2); continue; } if (!fCheck) { continue; } /* check rest for macro expansion */ } while ('\000' != (c = *pcScan++)) { if ('$' != c) { continue; } if ('$' == (c = *pcScan++) || '_' == c || '.' == c || '|' == c || '@' == c || '+' == c || '-' == c || ',' == c || '*' == c || '#' == c || 'S' == c) { pNE->fDStar |= '*' == c || '@' == c; continue; } if ('s' == c) { if ((char *)0 == cmd->pcscript) { fprintf(stderr, "%s: %s: %s:%d: $s used without an in-line script\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); iRet = 5; } continue; } if ((char *)0 != strchr("cChHiIlLrRaAtToOnNeE~bBwWpPvVqQxkKyYzZ^", c)) { continue; } if ('{' == c) { register const char *pcKeep; pcKeep = pcScan; if ((const char *)0 == (pcEnd = strchr(pcScan, '}'))) { fprintf(stderr, "%s: %s: %s:%d: end of argument in ${env} expression\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); iRet = 6; break; } if (pcEnd == pcScan && fSanity) { /* ${} */ fprintf(stderr, "%s: %s: %s:%d: ${} is meaningless\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); ++pcScan; iRet = 7; break; } /* Tell sh programmers all digits ${...} is bad */ do { ++pcScan; } while (isdigit(*pcScan)); if (pcEnd == pcScan && fSanity) { fprintf(stderr, "%s: %s: %s:%d: ${%.*s} is all digits, did you mean $%.*s?\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, (int)(pcScan-pcKeep), pcKeep, (int)(pcScan-pcKeep), pcKeep); /* we don't fail here, env/op allows */ } pcScan = pcEnd+1; continue; } if ('\\' == c) { if ('\000' == *pcScan) { fprintf(stderr, "%s: %s: %s:%d: end of argument in backslash escape\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); iRet = 2; break; } ++pcScan; continue; } if ('u' == c || 'U' == c) { pNE->fUid = 1; continue; } if ('g' == c || 'G' == c) { pNE->fGid = 1; continue; } if ('d' == c || 'D' == c) { pNE->fSecFile = 1; pNE->fSecDir = 1; pNE->fDirFd |= 'D' == c; continue; } if ('f' == c || 'F' == c) { pNE->fSecFile = 1; pNE->fSecFd |= 'F' == c; continue; } if ('m' == c || 'M' == c) { #if !OP_MAC_SUPPORT static const char *pcVetchMac = (char *)0; if (pcVetchMac != cmd->pcfile && fSanity) { fprintf(stderr, "%s: %s: %s:%d: unsupported MAC specifications in configuration file\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); pcVetchMac = cmd->pcfile; } #endif pNE->fMacs = 'm' == c; continue; } if ('X' == c) { if (fSanity && (char *)0 == FindOpt(cmd, acOptChroot)) { fprintf(stderr, "%s: %s: %s:%d: uses $X without a chroot set\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); } continue; } if (!isdigit(c)) { fatal("%s: unknown expansion $%c", cmd->pcname, c); } ul = strtoul(pcScan-1, & pcEnd, 10); pcScan = pcEnd; if (pNE->lCmdParam < ul) { pNE->lCmdParam = ul; } } } /* When no $* (or $@) is expanded set a fixed argument list. */ if (!pNE->fDStar && -1 == pNE->lTotalParam) { pNE->lTotalParam = pNE->lCmdParam; } if (-1 != pNE->lTotalParam && pNE->lHelpParam < pNE->lTotalParam) { pNE->lHelpParam = pNE->lTotalParam; } if (-1 != pNE->lTotalParam && pNE->lCmdParam > pNE->lTotalParam) { fprintf(stderr, "%s: %s: %s:%d: $%lu beyond given argument list\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pNE->lCmdParam); iRet = 1; } return iRet; } /* Generic attribute match, not a group or login, any(REs) ~= string. (ksb) * Note that the OldSchool regexec we keep returns 1 for a match, * people used to the new interface be aware of that logic reversal. */ static int GenMatch(const char *pcCause, const char *pcValue, const char *pcList) { register const char *pcCur; register regex_t *reg; register int iRet; auto char *pcField, *pcCarp; iRet = 0; if ((char *)0 == pcList) { return iRet; } for (pcCur = GetField(pcList, &pcField); 0 == iRet && (char *)0 != pcCur; pcCur = GetField(pcCur, &pcField)) { if ((regex_t *)0 == (reg = ReComp(pcField, &pcCarp))) fatal("%s: regcomp: %s: %s", pcCause, pcField, pcCarp); iRet = ReMatch(reg, pcValue); regfree(reg); } EndField(); return iRet; } /* Locate the command the Customer asked for from the list. (ksb) * We match the $# first (if configured) then $1,$2,... here, but not $* * rndc /usr/sbin/rndc $1 ; * $1=^start$,^stop$ * rndc /etc/init.d/named $2 ; * $1=^restart$ */ static cmd_t * Find(const char *pcMnemonic, const long iArgc, const char **ppcArgv, const char **ppcWrong) { register int i, iCur; register cmd_t *cmd; register const char *pcCount, *pcAttr; auto char *pcEnd; auto char acCvt[OP_MAX_LDWIDTH]; /* $n from config */ static char acDummy[] = "=."; /* deault match for $n */ for (cmd = NewCmd((const char *)0); (cmd_t *)0 != cmd; cmd = cmd->pODnext) { if (0 != strcmp(cmd->pcname, pcMnemonic)) { continue; } if ((char *)0 != (pcCount = FindOpt(cmd, acOptCount)) && iArgc != strtol(pcCount, &pcEnd, 0)+1) { if (acBadList != *ppcWrong) *ppcWrong = acBadNumber; continue; } if ((char *)0 != (pcCount = FindOpt(cmd, "!#")) && iArgc == strtol(pcCount, &pcEnd, 0)+1) { if (acBadList != *ppcWrong) *ppcWrong = acBadNumber; continue; } for (i = 0; i < cmd->nopts; ++i) { pcAttr = cmd->opts[i]; if (!('!' == pcAttr[0] || '$' == pcAttr[0]) || !isdigit(pcAttr[1])) { continue; } iCur = strtol(pcAttr+1, &pcEnd, 0); /* Trap !3 means argc <= 3, vs !3=. means "" != $3 * otherwise we fail when argc <= n (no $n) */ if ('!' == *pcAttr && '\000' == *pcEnd) { if (iArgc > iCur) break; continue; } if (iArgc <= iCur) { break; } snprintf(acCvt, sizeof(acCvt), "%.*s", (int)(pcEnd-pcAttr), pcAttr); if ('\000' == *pcEnd) { pcEnd = acDummy; } if (GenMatch(acCvt, ppcArgv[iCur], pcEnd+1)) { if ('!' == pcAttr[0]) break; } else if ('$' == pcAttr[0]) { break; } } if (i == cmd->nopts) break; *ppcWrong = acBadList; } return cmd; } %% %c static const char acCompOpts[] = #if OP_OPTIONS "options," #if !SENTINEL "no" #endif "sentinel," #if !OP_SHOW_RULES "no" #endif "showrules," #if !RUN_AS_MORTAL "no" #endif "mortal," #if !USE_PAM "no" #endif "pam," #if !DEBUG "no" #endif "debug," #if !defined(TEXT_HOSTTYPE) "nohosttype" #else TEXT_HOSTTYPE #endif #if defined(HOSTOS) #define op_sTrInG(Mx) #Mx /*space*/ #define op_InDirEct(My) op_sTrInG(My) "(" op_InDirEct(HOSTOS) ")" #undef op_sTrInG #undef op_InDirEct #endif #else "nooptions" #endif ; static char asFKnown[] = /* see FileAttr below */ "path\000dev\000ino\000nlink\000atime\000mtime\000ctime\000btime\000birthtime\000size\000blksize\000blocks\000uid\000login@g\000login\000owners\000gid\000group\000mode\000perms\000access\000type\000\000"; static const char acMyself[] = "."; /* token might change later */ static const char acCurDir[] = "."; /* UNIX name for current directory */ /* Everyplace we take "." for the Customer's login we should take %l (ksb) */ static int /* inline */ IsMyself(const char *const pcCheck) { return (char *)0 == pcCheck ? 0 : 0 == strcmp(acMyself, pcCheck) ? 1 : 0 == strcmp(acSpecMe, pcCheck) ? 1 : 0; } static int ReadFile(const char *); /* Info we need for version output. (ksb) */ static void Version() { register int uReal, uPriv, gReal, gPriv; register struct passwd *pw; register struct group *grp; auto char acUser[96], acGroup[96]; /* login or group names */ setpwent(); pw = (struct passwd *)0; setgrent(); grp = (struct group *)0; uPriv = geteuid(); gPriv = getegid(); uReal = getuid(); gReal = getgid(); #if SENTINEL #if !defined(SENTINEL_MODE_MASK) #define SENTINEL_MODE_MASK 0022 /* don't allow group or world write */ #endif /* A mortal login in 5 groups may sentinel each group. The super user * may sentinel anything she likes. */ if (0 == uPriv || 0 == uReal || gPriv != gReal) { register char *pcTail; auto char acSentDir[MAXPATHLEN+4]; auto struct stat stCfg; if (0 != strcmp(acAccess, strncpy(acSentDir, acAccess, sizeof(acSentDir))) || (char *)0 == (pcTail = strrchr(acSentDir, '/')) || 0 != strcmp(Progname, strncpy(pcTail+1, Progname, sizeof(acSentDir)-(pcTail-acSentDir)))) { printf("%s: sentinel computation overflows path length\n", Progname); } else if (-1 == stat(acSentDir, &stCfg)) { if (0 != strcmp(acDefPam, Progname)) printf("%s: no sentinel configuration for group `%s\'\n", Progname, Progname); } else if ((struct group *)0 == (grp = getgrnam(Progname))) { printf("%s: sentinel: no group `%s\'\n", Progname, Progname); } else if (stCfg.st_gid != grp->gr_gid) { printf("%s: sentinel: group id (%ld) doesn't match directory gid (%ld)\n", Progname, (long)grp->gr_gid, (long)stCfg.st_gid); } else if (0 != (stCfg.st_mode&SENTINEL_MODE_MASK)) { printf("%s: sentinel: directory too permissive (%04o includes bits from %04o)\n", Progname, (stCfg.st_mode & 07777), SENTINEL_MODE_MASK); } else { uReal = -1; gReal = -1; uPriv = stCfg.st_uid; gPriv = stCfg.st_gid; } } /* move the the sentinel name if we have see one */ ReadFile(acAccess); #endif printf("%s: access file `%s'\n", Progname, pcAccess); printf("%s: using regex\n", Progname); printf("%s: multiple configuration files accepted\n", Progname); printf("%s: in-line script and $s accepted\n", Progname); #if OP_OPTIONS printf("%s: %s\n", Progname, acCompOpts); #endif #if USE_PAM printf("%s: with pam support, default application \"%s\"\n", Progname, acDefPam); #endif #if SENTINEL printf("%s: with sentinel mode mask %04o\n", Progname, SENTINEL_MODE_MASK); #endif if (uReal == uPriv) uPriv = -1; else if ((struct passwd *)0 != (pw = getpwuid(uPriv))) snprintf(acUser, sizeof(acUser), "login %s", pw->pw_name); else snprintf(acUser, sizeof(acUser), "uid %ld", (long)uPriv); if (gReal == gPriv) gPriv = -1; else if ((struct group *)0 != (grp = getgrgid(gPriv))) snprintf(acGroup, sizeof(acGroup), "group %s", grp->gr_name); else snprintf(acGroup, sizeof(acGroup), "gid %ld", (long)gPriv); /* Sadly we don't have issetugid every place we need it. */ #if HAVE_issetugid if (!issetugid() && 0 == getuid()) printf("%s: run as root\n", Progname); else #endif if (-1 != gPriv && -1 == uPriv && 0 == uReal) printf("%s: managing superuser, %s\n", Progname, acGroup); else if (-1 == gPriv && -1 == uPriv && 0 == uReal) printf("%s: managing superuser\n", Progname); else if (-1 != gPriv && -1 != uPriv) printf("%s: managing %s, %s\n", Progname, acUser, acGroup); else if (-1 != uPriv) printf("%s: managing %s\n", Progname, acUser); else if (-1 != gPriv) printf("%s: managing %s\n", Progname, acGroup); else /* RUN_AS_MORTAL is set */ printf("%s: not setuid or setgid\n", Progname); } static const char *apcConfHelp[] = { "$1\000the specified positional parameter must match one of these REs", "$*\000every other positional parameter must match on of these REs", "$#\000count of the number of words allowed on the command line", "!1\000the specified parameter must not match any REs (others likewise)", "basename\000force a different argv[0] for the process", "chroot\000change root first, %f, %d, %u, %i, %l, or a path", #if USE_PAM "cleanup\000close our PAM session, specification as session, %u, %f, %i, or . (to match session)", #else "cleanup\000[unsupported] fork a process to close our PAM session, specification as session", #endif "daemon\000double-fork the process into the background and redirect I/O to /dev/null, boolean", "dir\000change directory here first, %f, %d, %u, %i, %l, or a path", "environment\000allow all existing environment variables to pass, or limit to matching REs", "gid\000force the real uid to this group, gid, %f, %d, %u, %g, or %l (invoker's gid)", "egid\000make the effective gid different from the gid (same values)", "fowners\000an alias for %_.owners", "fperms\000an alias for %_.perms", "uid\000force the real uid to this login, uid, %f, %d, %u, or %l (invoker's uid)", "euid\000make the effective uid different from the uid (same values)", "users\000allow execution only for logins that match one of these REs", "groups\000allow execution only for logins that have a group matching one of these REs", "netgroups\000allow execution for logins included in any listed group", "helmet\000a program to run a final check before execution", "jacket\000a clean-up program which is the parent of the new program", "initgroups\000set the supplementary groups for login, %f, %d, or %u, or %l", #if OP_MAC_SUPPORT "mac\000process MAC label, an expanded string include $m to force -m", #endif "nice\000bias the new scheduling priority", "nolog\000skip logging for all successful executions", #if HAVE_SETFIB "fib\000specify an alternate network view", #else "fib\000[unsupported platform] specify an alternate network view", #endif "password\000credential access via %f, %d, %u, %l, or login's password", #if USE_PAM "pam\000pam application credentials required for execution, default as .", "session\000set pam session attributes for this login, %f, %d, %u, %l, or %i", #else "pam\000[unsupported] pam application credentials required for execution, default as .", "session\000[unsupported] set pam session attributes for this login, %f, %d, %u, %l, or %i", #endif "stdin\000set a forced standard input, %f or a path", "stdout\000set a forced standard output, %f or a path", "stderr\000set a forced standard error, %f or a path", "umask\000set the processes umask (default 022)", "$ENV\000pass environment $ENV as is, or set a specific value", "%{ENV}\000the new $ENV value must match one of these REs", "!{ENV}\000the new $ENV value must not match any of these REs", "%d.attr\000%d's stat(2) attr must match one of these REs", "!d.attr\000%d's stat(2) attr must not match any of these REs", "%u\000-u's login must match one of these REs", "!u\000when -u's login matches any of these REs, reject the attempt", "%u@g\000allow a -u login that is in at least one group that matches one of these REs", "!u@g\000reject any -u login that is in any group that matches one of these REs", "%g\000-g's group must match one of these REs", "!g\000-g's group must not match any of these REs", "%g@u\000allow a -g group that contains at least one login that matches one of these REs", "!g@u\000reject any -g group that contains any login that matches one of these REs", "%f.attr\000%f's stat(2) attr must match one of these REs", "!f.attr\000%f's stat(2) attr must not match any of these REs", #if OP_MAC_SUPPORT "%m\000-m's spelling must match one of these REs", "!m\000-m's spelling must not match any of these REs", #endif "%_.attr\000$_'s stat(2) attr must match one of these REs", "!_.attr\000$_'s stat(2) attr must not match any of these REs", "attr\000one of path,dev,ino,nlink,atime,mtime,ctime,btime,birthtime,size,blksize,blocks,uid,login,owners,gid,group,login@g,mode,perms,access or type", (const char *)0 }; static const char *apcExpHelp[] = { "$a client groups (gid) list\000$b privileged group (gid)", "$c configuration file (directory)\000$d -f specification's directory (fd)", "$e privileged login (uid)\000$f -f specification (fd)", "$g -g specification (gid)\000$h client login's home (targets)", "$i initgroups login (uid)\000$j unused macro (also unused)", "$k client login's shell (targets)\000$l client login (uid)", #if OP_MAC_SUPPORT "$m -m MAC (current) process label\000$n new group (gid) list", #else "$m unsupported MAC label\000$n new group (gid) list", #endif "$o target group (gid)\000$p pam session login (uid)", "$q basename of our name (as given)\000$r client real group (gid)", "$s in-line script (shell)\000$t target login (uid)", "$u -u login (uid)\000$v version of op (numeric)", "$w which configuration file (line)\000$x target directory (root)", "$y tty name (orignal umask)\000$z escalated pid (original ppid)", "$~ privileged login's home\000${env} client's value of $env", "$_ target program or script\000$| the empty string", "$1 positional parameter, $2...\000$# count of parameters specified", "$@ parameters as words ($* one word)\000$. break word here [internal]", "$- request as words ($+ one word)\000$\\ tr escape [a,b,f,n,r,t,v,s,d,o,q]", "$$ a literal dollar\000$^ build options description", (const char *)0 }; /* Show the customer what op can take in a configuration file. (ksb) */ static void ConfHelp(FILE *fpOut) { register int i, iWide, iLen; register const char *pc; fprintf(fpOut, "Configuration format:\n\tmnemonic\tcommand [args] ;\n\t\t\tkeyword[=value][,value...] ...\nmnemonic: a name the customer can remember, or the word \"%s\" (which picks\n\tup at keywords) to set default keywords until the next occurrence or EOF\ncommand: a macro expanded path, or the string %s, or an in-line script\n\tin curly braces. The close curly must be the first nonwhite-space\n\tcharacter on a line.\nargs: expanded with the macros listed below to form the command's parameters\n\tReplacing the semicolon with an ampersand (&) sets the daemon option\nkeywords:\n", acDefault, acMagicShell); iWide = 3; for (i = 0; (const char *)0 != (pc = apcConfHelp[i++]); ) { iLen = strlen(pc); if (iLen > iWide) iWide = iLen; } for (i = 0; (const char *)0 != (pc = apcConfHelp[i++]); ) { fprintf(fpOut, "%*s %s\n", iWide, pc, pc+strlen(pc)+1); } fprintf(fpOut, "RE is an abbreviation for regular expression, see regex(3)\n"); fprintf(fpOut, "\nTarget command parameters and environment macros (upper case):\n"); iWide = 1; for (i = 0; (const char *)0 != (pc = apcExpHelp[i++]); ) { iLen = strlen(pc); if (iLen > iWide) iWide = iLen; } for (i = 0; (const char *)0 != (pc = apcExpHelp[i++]); ) { fprintf(fpOut, " %*s %s\n", -iWide, pc, pc+strlen(pc)+1); } fflush(fpOut); } /* If -u ksb:sac then make it -u ksb and -g sac, like All Good Programs (ksb) * _but_ should we set -g? No. That lets us suggest a group w/o forcing * the %g macro check. Very subtle backward compatible, or a little broken. */ static void CheckColon(char **ppcUser, char **ppcGroup) { register char *pcColon; if ((char **)0 == ppcGroup || (char **)0 == ppcUser || (char *)0 == *ppcUser) return; if ((char *)0 == (pcColon = strchr(*ppcUser, ':'))) return; *pcColon++ = '\000'; *ppcGroup = pcColon; } /* Return a new command structure on the end of the list. (ksb) * When given a name of (char *)0 return the head command structure. */ static cmd_t * NewCmd(const char *pcName) { static cmd_t *pCMHead = (cmd_t *)0; static cmd_t **ppCMEnd = & pCMHead; register cmd_t *cmd; if ((char *)0 == pcName) { return pCMHead; } if ((cmd_t *)0 == (cmd = malloc(sizeof(cmd_t)))) { fatal(acBadAlloc); } cmd->pcname = pcName; cmd->fcarp = 0; cmd->nargs = cmd->nopts = 0; cmd->margs = cmd->mopts = 16; cmd->pcfile = acDevNull; cmd->iline = -1; cmd->args = (const char **)malloc(sizeof(char *)*cmd->margs); cmd->opts = (const char **)malloc(sizeof(char *)*cmd->mopts); cmd->pcscript = (const char *)0; if ((const char **)0 == cmd->args || (const char **)0 == cmd->opts) { fatal(acBadAlloc); } cmd->pODnext = (cmd_t *)0; *ppCMEnd = cmd; ppCMEnd = & cmd->pODnext; return cmd; } /* Simple list accumulation, not the way I'd do it. (ksb) * We save memory here because we don't run all but one of these. Must rules * have < 15 options or aguments. */ static void AddElem(int *piMax, int *piCur, const char ***pppcVec, const char *pcValue) { if (*piMax == *piCur) { *piMax += 32; *pppcVec = (const char **)realloc((void *)(*pppcVec), sizeof(char *) * *piMax); if ((const char **)0 == (*pppcVec)) fatal(acBadAlloc); } (*pppcVec)[(*piCur)++] = pcValue; } /* Parse a whole file into command structure. (ksb) * (Replaces the huge lex spec with the mkcmd file-cache code.) */ static void OneFile(int fdIn, char *pcFile) { register int c, cSawSemi; register unsigned int uCur, fEat; register cmd_t *cmd, *cmdDef; register char *pcCur, *pcTok, *pcDef; auto unsigned int uLines; cmd = cmdDef = (cmd_t *)0; pcDef = pcTok = (char *)0; cSawSemi = '\000'; pcCur = cache_file(fdIn, &uLines); for (uCur = 1; uCur <= uLines; ++uCur, ++pcCur) { if ('\000' == *pcCur || '#' == *pcCur) { pcCur += strlen(pcCur); continue; } if (';' == *pcCur || '&' == *pcCur) { fatal("%s:%d \"%c\" is not a possible mnemonic", pcFile, uCur, *pcCur); } if (!isspace(*pcCur)) { if ((cmd_t *)0 != cmd && '&' == cSawSemi) { AddElem(&cmd->mopts, &cmd->nopts, &cmd->opts, acOptDaemon); } for (pcDef = pcCur; '\000' != (c = *pcCur); ++pcCur) { if (isspace(c) || ';' == c || '&' == c) break; } while (isspace(c)) { *pcCur++ = '\000'; c = *pcCur; } /* The DEFAULT pseudo mnemonic is auto terminated */ cSawSemi = (0 == strcmp(pcDef, acDefault)) ? ';' : '\000'; cmd = NewCmd('\000' != cSawSemi ? acDefault : pcDef); cmd->pcfile = pcFile; cmd->iline = uCur; cmd->pODdefscope = cmdDef; if ('\000' != cSawSemi) { cmdDef = cmd; } if (';' == c || '&' == c) { cSawSemi = c; *pcCur = '\000'; c = *++pcCur; } /* mnemonic { # long shell script that * # spans mulitple lines and ends with a * # close curly as the first non-white * } args ; ... * run with $S -c $s $1 $2 * where $s is "the text w/o the outer curlies" */ switch (cSawSemi) { register char *pcEnd; static const char acDefCmd[] = "/bin/sync"; case ';': case '&': if (cmd == cmdDef) { break; } /* Configured a null command, make it sync. */ AddElem(&cmd->margs, &cmd->nargs, &cmd->args, acDefCmd); if (fSanity) { fprintf(stderr, "%s: %s: %s:%d: empty argument list, assumed \"%s\"\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acDefCmd); } break; case '\000': /* look for in-line script */ if ('{' != c || !('\000' == pcCur[1] || isspace(pcCur[1]))) /*}*/{ break; } cmd->pcscript = pcCur+1; while (uCur < uLines) { ++uCur; pcCur += strlen(pcCur); *pcCur++ = '\n'; while (isspace(*pcCur)) ++pcCur; if (/*{*/'}' == *pcCur) break; } /*{*/ if ('}' != *pcCur) { /*{*/ fatal("%s:%d: Missing '}' for in-line script", pcFile, uCur); } pcEnd = pcCur++; do { *pcEnd-- = '\000'; } while ('\n' != *pcEnd); AddElem(&cmd->margs, &cmd->nargs, &cmd->args, "$S"); AddElem(&cmd->margs, &cmd->nargs, &cmd->args, "-c"); AddElem(&cmd->margs, &cmd->nargs, &cmd->args, "$s"); break; } } for (fEat = 0; '\000' != (c = *pcCur); /* nope */) { if (fEat || isspace(c)) { ++pcCur; continue; } if (';' == c || '&' == c) { ++pcCur; if ((cmd_t *)0 == cmd) { fatal("%s:%d %c doesn't terminate a parameter list", pcFile, uCur, c); } if ('\000' != cSawSemi) { fatal("%s:%d double argument separator", pcFile, uCur); } if (0 == cmd->nargs && acDefault != cmd->pcname) { fatal("%s:%d %s: has no command template", pcFile, uCur, cmd->pcname); } cSawSemi = c; continue; } if ('#' == c) { ++pcCur; fEat = 1; continue; } for (pcTok = pcCur; '\000' != (c = *pcCur); ++pcCur) { if (isspace(c)) break; } if ('\000' != *pcCur) { *pcCur++ = '\000'; } if ('\000' == *pcTok) { break; } if ((cmd_t *)0 == cmd) { fatal("%s:%d %s definitions must start in column 1", pcFile, uCur, pcTok); } if (fSanity && ('&' == pcCur[-1] || ';' == pcCur[-1])) { fprintf(stderr, "%s: %s: %s:%d: \"%c\" on end of argument needs a space before it to begin attribute list\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcCur[-1]); } if ('\000' != cSawSemi) { AddElem(&cmd->mopts, &cmd->nopts, &cmd->opts, pcTok); } else { AddElem(&cmd->margs, &cmd->nargs, &cmd->args, pcTok); } } } if ((cmd_t *)0 != cmd && '\000' == cSawSemi) { fatal("%s:%d %s: missing definition", pcFile, uCur-1, cmd->pcname); } if ('&' == cSawSemi) { AddElem(&cmd->mopts, &cmd->nopts, &cmd->opts, acOptDaemon); } } /* Create the actual command spec from the default and the spec given. (ksb) * If the first thing in access.cf is a DEFAULT rule then it applies * to all escalations w/o a DEAFAULT before their declatation. */ static cmd_t * Build(cmd_t *cmdOrig) { register cmd_t *cmdRet, *cmdDef; register int i, iDef; register size_t iLen; register char *pcEq; cmdDef = NewCmd((const char *)0); if ((cmd_t *)0 != cmdDef && acDefault != cmdDef->pcname) { cmdDef = (cmd_t *)0; } if ((cmd_t *)0 != cmdOrig->pODdefscope) { cmdDef = cmdOrig->pODdefscope; } if ((cmd_t *)0 == cmdDef) { return cmdOrig; } if ((cmd_t *)0 == (cmdRet = malloc(sizeof(cmd_t)))) { fatal(acBadAlloc); } cmdRet->pcname = cmdOrig->pcname; cmdRet->iline = cmdOrig->iline; cmdRet->pcfile = cmdOrig->pcfile; cmdRet->nargs = cmdOrig->nargs; cmdRet->margs = cmdOrig->margs; cmdRet->mopts = ((cmdOrig->nopts+cmdDef->nopts)|3)+1; cmdRet->args = cmdOrig->args; cmdRet->opts = (const char **)malloc(sizeof(char *)*cmdRet->mopts); cmdRet->pODnext = (cmd_t *)0; cmdRet->pODdefscope = (cmd_t *)0; cmdRet->fcarp = cmdOrig->fcarp; cmdRet->nopts = cmdOrig->nopts; cmdRet->pcscript = cmdOrig->pcscript; for (i = 0; i < cmdOrig->nopts; ++i) { cmdRet->opts[i] = cmdOrig->opts[i]; } for (iDef = 0; iDef < cmdDef->nopts; ++iDef) { if ((char *)0 == (pcEq = strchr(cmdDef->opts[iDef], '='))) { iLen = strlen(cmdDef->opts[iDef]); } else { iLen = pcEq - cmdDef->opts[iDef]; } for (i = 0; i < cmdRet->nopts; ++i) { if (0 != strncmp(cmdRet->opts[i], cmdDef->opts[iDef], iLen)) { continue; } switch (cmdRet->opts[i][iLen]) { case '\000': case '=': break; default: continue; } break; } if (i == cmdRet->nopts) { cmdRet->opts[cmdRet->nopts++] = cmdDef->opts[iDef]; } } cmdRet->opts[cmdRet->nopts] = (char *)0; return (cmd_t *)CmdOutput(cmdRet); } /* Check out a match of a group or login against a list of REs. (ksb/pv) * Set pcTag to (const char *)0 and we'll ignore RE comp errors. */ static int AnyMatch(const char *pcRegList, const char *pcName, const int iId, char **ppcCarp) { register const char *pcRE, *pcCheck; register int fMatch; register regex_t *reg; auto char acCvt[OP_MAX_LDWIDTH]; auto char *pcField; snprintf(acCvt, sizeof(acCvt), "%d", iId); fMatch = 0; for (pcRE = GetField(pcRegList, &pcField); 0 == fMatch && (char *)0 != pcRE; pcRE = GetField(pcRE, &pcField)) { pcCheck = pcName; if ('#' == pcField[0]) { pcCheck = acCvt; if ((regex_t *)0 == (reg = ReComp(pcField+1, ppcCarp))) continue; } else if ((regex_t *)0 == (reg = ReComp(pcField, ppcCarp))) { continue; } if (ReMatch(reg, pcCheck)) fMatch = 1; regfree(reg); } EndField(); return fMatch; } /* Check the group file for membership of the login in any matching (pv/ksb) * group. Don't call getpw* here, the pcName is in the shared buf. */ static int MemberMatch(const char *pcRegList, const char *pcName, const gid_t *pwGid, const char *pcTag) { register struct group *grp; register int fMatch, i; register const char *pcRE; register regex_t *reg; auto char *pcField, *pcCarp; fMatch = 0; setgrent(); pcCarp = (char *)0; while (0 == fMatch && (struct group *)0 != (grp = getgrent())) { for (pcRE = GetField(pcRegList, &pcField); 0 == fMatch && (char *)0 != pcRE; pcRE = GetField(pcRE, &pcField)) { if ((regex_t *)0 == (reg = ReComp(pcField, &pcCarp))) continue; if (0 == ReMatch(reg, grp->gr_name)) { regfree(reg); continue; } regfree(reg); if ((gid_t *)0 != pwGid && *pwGid == grp->gr_gid) { fMatch = 1; break; } for (i = 0; (char *)0 != grp->gr_mem[i]; ++i) { if (0 == strcmp(pcName, grp->gr_mem[i])) { fMatch = 1; break; } } } EndField(); } if ((char *)0 != pcCarp) { syslog(LOG_ERR, "MemberMatch: %s: regcomp: %s", pcTag, pcCarp); } return fMatch; } /* Map a login name or #uid to a real uid, for %u. (ksb) * Consult attributes %u, !u, and %u@g, !u@g * %u list of REs, user must match at least one (^charon$,^entomb$) * !u list of REs, user may never match any (^root$) * %u@g list of REs, user must be a member of a matching group (^ops$) * !u@g list of REs, user must not be a member of a matching group (^wheel$) */ static uid_t MapUid(const char *pcThis, const cmd_t *cmd, gid_t *pwGroup) { register struct passwd *pwd; register char *pcLimit; auto char *pcCarp; pcCarp = (char *)0; if ((char *)0 != pcThis && '#' == *pcThis) { ++pcThis; } if ((char *)0 == pcThis || '\000' == *pcThis) { fatal("Mnemonic %s requires -u", cmd->pcname); } if (isdigit(*pcThis)) { if ((struct passwd *)0 == (pwd = getpwuid(atoi(pcThis)))) { fatal("%s: no such uid #%s", cmd->pcname, pcThis); } } else if ((struct passwd *)0 == (pwd = getpwnam(pcThis))) { fatal("%s: no such user %s", cmd->pcname, pcThis); } if ((gid_t *)0 != pwGroup) { *pwGroup = pwd->pw_gid; } /* apply any injunction against or a members-only list */ if ((char *)0 != (pcLimit = FindOpt(cmd, "!u"))) { if (AnyMatch(pcLimit, pwd->pw_name, pwd->pw_uid, &pcCarp)) fatal("%s: login %s forbidden", cmd->pcname, pwd->pw_name); } if ((char *)0 != (pcLimit = FindOpt(cmd, "%u"))) { if (!AnyMatch(pcLimit, pwd->pw_name, pwd->pw_uid, &pcCarp)) fatal("%s: login %s not in allowed list", cmd->pcname, pwd->pw_name); } if ((char *)0 != (pcLimit = FindOpt(cmd, "!u@g"))) { if (MemberMatch(pcLimit, pwd->pw_name, pwGroup, "!u@g")) fatal("%s: login %s black listed", cmd->pcname, pwd->pw_name); } if ((char *)0 != (pcLimit = FindOpt(cmd, "%u@g"))) { if (!MemberMatch(pcLimit, pwd->pw_name, pwGroup, "%u@g")) fatal("%s: login %s doesn't have a correct group", cmd->pcname, pwd->pw_name); } if ((char *)0 != pcCarp) { syslog(LOG_ERR, "MapUid: regcomp: %s", pcCarp); } return pwd->pw_uid; } /* Check if the login list specified contains a matching login (ksb) * When any RE is "", "%u" or "^%u$" match pcUid as a whole word, if %u is * a number reverse it to a login name first, if not given (!fUid) then you * can't match it anyway (skip the check). Members format "a\000b\000\000". * Bonus %e for exec login (geteuid) and %l for original login (getuid), * since none of those are specail in a regular expression we'll pass Sanity. */ static int /*ARGSUSED*/ MatchesMember(const char *pcRegList, const char *pcMembers, const char *pcTag) { register int fMatch, cWhich; register const char *pcCursor, *pcRE, *pcCheck, *pcWhom; register regex_t *reg; auto char *pcField, *pcCarp; auto char acLoginRE[OP_MAX_LDWIDTH+4]; /* "^46600$" */ fMatch = 0; pcCarp = (char *)0; for (pcRE = GetField(pcRegList, &pcField); 0 == fMatch && (char *)0 != pcRE; pcRE = GetField(pcRE, &pcField)) { register struct passwd *pwd; pcCheck = pcField; cWhich = 'u'; if ('^' == *pcCheck) { ++pcCheck; } if ('%' == *pcCheck && ('u' == pcCheck[1] || 'l' == pcCheck[1] || 'e' == pcCheck[1] || 'f' == pcCheck[1] || 'd' == pcCheck[1])) { cWhich = pcCheck[1]; pcCheck += 2; } if ('$' == *pcCheck) { ++pcCheck; } if ('\000' != *pcCheck) { if ((regex_t *)0 == (reg = ReComp(pcField, &pcCarp))) continue; } else { pcWhom = pcPerp; switch (cWhich) { /* To do %f and %d here we'd have to pass a lot more * to MapUid. XXX */ case 'f': /* -f's onwer */ case 'd': /* -f's dirname's owner */ fatal("no support for checking %s against -f", pcTag); /*NOTREACHED*/ case 'u': /* -u target, only matches when given */ if (!fUid) continue; pcWhom = pcUid; break; case 'l': /* original login name */ /* the default for -Wall flags */ break; case 'e': /* the setuid owner of op itself */ snprintf(acLoginRE, sizeof(acLoginRE), "%ld", (long int)geteuid()); pcWhom = acLoginRE; break; default: fatal("MapGid: %s: internal error", pcTag); /*NOTREACHED*/ } if (isdigit(*pcWhom)) { if ((struct passwd *)0 == (pwd = getpwuid(atoi(pcWhom)))) { fatal("%s: no such uid #%s", pcTag, pcWhom); } snprintf(acLoginRE, sizeof(acLoginRE), "^%s$", pwd->pw_name); if ((regex_t *)0 == (reg = ReComp(acLoginRE, &pcCarp))) continue; } else { snprintf(acLoginRE, sizeof(acLoginRE), "^%s$", pcWhom); if ((regex_t *)0 == (reg = ReComp(acLoginRE, &pcCarp))) continue; } } for (pcCursor = pcMembers; '\000' != *pcCursor; pcCursor += strlen(pcCursor)+1) { if (ReMatch(reg, pcCursor)) { fMatch = 1; break; } } regfree(reg); } EndField(); if ((char *)0 != pcCarp) { syslog(LOG_ERR, "MatchesMember: regcomp: %s", pcCarp); } return fMatch; } /* Same thing for the group, as %g. (ksb) * %g list of REs, group must match at least one (^operator$,^disk$,) * !g list of REs, group may never match any (^wheel$) * %g@u list of REs, group must include at least one user that matches (^ksb$) * !g@u list of REs, groups may never contain a login that matches any RE (^root$) * N.B. For g@u the login must be listed in the group file, not a member by * primary login group (because we check from the perspective of the group). * It would be easy to scan the password file looking for logins with * the group in question, that would also be wrong for our (local) model. */ static gid_t MapGid(char *pcThis, const cmd_t *cmd) { register struct group *grp; register char *pcLimit, *pcBlackList, *pcWhiteList; register gid_t gRet; auto char *pcCarp; pcCarp = (char *)0; if ((char *)0 != pcThis && '#' == *pcThis) { ++pcThis; } if ((char *)0 == pcThis || '\000' == *pcThis) { fatal("Mnemonic %s requires -g", cmd->pcname); } if (isdigit(*pcThis)) { if ((struct group *)0 == (grp = getgrgid(atoi(pcThis)))) fatal("%s: no such gid #%s", cmd->pcname, pcThis); if ((char *)0 == (pcThis = strdup(grp->gr_name))) fatal(acBadAlloc); } else if ((struct group *)0 == (grp = getgrnam(pcThis))) { fatal("%s: no such group %s", cmd->pcname, pcThis); } gRet = grp->gr_gid; if ((char *)0 != (pcLimit = FindOpt(cmd, "!g"))) { if (AnyMatch(pcLimit, pcThis, gRet, &pcCarp)) fatal("%s: group %s black-listed", cmd->pcname, pcThis); } if ((char *)0 != (pcLimit = FindOpt(cmd, "%g"))) { if (!AnyMatch(pcLimit, pcThis, gRet, &pcCarp)) fatal("%s: group %s not in allowed list", cmd->pcname, pcThis); } /* Chance here favors not reporting, since the failed RE may have * forced a fatal error, but that case the Customer will complain. */ if ((char *)0 != pcCarp) { syslog(LOG_ERR, "MapGid: regcomp: %s", pcCarp); } /* Since MatchesMember might call getgrent(3) we can't hold the * static buffer in libc when we call it, so save the member list. * In the Scan loops below you'd add the setpwent loop if you * wanted login group membership to count (which I think is wrong). */ pcBlackList = FindOpt(cmd, "!g@u"); pcWhiteList = FindOpt(cmd, "%g@u"); if ((char *)0 != pcWhiteList || (char *)0 != pcBlackList) { register const char *pcMembers; register char *pvDispose, *pcCursor, **ppcScan; register size_t wLen; wLen = 2; for (ppcScan = grp->gr_mem; (char *)0 != *ppcScan; ++ppcScan) { wLen += strlen(*ppcScan)+1; } pcMembers = pcCursor = pvDispose = malloc((wLen|15)+1); for (ppcScan = grp->gr_mem; (char *)0 != *ppcScan; ++ppcScan) { (void)strcpy(pcCursor, *ppcScan); pcCursor += strlen(pcCursor)+1; } *pcCursor = '\000'; if ((char *)0 != pcWhiteList && !MatchesMember(pcWhiteList, pcMembers, "%g@u")) { fatal("%s: group %s doesn't have any matching login", cmd->pcname, pcThis); } if ((char *)0 != pcBlackList && MatchesMember(pcBlackList, pcMembers, "!g@u")) { fatal("%s: group %s is black-listed by membership", cmd->pcname, pcThis); } /* Done with pcMembers, dispose of it. */ free(pvDispose); } return gRet; } /* Return the highed explicit $n in the cmd's attributes list, for (ksb) * counting how many command-line params participate in mnemonic matching. */ static long MaxParams(const cmd_t *cmd) { register int i; register const char *pcScan; register long lRet, lCur; auto char *pcRest; lRet = 0; for (i = 0; i < cmd->nopts; ++i) { pcScan = cmd->opts[i]; if ('$' != pcScan[0] && '!' != *pcScan) { continue; } if (!isdigit(cmd->opts[i][1])) { continue; } if (0 == (lCur = strtoul(pcScan+1, & pcRest, 10))) { continue; } if (lCur > lRet) lRet = lCur; } return lRet; } /* Return 1 if every element in pcList is in pcListDup, given that (ksb) * pcKeep is long enough to hold copy of any element from pcList. */ static int HasEvery(const char *pcList, char *pcListDup, char *pcKeep) { register int fSaw; register const char *pcScan; auto char *pcField, *pcFieldDup; while ((char *)0 != (pcList = GetField(pcList, &pcField))) { (void)strcpy(pcKeep, pcField); EndField(); fSaw = 0; for (pcScan = GetField(pcListDup, &pcFieldDup); (char *)0 != pcScan; pcScan = GetField(pcScan, &pcFieldDup)) { if (0 != strcmp(pcKeep, pcFieldDup)) continue; fSaw = 1; break; } EndField(); if (!fSaw) return 0; } EndField(); return 1; } /* Do the command have exactly the same $1, $2, $3... $n RE lists? (ksb) * We don't try to see if the REs match the same text, I'm not quite that * crazy. But we do look for some other obvious cases, like a permuted list. * We check for a leading zero botch in the main sanity checker (e.g. $01). * We comment (but do not fail) if two mnemonics are spelled the same from * two different files. */ static int SamePats(const cmd_t *cmd, const cmd_t *cmdDup) { register long l, lMax, lMaxDup; register char *pcList, *pcListDup; register int fSaw; register char *pcMem; register size_t wLen; auto char acCvt[OP_MAX_LDWIDTH]; /* "$%ld" */ lMax = MaxParams(cmd); lMaxDup = MaxParams(cmdDup); if (lMax < lMaxDup) { lMax = lMaxDup; } for (l = 1; l <= lMax; ++l) { if (sizeof(acCvt) <= snprintf(acCvt, sizeof(acCvt), "$%ld", l)) { fatal("%s: $%ld buffer overflow", cmd->pcname, l); } pcList = FindOpt(cmd, acCvt); pcListDup = FindOpt(cmdDup, acCvt); if ((char *)0 == pcList && (char *)0 == pcListDup) continue; if ((char *)0 == pcList || (char *)0 == pcListDup) return 0; /* The two lengths or'd must be at least as big as either * or in 017 and add 1 for the \000 on the end */ wLen = (strlen(pcList)|strlen(pcListDup)|15)+1; if ((char *)0 == (pcMem = malloc(wLen))) { fatal(acBadAlloc); } fSaw = HasEvery(pcList, pcListDup, pcMem) && HasEvery(pcListDup, pcList, pcMem); free((void *)pcMem); if (!fSaw) return 0; } return 1; } /* Trusting the client user's environment might be a Bad Thing. (ksb) * This checks trusting the value in the _old_ environment (1), or one * forced by an assignement (2), or not at all (0). */ static int CheckTrust(const cmd_t *cmd, const char *pcCheck) { register char *pcCur, *pcList; if ((char *)0 != (pcCur = FindOpt(cmd, pcCheck))) { if ('\000' != *pcCur) return 2; /* force to a know value, not `trusted' */ } else if ((char *)0 == (pcList = FindOpt(cmd, acOptEnv))) { return 0; /* no environment trusted */ } else if ('\000' == *pcList) { /* allows all to pass */ } else if (!GenMatch(acOptEnv, pcCheck+1, pcList)) { return 0; /* our target is not in the match list */ } return 1; } /* Most of the next 940 lines (to CheckCompile) is meant to check the * combinations of chroot, dir, and command specification to be sure you are * running what you think you are. We data-mine the REs matching variables, * paths, -f, -u, -g, and such to try to figure out if the configuration * provided can be tricked into running an unexpected binary. * This is not a cake walk. --ksb */ typedef struct FRnode { int afnonlit[2]; /* saw a non-literal */ int frooted; /* all non-lit REs at slash */ int inegated; /* number of matches !dropped */ int iexcluded; /* number of matches and'd out */ int fswapbit; /* passed in to ForceInit */ size_t wmemmax; /* max size of the list */ size_t wmemcur; /* the end of the list */ char *pcmem; /* free when done */ const char *pslist; /* all potential values found */ const char *pcname; /* error message name */ const char *pcmatching; /* positive match limit */ const char *pcmatchname; /* options the make the above */ const char *pcforbid; /* list neg REs e.g. from "!1" */ const char *pcforbidname; /* options that make the above */ const char *pcand; /* RE to and with matches */ const char *pcandname; /* options that make the above */ const char *pcandnot; /* RE to exclude matches */ const char *pcandnotname; /* options that make the above */ const char *pckeep; /* used when swapped for a hold */ /* set by as needed: */ int fswapped; /* we swapped and with primary */ int ffallback; /* we are in fallback matches */ } FORCE_RE; static const cmd_t *cmdVetchTrustEnv = (cmd_t *)0; /* Adding a new match to the buffered disjunctions, which are packed (ksb) * into a psList ("item\000next\000...last\000\000"). If the RE has * an unquoted '*' '+' or '.' in it we should skip it, or \{range\} notation. */ static int AddRE(const char *pcRE, size_t wLen, FORCE_RE *pFR) { register char *pcSnip; register int fExclude = 0; pcSnip = (char *)0; if (pFR->fswapped && (char *)0 != (pcSnip = strrchr(pcRE, '/'))) { if ('\000' == pcSnip[1]) { while (pcSnip > pcRE) { if ('/' != *--pcSnip) break; } while (pcSnip > pcRE+1) { if ('/' != *pcSnip) break; --pcSnip; } } *pcSnip = '\000'; } if ((char *)0 != pFR->pcand && !GenMatch("and RE", pcRE, pFR->pcand)) { fExclude = 1; } else if ((char *)0 != pFR->pcandnot && GenMatch("and not RE", pcRE, pFR->pcandnot)) { fExclude = 1; } if ((char *)0 != pcSnip) { *pcSnip = '/'; } if (fExclude) { pFR->iexcluded += 1; return 0; } if (pFR->wmemmax < (pFR->wmemcur+wLen+2)) { register size_t wResize; wResize = ((2+wLen)|16383)+1; while ((char *)0 == realloc((void *)pFR->pcmem, pFR->wmemmax+wResize)) { if ((wResize -= 1024) < wLen) fatal(acBadAlloc); } pFR->wmemmax += wResize; } (void)strncpy(pFR->pcmem+pFR->wmemcur, pcRE, wLen); pFR->wmemcur += wLen; /* This is where we might need the +2 in the realloc above */ if (pFR->fswapped) { if ('/' != pFR->pcmem[pFR->wmemcur-1]) pFR->pcmem[pFR->wmemcur++] = '/'; pFR->pcmem[pFR->wmemcur++] = '*'; } pFR->pcmem[pFR->wmemcur++] = '\000'; pFR->pcmem[pFR->wmemcur] = '\000'; return 0; } /* When the RE has a range in it fill that in for us (ksb) * We don't do [[:things:]], deal with it. --ksb * and [^13579] is a non-literal, we don't complemnt. */ static int REBracket(const char *pcRE, size_t wLen, FORCE_RE *pFR) { register size_t w, wOpen, wClose; register int c; register char *pcMem, *pcI; auto int iRet; /* find pcRE<0> ... [ range ]right afnonlit[pFR->ffallback] = 1; return 0; } if ('\\' == c) { /* \[ is not special */ w += 1; continue; } if ('[' != c) { continue; } wOpen = w-1; wClose = w+1; while (wClose < wLen && ']' != pcRE[wClose]) ++wClose; if (wClose == wLen) { return 0; } break; } /* Negative ranges are non-literals, since we don't really * want to match control characters and spaces. Use of a * negative match usually is something like "$1=^[^/]*$", * which could just as well be exclude by "!1=/". In either * case we'll not find a literal match for $1. --ksb */ if (wOpen+1 < wLen && '^' == pcRE[wOpen+1]) { pFR->afnonlit[pFR->ffallback] = 1; return 0; } /* No range operations, just add the RE to the list */ if (wClose == wLen) { return AddRE(pcRE, wLen, pFR); } /* Open a range, []0-7] matches 9 possible character. */ if ((char *)0 == (pcMem = malloc(((wOpen+wLen-wClose+1)|7)+1))) { fatal(acBadAlloc); } (void)memset(pcMem, '\000', wOpen+wLen-wClose+1); strncpy(pcMem, pcRE, wOpen); pcI = pcMem+wOpen; strncpy(pcMem+wOpen+1, pcRE+wClose+1, wLen-wClose-1); *pcI = '_'; wLen = strlen(pcMem); iRet = 0; while (w < wClose) { c = pcRE[w++]; if (w+1 < wClose && '-' == pcRE[w]) { ++w; while (c <= pcRE[w]) { *pcI = c++; iRet |= REBracket(pcMem, wLen, pFR); } ++w; continue; } *pcI = c; iRet |= REBracket(pcMem, wLen, pFR); } free((void *)pcMem); return iRet; } /* Assume nothing to produce a list of RE's w/o | in them (ksb) * String with a | but no \(\) around it pass each down, w/o a | pass down. * String with ..A..\(B1 | B2 |...\)..Z.., pass A\(B1\)Z down * to ourself, then A\(B2\)Z ... for each Bn * We pick the inner-most \(\) pair with an active | in it since that * reduces recursion and memory alloaction the most. */ static int RESplit(const char *pcRE, size_t wLen, const char *pcPrefix, const char *pcSuffix, FORCE_RE *pFRCall) { register int c; register const char *pcScan, *pcNewRE; register char *pcCallLeft, *pcCallRight; register size_t w; auto size_t wLeft, wCallLeft, wRight, wCallRight; auto int iRet, iDepth; typedef struct BPnode { int foccure, funoccure; int fdisjuncts; const char *pcleft, *pcright; const char *pcbleft, *pcbright; } BACK_PARENS; auto BACK_PARENS aBP[11]; /* \1...\9 + a spare */ static const char acEmpty[] = ""; for (iDepth = 0; iDepth < sizeof(aBP)/sizeof(aBP[0]); ++iDepth) { aBP[iDepth].fdisjuncts = 0; aBP[iDepth].foccure = aBP[iDepth].funoccure = 0; aBP[iDepth].pcleft = aBP[iDepth].pcright = (const char *)0; aBP[iDepth].pcbleft = aBP[iDepth].pcbright = (const char *)0; } iDepth = 0; aBP[0].pcleft = pcRE; for (w = 0; w < wLen; ) { if ('|' == (c = pcRE[w++])) aBP[iDepth].fdisjuncts = 1; if ('\\' == c) { ++w; continue; } if ('(' == c) { if (++iDepth > 9) { fatal(acBrokenCode); } if (0 == aBP[iDepth].foccure++) { aBP[iDepth].pcleft = pcRE+w; } } if (')' == c) { if (iDepth < 1) { /* When we found an unclosed we have an * unquoted comma in the RE, so quit. */ return -1; } /* \(a\)other\(x|y|z\)... * we need to ignore \(a\) */ if (!aBP[iDepth].fdisjuncts) { --aBP[iDepth].foccure; } else if (0 == aBP[iDepth].funoccure++) { aBP[iDepth].pcright = pcRE+w-1; } --iDepth; } } aBP[0].pcright = pcRE+w; if (0 != iDepth) { return -1; } for (iDepth = 10; --iDepth > 0; ) { if (aBP[iDepth].fdisjuncts) break; } if (0 != iDepth) { /* fall out */ } else if ((char *)0 == pcPrefix && (char *)0 == pcSuffix) { register size_t wCursor; for (wCursor = w = 0; w < wLen; ++w) { if ('\\' == pcRE[w] && '|' == pcRE[w+1]) { ++w; continue; } if ('|' != pcRE[w]) continue; REBracket(pcRE+wCursor, (w-wCursor), pFRCall); wCursor = w+1; } return REBracket(pcRE+wCursor, w-wCursor, pFRCall); } else { /* The left or right side may have a \(re\) in it, or * even make one, so put them back together and try again. * nested inside a \(re\). */ register size_t wMem; register char *pcMem; if ((char *)0 == pcPrefix) pcPrefix = acEmpty; if ((char *)0 == pcSuffix) pcSuffix = acEmpty; wMem = ((strlen(pcPrefix)+wLen+strlen(pcSuffix))|7)+1; if ((char *)0 == (pcMem = malloc(wMem))) { fatal(acBadAlloc); } snprintf(pcMem, wMem, "%s%.*s%s", pcPrefix, (int)wLen, pcRE, pcSuffix); iRet = RESplit(pcMem, strlen(pcMem), (char *)0, (char *)0, pFRCall); free((void *)pcMem); return iRet; } wLeft = wCallLeft = aBP[iDepth].pcleft-pcRE; if ((char *)0 != pcPrefix) wCallLeft += strlen(pcPrefix); wRight = wCallRight = wLen - (aBP[iDepth].pcright-pcRE); if ((char *)0 != pcSuffix) wCallRight += strlen(pcSuffix); wCallLeft |= 7; wCallRight |= 7; pcCallLeft = (char *)0; if ((char *)0 == (pcCallRight = malloc(wCallRight+1)) || (char *)0 == (pcCallLeft = malloc(wCallLeft+1))) { fatal(acBadAllow); } snprintf(pcCallLeft, wCallLeft+1, "%s%.*s", (char *)0 == pcPrefix ? acEmpty : pcPrefix, (int)wLeft, pcRE); snprintf(pcCallRight, wCallRight+1, "%.*s%s", (int)wRight, aBP[iDepth].pcright, (char *)0 == pcSuffix ? acEmpty : pcSuffix); iRet = 0; for (pcNewRE = pcScan = aBP[iDepth].pcleft; pcScan < aBP[iDepth].pcright; ++pcScan) { register int iFudge; if ('\\' == (c = *pcScan)) { ++pcScan; continue; } if ('|' == c) { iFudge = 1; /* rm it in length */ } else if (pcScan == aBP[iDepth].pcright-1) { iFudge = 0; } else { continue; } ++pcScan; iRet = RESplit(pcNewRE, (pcScan-pcNewRE)-iFudge, pcCallLeft, pcCallRight, pFRCall); if (pcScan == aBP[iDepth].pcright || 0 != iRet) break; pcNewRE = pcScan; } if ((char *)0 != pcCallLeft && pcCallLeft != pcPrefix) { free(pcCallLeft); } if ((char *)0 != pcCallRight && pcCallRight != pcSuffix) { free(pcCallRight); } return iRet; } /* Carp (once) if the command has an unquoted comma in an RE (ksb) */ static int CheckREFormat(const cmd_t *cmd, const char *pcName, const char *pcValue) { register const char *pcScan; register int c; static int fSupport = 1; auto int iRGComma, iUQComma, iParens, iBracket; auto int iBackComma, iRange, iNonDigit, fRange; iRGComma = iUQComma = iParens = iBracket = 0; iBackComma = iRange = iNonDigit = fRange = 0; for (pcScan = pcValue; '\000' != (c = *pcScan++); /* nada */) { switch (c) { case ',': if (',' == *pcScan) { ++pcScan; continue; } if (0 != iParens || 0 != iBracket) ++iUQComma; continue; case '(': ++iParens; continue; case ')': --iParens; continue; case '[': ++iBracket; if (']' == *pcScan) ++pcScan; continue; case ']': --iBracket; continue; case '\\': if ('\000' == (c = *pcScan)) break; if (',' == c) ++iBackComma; ++pcScan; continue; case '{': if ('\000' == (c = *pcScan)) break; if (',' != c && !isdigit(c)) continue; /* re_format says not a range */ fRange = 1; ++iRange; do { if (',' == c) { if (',' == *pcScan) ++pcScan; else ++iRGComma; continue; } else if (/*{*/ '}' == c) { ++pcScan; --iRange; break; } if (!isdigit(c)) ++iNonDigit; } while ('\000' != (c = *pcScan++)); continue; } } if (iUQComma) fprintf(stderr, "%s: %s: %s:%d: %s: commas inside parenthesis must be spelled \",,\"\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcName); else if (0 != iRGComma) fprintf(stderr, "%s: %s: %s:%d: %s: commas inside '{...}' must be spelled \",,\"\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcName); else if (iBackComma) fprintf(stderr, "%s: %s: %s:%d: %s: quote commas with \",,\", not \"\\,\"\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcName); else if (iParens > 0) fprintf(stderr, "%s: %s: %s:%d: %s: missing ')' in RE\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcName); else if (iParens < 0) fprintf(stderr, "%s: %s: %s:%d: %s: missing '(' in RE\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcName); /*)*/ else if (0 != iRange) fprintf(stderr, "%s: %s: %s:%d: %s: unbalanced '{...}' in RE\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcName); else if (0 != iNonDigit) fprintf(stderr, "%s: %s: %s:%d: %s: range '{...}' contains non-digit limits\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcName); else if (0 != iBracket) fprintf(stderr, "%s: %s: %s:%d: %s: unbalanced '[...]' in RE\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcName); else { if (fRange && fSupport) { fprintf(stderr, "%s: %s: %s:%d: %s: no support to check REs with bounds expressions ({n,m})\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcName); fSupport = 0; } return 0; } return 1; } /* Given a list of posible options create a list of the ones that (ksb) * exist, return a cat of RE values (sep with commas for GetField). * If none in the list exist, return pcDefRE, set *ppcLabel to "none"; */ static char * Gather(const cmd_t *cmd, char const **ppcScan, char const **ppcLabel, char const **ppcList, char *pcDefRE) { register int iLabLen, iListLen, i, iReal; register const char *pcOpt, *pcValue; register char *pcLabel, *pcRet; *ppcList = (const char *)0; *ppcLabel = (const char *)0; if ((const char **)0 == ppcScan) /* no output buffer */ return (char *)0; iReal = iLabLen = iListLen = 0; for (i = 0; (char *)0 != (pcOpt = ppcScan[i++]); /* nothing */) { if ((char *)0 == (pcValue = FindOpt(cmd, pcOpt)) || '\000' == *pcValue) continue; iListLen += strlen(pcValue)+1; /* "," */ iLabLen += strlen(pcOpt)+2; /* ", " */ ++iReal; /* for adding "and" */ } if (0 == iReal) { if ((char *)0 != pcDefRE) *ppcLabel = strdup("none"); return pcDefRE; } if (iReal > 1) { iLabLen += 3; /* actually size for "and" */ } *ppcList = pcRet = calloc((iListLen|7)+1, sizeof(char)); *ppcLabel = pcLabel = calloc((iLabLen|7)+1, sizeof(char)); if ((char *)0 == pcRet || (char *)0 == pcLabel) { fatal(acBadAlloc); } *pcRet = '\000'; for (i = 0; (char *)0 != (pcOpt = ppcScan[i++]); /* nothing */) { if ((char *)0 == (pcValue = FindOpt(cmd, pcOpt)) || '\000' == *pcValue) continue; if ('\000' == *pcValue || CheckREFormat(cmd, pcOpt, pcValue)) continue; if ('\000' != pcRet[0]) strncat(pcRet, ",", iListLen+1); strncat(pcRet, pcValue, iListLen+1); --iReal; if ('\000' != pcLabel[0]) strncat(pcLabel, 0 == iReal ? " and " : ", ", iListLen+1); strncat(pcLabel, pcOpt, iLabLen+1); } return pcRet; } /* Setup for the ForceRE loop (ksb) * Now handles disjunctions like $2=^bin|sbin$,^/bin|/sbin$ by mapping * them to lists "$2=^bin$,^sbin$,^/bin$,^/sbin$". */ static int ForceInit(const cmd_t *cmd, FORCE_RE *pFR, const char *pcName, const char **ppcAllow, const char **ppcRENot, const char **ppcREAnd, const char **ppcREAndNot, const int fSwapBit) { auto char *pcField, *pcCopy; register const char *pcList; pFR->afnonlit[0] = pFR->afnonlit[1] = 0; pFR->frooted = pFR->fswapped = 0; pFR->iexcluded = pFR->inegated = pFR->ffallback = 0; pFR->wmemcur = 0; pFR->wmemmax = 0; pFR->pslist = (char *)0; pFR->pcmatching = pFR->pcforbid = (char *)0; pFR->fswapbit = fSwapBit; pFR->pckeep = pFR->pcand = pFR->pcandnot = (const char *)0; pFR->pcname = pcName; Gather(cmd, ppcAllow, &pFR->pcmatchname, &pFR->pcmatching, (char *)0); Gather(cmd, ppcRENot, &pFR->pcforbidname, &pFR->pcforbid, (char *)0); Gather(cmd, ppcREAnd, &pFR->pcandname, &pFR->pcand, (char *)0); Gather(cmd, ppcREAndNot, &pFR->pcandnotname, &pFR->pcandnot, (char *)0); /* When we return 0 there is a small memory leak from Gathers above */ if ((char *)0 != (pcList = pFR->pcmatching)) { /* OK */ } else if ((char *)0 != (pcList = pFR->pcand)) { pFR->ffallback = 1; pFR->fswapped = pFR->fswapbit; pFR->pcand = (char *)0; } else if ((char *)0 != pFR->pcforbid || (char *)0 != pFR->pcandnot) { fprintf(stderr, "%s: %s: %s:%d: %s is only negatively limited (by %s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pFR->pcname, (char *)0 != pFR->pcforbid ? pFR->pcforbidname : pFR->pcandnotname); return 0; } else if ('$' == pFR->pcname[0]) { /* $var only in cmd checks */ if (cmd != cmdVetchTrustEnv) fprintf(stderr, "%s: %s: %s:%d: completely trusts the client's value of %s as part of the command path\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pFR->pcname); cmdVetchTrustEnv = cmd; return 0; } else { fprintf(stderr, "%s: %s: %s:%d: %s is not limited at all\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pFR->pcname); return 0; } pFR->wmemmax = (strlen(pcList)|4095)+1; if ((char *)0 == (pFR->pcmem = malloc(pFR->wmemmax))) { fatal(acBadAlloc); } /* Remeber if all the discarded alternations were rooted at slash. */ pFR->frooted = 1; while ((char *)0 != (pcList = GetField(pcList, &pcField))) { pcCopy = strdup(pcField); EndField(); RESplit(pcCopy, strlen(pcCopy), (char *)0, (char *)0, pFR); free((void *)pcCopy); } EndField(); /* Try the limiting positive for matches if no positives, use the * original positive match as the and-limit. */ if (0 == pFR->wmemcur) { if ((char *)0 == (pcList = pFR->pcand)) { free((void *)pFR->pcmem); pFR->pcmem = (char *)0; return pFR->inegated || pFR->iexcluded || pFR->afnonlit[0]; } pFR->ffallback = 1; pFR->fswapped = pFR->fswapbit; pFR->pckeep = pFR->pcand; pFR->pcand = pFR->pcmatching; while ((char *)0 != (pcList = GetField(pcList, &pcField))) { pcCopy = strdup(pcField); EndField(); RESplit(pcCopy, strlen(pcCopy), (char *)0, (char *)0, pFR); free((void *)pcCopy); } EndField(); pFR->pcand = pFR->pckeep; if (0 == pFR->wmemcur) { free((void *)pFR->pcmem); pFR->pslist = pFR->pcmem = (char *)0; return pFR->inegated || pFR->iexcluded || pFR->afnonlit[0]; } } pFR->pcmem[pFR->wmemcur] = '\000'; pFR->pslist = pFR->pcmem; return 1; } /* Free any memory and return true if we saw any non-literal elems. (ksb) * We pass the cmd here for debugging, handy in gdb, it does produce * a carp from gcc -Wextra, but I can live with that. */ static int /*ARGSUSED*/ ForceEnd(const cmd_t *cmd, FORCE_RE *pFR) { if ((char *)0 != pFR->pcmatchname) free((void *)pFR->pcmatchname); if ((char *)0 != pFR->pcmatching) free((void *)pFR->pcmatching); if ((char *)0 != pFR->pcforbidname) free((void *)pFR->pcforbidname); if ((char *)0 != pFR->pcforbid) free((void *)pFR->pcforbid); if ((char *)0 != pFR->pcandname) free((void *)pFR->pcandname); if ((char *)0 != pFR->pcand) free((void *)pFR->pcand); if ((char *)0 != pFR->pcandnotname) free((void *)pFR->pcandnotname); if ((char *)0 != pFR->pcandnot) free((void *)pFR->pcandnot); if ((char *)0 != pFR->pcmem) { free((void *)pFR->pcmem); pFR->pcmem = (char *)0; } return pFR->afnonlit[0]; } /* If the RE forces a literal string return that string. (ksb) * For example this matches 3 fixed strings: * $1=^/bin/\(ls|pwd\)$,^/usr/bin/sort$ * We are called with just spec from the list at a time. */ static char * ForceRE(const cmd_t *cmd, FORCE_RE *pFR) { register int i, iBack, iSearch, fAbort; register const char *pcScan; auto const char *pcOrig; auto char *pcCarp, *pcCopy; auto char acPath[MAXPATHLEN+4]; auto const char *aapcBakRef[10][2]; auto regex_t *reg; static const cmd_t *cmdNegative = (cmd_t *)0; /* If we are at the end of the list try the pcand list * to see if we can get some more matches. We'll and with * the old list, in case it was all *'s and ?'s and +'s. * When we finish that list we end up here to put the FRnode * back in order and return control with a NULL. */ if ((char *)0 == (pcScan = pFR->pslist)) { register const char *pcList; auto char *pcField; if ((char *)0 == pFR->pcmem) { /* all excluded case */ return (char *)0; } pcList = pFR->pcand; if (pFR->ffallback) { /* done with reverse case */ pFR->pcand = pFR->pckeep; pFR->ffallback = 0; pFR->fswapped = 0; return (char *)0; } if ((char *)0 == pcList) { return (char *)0; } /* Reverse the positive matches, reset the buffer and * load with potentials for the new positive RE list. * Then recurse for that list. */ pFR->ffallback = 1; pFR->fswapped = pFR->fswapbit; pFR->wmemcur = 0; pFR->pcmem[0] = '\000'; while ((char *)0 != (pcList = GetField(pcList, &pcField))) { pcCopy = strdup(pcField); EndField(); RESplit(pcCopy, strlen(pcCopy), (char *)0, (char *)0, pFR); free((void *)pcCopy); } EndField(); pFR->pslist = pFR->pcmem; return ForceRE(cmd, pFR); } i = strlen(pcScan)+1; pFR->pslist = ('\000' == pcScan[i]) ? (char *)0 : pcScan+i; for (i = 0; i < 10; ++i) { aapcBakRef[i][1] = aapcBakRef[i][0] = (char *)0; } pcOrig = pcScan; if ('^' != *pcScan++) { pFR->afnonlit[pFR->ffallback] = 1; pFR->frooted = 0; return ForceRE(cmd, pFR); } if (0 == pFR->frooted) { /* nada */; } else if ('/' == pcScan[0] && !('*' == pcScan[1] || '?' == pcScan[1])) { /* ok */; } else if ('[' == pcScan[0] && '/' == pcScan[1] && ']' == pcScan[2] && !('*' == pcScan[3] || '?' == pcScan[3])) { /* ok */; } else { pFR->frooted = 0; } fAbort = iBack = 0; acPath[0] = '\000'; aapcBakRef[0][0] = acPath; for (i = 0; '\000' != *pcScan; ++i) { if (i > MAXPATHLEN) { /* too large */ fAbort = 1; break; } /* In a$b$d the dollars are not special to the RE engine * in the first position *, +, and ? are not special. */ if ('$' == pcScan[0] && '\000' == pcScan[1]) break; if ('*' == pcScan[0] && i > 0) break; if ('+' == pcScan[0] && i > 0) break; if ('?' == pcScan[0] && i > 0) break; if ('|' == pcScan[0]) break; if ('.' == pcScan[0]) break; acPath[i] = *pcScan; if ('(' == *pcScan) { acPath[i] = *++pcScan; aapcBakRef[++iBack][0] = &acPath[i]; } else if (')' == *pcScan) { iSearch = iBack; while (iSearch > 0 && (const char *)0 != aapcBakRef[iSearch][1]) --iSearch; if (0 == iSearch) { fprintf(stderr, "%s: RE: %s: unbalanced close of a back-reference\n", Progname, pcOrig); fAbort = 1; break; } --i; aapcBakRef[iSearch][1] = &acPath[i]; } else if ('\\' != *pcScan) { /* nada */; } else if ('\000' == *++pcScan) { /* regcomp complains about trailing backslash for us */ fAbort = 1; break; } else if (isdigit(*pcScan)) { register int iT, iLen; iT = *pcScan-'0'; if ((const char *)0 == aapcBakRef[iT][1]) { fprintf(stderr, "%s: RE: %s: unbalanced open of a back-reference\n", Progname, pcOrig); fAbort = 1; break; } iLen = aapcBakRef[iT][1]-aapcBakRef[iT][0]; if (MAXPATHLEN < i + iLen) { /* too large? */ fAbort = 1; break; } strncpy(&acPath[i], aapcBakRef[iT][0], iLen); i += iLen-1; } ++pcScan; } acPath[i] = '\000'; /* Non-constant RE, is it no anchored? */ if (fAbort) { if ('/' != acPath[0]) { pFR->frooted = 0; } return ForceRE(cmd, pFR); } /* We found a string, now look for unclosed backrefs if we made it * to the end of the string. */ iSearch = iBack; while (iSearch > 0 && (const char *)0 != aapcBakRef[iSearch][1]) --iSearch; if ((('$' == pcScan[0] && '\000' == pcScan[1]) || '\000' == *pcScan) && 0 != iSearch) { fprintf(stderr, "%s: RE: %s: unclosed back-reference\n", Progname, pcOrig); return ForceRE(cmd, pFR); } if ('$' != pcScan[0] || '\000' != pcScan[1]) { pFR->afnonlit[pFR->ffallback] = 1; return ForceRE(cmd, pFR); } /* Are we foiled by a configuration error! The positive RE forces a * literal string which the negative RE also discards. */ pcCarp = (char *)0; if ((char *)0 != pFR->pcforbid && (regex_t *)0 != (reg = ReComp(pFR->pcforbid, &pcCarp)) && ReMatch(reg, acPath)) { pFR->inegated += 1; if (cmd != cmdNegative) fprintf(stderr, "%s: %s: %s:%d: %s forces a fixed match (%s), but %s (%s) forbids that string\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pFR->pcmatchname, acPath, pFR->pcforbidname, pFR->pcforbid); cmdNegative = cmd; return ForceRE(cmd, pFR); } pcCarp = (char *)0; if ((char *)0 != pFR->pcandnot && (regex_t *)0 != (reg = ReComp(pFR->pcandnot, &pcCarp)) && ReMatch(reg, acPath)) { pFR->inegated += 1; if (cmd != cmdNegative) fprintf(stderr, "%s: %s: %s:%d: %s forces a fixed match (%s), but %s (%s) forbids that string\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pFR->pcmatchname, acPath, pFR->pcandnotname, pFR->pcandnot); cmdNegative = cmd; return ForceRE(cmd, pFR); } return strdup(acPath); } /* See if $PATH searches "." or ".." (ksb) * N.B. PATH=bin:... PATH=:/usr/local/bin... and PATH=/something::/other * all do. */ static int CheckPATHTrust(const char *pcPath) { if ((char *)0 == pcPath) { return 1; } while ('/' == *pcPath) { while ('\000' != *pcPath && ':' != *pcPath) ++pcPath; if ('\000' == *pcPath) return 1; if (':' == pcPath[0] && ':' == pcPath[1]) return 0; ++pcPath; /* skik the colon sep */ } return '\000' == *pcPath; } /* When the RE matches any string return true. N.B. ^$ is special. (ksb) * We don't handle parentheses, I'm not that crazy about the message. */ static int IsDotStar(const char *pcRE) { if ('^' == *pcRE) { ++pcRE; if ('$' == *pcRE) return 0; } while ('.' == pcRE[0] && ('*' == pcRE[1] || '?' == pcRE[1])) pcRE += 2; if ('$' == *pcRE) ++pcRE; return '\000' == *pcRE; } /* Reduce the number of arguments passed through the functions below (ksb) */ typedef struct SPgrp { int iexit; const char *pcspecF; /* who used -f */ const char *pcspecG; const char *pcspecU; const char *pcspecM; int freal; /* $1 built still might exist */ const char *pcroot; /* any know, active chroot */ const char *pcdir; /* the computed dir */ char *pcexecok; /* we did stat(2) a cmd here */ char *pcexecmiss; /* we failed stat(2) a cmd here */ int fwabsroot; /* warned const nonasb root */ int fwabsdir; /* warned const nonasb chdir */ int ierrno; /* ... the errno from that stat */ NEEDS *pNE; /* for $* in FinishExec */ char acpwd[MAXPATHLEN*2+256]; char accmd[MAXPATHLEN*2+256]; } SPEC; static const char acPos_Cmd[] = "%_", acPos_CmdP[] = "%_.path", acNeg_Cmd[] = "!_", acNeg_CmdP[] = "!_.path", acSlash[] = "/"; static const char *apcPosF[] = { acPos_CmdP, acPos_Cmd, "%f.path", "%f", (char *)0 }, *apcNegF[] = { acNeg_CmdP, acNeg_Cmd, "!f.path", "!%f", (char *)0 }, *apcPosG[] = { acPos_CmdP, acPos_Cmd, "%g", (char *)0}, *apcNegG[] = { acNeg_CmdP, acNeg_Cmd, "!g", (char *)0}, *apcPosP[] = { acPos_CmdP, acPos_Cmd, acDevNull, "$*", (char *)0}, *apcNegP[] = { acNeg_CmdP, acNeg_Cmd, acDevNull, "!*", (char *)0}, *apcPosU[] = { acPos_CmdP, acPos_Cmd, "%u", (char *)0}, *apcNegU[] = { acNeg_CmdP, acNeg_Cmd, "!u", (char *)0}, *apcPosE[] = { acPos_CmdP, acPos_Cmd, (char *)0, (char *)0}, *apcNegE[] = { acNeg_CmdP, acNeg_Cmd, (char *)0, (char *)0}, #define SKIP_WHOLE_PATS 2 /* skip the %_ and !_ limiters above */ *apcPosD[] = { "%d.path", "%d", (char *)0}, *apcNegD[] = { "!d.path", "!d", (char *)0}; static char acFakeDir[16], /* sential for run-time value */ acFakeRoot[16]; /* We passed the chroot and dir checks, now look at the program path (ksb) * Pathed can become absolute or relative, but the others lock us in. * We go to a lot of trouble to parse the whole strings (maybe a few * times) to check for unsafe combinations. */ static void FinishExec(const cmd_t *cmd, SPEC *pSP, const char *pcScan, char *pcBuild) { register char *pcEnd; register int c; auto int iDShorts, iSkip; auto char *pcFreeNeg, *pcFreePos; auto const char *pcExcludes = (char *)0; auto FORCE_RE FR; auto struct stat stExec; auto char acToExec[MAXPATHLEN*4+512]; static const char acMe[] = "command"; /* Each cmd complains only once for each case */ static const cmd_t *cmdUsesF = (cmd_t *)0, *cmdUsesG = (cmd_t *)0, *cmdUsesU = (cmd_t *)0, *cmdUsesQ = (cmd_t *)0, *cmdUses_ = (cmd_t *)0, *cmdVetchLitWhite = (cmd_t *)0, *cmdVetchDollarStar = (cmd_t *)0, *cmdVetchSingle = (cmd_t *)0, *cmdVetchFake = (cmd_t *)0, *cmdVetchPath = (cmd_t *)0, *cmdVetchRel = (cmd_t *)0, *cmdVetchAbs = (cmd_t *)0, *cmdVetchRej = (cmd_t *)0, *cmdMissedDir = (cmd_t *)0; pcFreeNeg = pcFreePos = (char *)0; /* fix a small memory leak */ if (0 == strcmp(acMagicShell, pcScan)) pcScan = "$S"; while ('\000' != (c = (*pcBuild = *pcScan++))) { if ('/' == c) { /* but, for looks, do not double them */ if (pcBuild == pSP->accmd || '/' != pcBuild[-1]) ++pcBuild; continue; } if ('$' != c) { ++pcBuild; continue; } switch (*pcScan) { case '\000': /* "...$" */ fprintf(stderr, "%s: %s: %s:%d: trailing dollar sign\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); continue; case '$': /* $$ -> $ */ ++pcBuild, ++pcScan; continue; case '|': /* $| -> empty */ ++pcScan; continue; case '.': /* split word */ *pcBuild = '\000'; pcScan += strlen(pcScan); continue; case '\\': switch (c = *++pcScan) { case 'a': *pcBuild++ = '\007'; break; case 'b': *pcBuild++ = '\b'; break; case 'd': *pcBuild++ = '"'; break; case 'f': *pcBuild++ = '\f'; break; case 'n': *pcBuild++ = '\n'; break; case 'o': *pcBuild++ = '`'; break; case 'q': *pcBuild++ = '\''; break; case 'r': *pcBuild++ = '\r'; break; case 's': *pcBuild++ = ' '; break; case 't': *pcBuild++ = '\t'; break; case 'v': *pcBuild++ = '\v'; break; case '\000': /* trailing backslash */ --pcScan; *pcBuild = '\\'; break; default: *pcBuild++ = c; break; } if (isspace(pcBuild[-1]) && cmd != cmdVetchLitWhite) { fprintf(stderr, "%s: %s: %s:%d: forced white-space in the command path?\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); cmdVetchLitWhite = cmd; } ++pcScan; continue; case 'x': strcpy(pcBuild, ((char *)0 == pSP->pcdir) ? acCurDir : pSP->pcdir); pcBuild += strlen(pcBuild); continue; case 'X': strcpy(pcBuild, ((char *)0 == pSP->pcroot) ? acSlash : pSP->pcroot); pcBuild += strlen(pcBuild); continue; case 's': /* { script } is trusting acShell, basically */ case 'S': /* this is even more explicit */ { auto char *pcRecur, *pcOpt; auto int wLen; ++pcScan; switch (CheckTrust(cmd, acShell)) { case 0: /* not allowed so we get our default */ strcpy(pcBuild, acDefShell); break; case 1: fprintf(stderr, "%s: %s: %s:%d: trusting %s from the user's environment\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acShell); pSP->iexit = EX_DATAERR; strcpy(pcBuild, acShell); pSP->freal = 0; break; case 2: /* the value is in our config, check it */ if ((char *)0 == (pcOpt = FindOpt(cmd, acShell))) fatal("Internal error: check trust failure"); wLen = (strlen(pcScan)+strlen(pcOpt))|3; if ((char *)0 == (pcRecur = malloc(++wLen))) fatal(acBadAlloc); snprintf(pcRecur, wLen, "%s%s", pcOpt, pcScan); FinishExec(cmd, pSP, pcRecur, pcBuild); free((void *)pcRecur); return; } pcBuild += strlen(pcBuild); } continue; case 'f': case 'd': pcBuild[0] = '$'; pcBuild[1] = *pcScan; pcBuild[2] = '\000'; ++pcScan; if ((char *)0 != pSP->pcspecF) { if (cmd != cmdUsesF) fprintf(stderr, "%s: %s: %s:%d: -f used for both command and %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pSP->pcspecF); pSP->iexit = EX_DATAERR; cmdUsesF = cmd; } /* $f/$d may occure twice in a command, we don't bump it */ pSP->pcspecF = acMe; iDShorts = 'd' == pcScan[-1]; iSkip = (pSP->accmd == pcBuild) ? 0 : SKIP_WHOLE_PATS; if (ForceInit(cmd, &FR, "file as a command", apcPosF+iSkip, apcNegF+iSkip, apcPosD, apcNegD, 1)) { pcExcludes = "%d or !d path limits"; break; } *pcBuild++ = '$'; *pcBuild++ = pcScan[-1]; pSP->freal = 0; continue; case 'g': /* you are kidding, right? */ ++pcScan; if ((char *)0 == pSP->pcspecG) { pSP->pcspecG = acMe; } else if (acMe != pSP->pcspecG) { if (cmd != cmdUsesG) fprintf(stderr, "%s: %s: %s:%d: -g used for both %s and %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acMe, pSP->pcspecG); pSP->iexit = EX_DATAERR; cmdUsesG = cmd; } iSkip = (pSP->accmd == pcBuild) ? 0 : SKIP_WHOLE_PATS; if (ForceInit(cmd, &FR, "group as a command", apcPosG+iSkip, apcNegG+iSkip, (const char **)0, (const char **)0, 0)) { pcExcludes = (char *)0; iDShorts = 0; break; } strcpy(pcBuild, "$g"); pcBuild += strlen(pcBuild); pSP->freal = 0; continue; case 'u': ++pcScan; if ((char *)0 == pSP->pcspecU) { pSP->pcspecU = acMe; } else if (acMe != pSP->pcspecU) { if (cmd != cmdUsesU) fprintf(stderr, "%s: %s: %s:%d: -u used by both %s and %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acMe, pSP->pcspecU); pSP->iexit = EX_DATAERR; cmdUsesU = cmd; } iSkip = (pSP->accmd == pcBuild) ? 0 : SKIP_WHOLE_PATS; if (ForceInit(cmd, &FR, "login as a command", apcPosU+iSkip, apcNegU+iSkip, (const char **)0, (const char **)0, 0)) { pcExcludes = (char *)0; iDShorts = 0; break; } strcpy(pcBuild, "$u"); pcBuild += strlen(pcBuild); pSP->freal = 0; continue; case '{': /*} a variable from the client environment */ { static char *pcDup = (char *)0; auto const char *pcMark; auto size_t wLen; if ((char *)0 == (pcEnd = strchr(pcScan, /*{*/'}'))) { fprintf(stderr, "%s: %s: %s:%d: unbalanced curly reference\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); pSP->iexit = EX_CONFIG; return; } if ((char *)0 != pcDup) /* free last version */ free((void *)pcDup); /* When we can't find a value use ${NAME} below */ pcMark = ++pcScan; pcScan = pcEnd+1; wLen = ((pcEnd-pcMark)|7)+9; snprintf(pcBuild, wLen, "$%.*s", (int)(pcEnd-pcMark), pcMark); /* Let Dup be !{NAME}, then %{NAME} to fetch RE * limits, then $NAME for environment set. */ if ((char *)0 == (pcDup = malloc(wLen))) fatal(acBadAlloc); snprintf(pcDup, wLen, "!{%.*s}", (int)(pcEnd-pcMark), pcMark); apcNegE[SKIP_WHOLE_PATS] = pcFreeNeg = strdup(pcDup); snprintf(pcDup, wLen, "%%{%.*s}", (int)(pcEnd-pcMark), pcMark); apcPosE[SKIP_WHOLE_PATS] = pcFreePos = strdup(pcDup); snprintf(pcDup, wLen, "$%.*s", (int)(pcEnd-pcMark), pcMark); iSkip = (pSP->accmd == pcBuild) ? 0 : SKIP_WHOLE_PATS; if (ForceInit(cmd, &FR, pcDup, apcPosE+iSkip, apcNegE+iSkip, (const char **)0, (const char **)0, 0)) { pcExcludes = (char *)0; iDShorts = 0; break; } /* Didn't used them, so dispose of them */ free((void *)pcFreeNeg); free((void *)pcFreePos); apcNegE[SKIP_WHOLE_PATS] = pcFreeNeg = (char *)0; apcPosE[SKIP_WHOLE_PATS] = pcFreePos = (char *)0; pSP->freal = 0; if (cmd != cmdVetchTrustEnv) fprintf(stderr, "%s: %s: %s:%d: completely trusts the client's value of %s as part of the command path\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcBuild); cmdVetchTrustEnv = cmd; pcBuild += strlen(pcBuild); continue; } { register long lCheck; auto int iFound; auto char *pcEol, *pcCount; auto char acMust[OP_MAX_LDWIDTH]; /* $%ld */ auto char acCannot[OP_MAX_LDWIDTH]; /* !%ld */ case '0': /* nmemonic name, or misleading `octal' */ case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': lCheck = strtol(pcScan, &pcEol, 0); pcScan = pcEol; goto param; case ',': /* makes the command "$2 $3 $4..." */ case '+': /* makes the command "argv[1] argv[2]..." */ case '*': /* makes the command "$1 $2 $3..." */ lCheck = ',' == *pcScan ? 1 : '+' == *pcScan ? 0 : ((NEEDS *)0 == pSP->pNE ? 0 : pSP->pNE->lCmdParam)+('*' == *pcScan ? 1 : 2 /*&*/); if ((char *)0 == (pcCount = FindOpt(cmd, acOptCount)) || (lCheck == (iFound = atoi(pcCount))) || lCheck == iFound+1) { /* $* could be the empty string */ if ((char *)0 != pcCount && lCheck == iFound+1) { ++pcScan; continue; } if (cmd == cmdVetchDollarStar) /* nada */; else if ((char *)0 == pcCount) fprintf(stderr, "%s: %s: %s:%d: $%c may have embedded spaces, since an unknown number of parameters are allowed, replace with $%c\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, *pcScan, '*' == *pcScan ? '@' : '-'); else fprintf(stderr, "%s: %s: %s:%d: $%c may have embedded spaces, since more than %ld %s allowed, replace with $%c\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, *pcScan, lCheck, 1 == lCheck ? "parameter is" : "parameters are", '*' == *pcScan ? '@' : '-'); cmdVetchDollarStar = cmd; return; } ++pcScan; goto param; case '@': /* splits words so treat as $1$. */ case '-': /* makes the command argv[0] */ lCheck = '-' == *pcScan ? 0 : ((NEEDS *)0 == pSP->pNE ? 0 : pSP->pNE->lCmdParam)+('@' == *pcScan ? 1 : 2/*!*/); /* We can't always we correct here, 2 params or 1? * And I'm not going to try both (even though I could). */ if ((char *)0 == (pcCount = FindOpt(cmd, acOptCount)) || (lCheck == (iFound = atoi(pcCount))) || lCheck == iFound+1) ++pcScan; /* assume abutted */ else pcScan += strlen(pcScan); /*FALLINTO*/ param: /* We know $0, is the name of the command */ if (0 == lCheck) { strcpy(pcBuild, cmd->pcname); pcBuild += strlen(pcBuild); continue; } snprintf(acMust, sizeof(acMust), "$%ld", lCheck); snprintf(acCannot, sizeof(acCannot), "!%ld", lCheck); strcpy(pcBuild, acMust); apcPosP[SKIP_WHOLE_PATS] = acMust; apcNegP[SKIP_WHOLE_PATS] = acCannot; iSkip = (pSP->accmd == pcBuild) ? 0 : SKIP_WHOLE_PATS; if (ForceInit(cmd, &FR, acMust, apcPosP+iSkip, apcNegP+iSkip, (const char **)0, (const char **)0, 0)) { pcExcludes = "$* or !* value limits"; iDShorts = 0; break; } pcBuild += strlen(pcBuild); pSP->freal = 0; } continue; case '#': /* $# known count, viz. $f.$# */ { register long lCheck; auto char *pcEol, *pcCount; if ((char *)0 != (pcCount = FindOpt(cmd, acOptCount))) { lCheck = strtol(pcCount, &pcEol, 0); snprintf(pcBuild, 32, "%ld", lCheck); } else { strcpy(pcBuild, acOptCount); pSP->freal = 0; } pcBuild += strlen(pcBuild); ++pcScan; } continue; case '_': /* you are not funny */ if (cmdUses_ != cmd) fprintf(stderr, "%s: %s: %s:%d: $%c may not be used as part of a command path\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, *pcScan); cmdUses_ = cmd; return; case 'q': /* these could allow symlink races */ case 'Q': *pcBuild++ = '$'; *pcBuild++ = *pcScan; *pcBuild = '\000'; if (cmdUsesQ != cmd) fprintf(stderr, "%s: %s: %s:%d: $%c should not be trusted as part of a command path\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, *pcScan); cmdUsesQ = cmd; /*FALLTHROUGH*/ default: /* We could do these, not worth it: * $b/$B/$c/C/$e/$E/$h/$H/$i/$I/$o/$O/ * $p/$P/$t/$T/$v/$v/$w/$W/$~ * Sometimes these (not always, and lots of code): * $K - code to find uid (match %u?), get shell * $k - match users= and get shell(s) * We can't ever do: * $a/$A/$h/$H/$m/$M/$l/$L/$r/$R */ ++pcBuild; /* accept the $ */ *pcBuild++ = *pcScan++; pSP->freal = 0; continue; } /* We just inited an RE loop, make it go. * When this RE is the first RE and it is always * anchored at slash we have a special case below. * N.B. c == 'd' when we are looking at %d rather than %f * iDShorts is non-zero when we are looking at %f * and really want %d. */ { auto int iCnt, fKeep; auto const char *pcTemp, *pcForced; fKeep = pSP->freal; iCnt = 0; pcForced = acDevNull; /* hush lint */ while ((char *)0 != (pcTemp = ForceRE(cmd, &FR))) { register char *pcSnip; /* Remove spliced double-slash so we look nice, * if looking at %d trim last path. */ if (0 == iDShorts) /* nothing */; else if ((char *)0 != (pcSnip = strrchr(pcTemp, '/'))) *pcSnip = '\000'; else pcTemp = ""; /* use "./" for DEBUG */ if (pcBuild > pSP->accmd && '/' == pcBuild[-1] && '/' == pcTemp[0]) ++pcTemp; (void)strcpy(pcBuild, pcTemp); FinishExec(cmd, pSP, pcScan, pcBuild+strlen(pcBuild)); ++iCnt; pcForced = pcTemp; } if (ForceEnd(cmd, &FR)) { register char *pcSwap; if (pcBuild == pSP->accmd && FR.frooted) { if ((char *)0 == (pcSwap = strdup(pcBuild))) fatal(acBadAlloc); *pcBuild = '/'; strcpy(pcBuild+1, (char *)0 == pcSwap ? "$_" : pcSwap); if ((char *)0 != pcSwap) free((void *)pcSwap); } pSP->freal = 0; FinishExec(cmd, pSP, pcScan, pcBuild+strlen(pcBuild)); } else if (1 == iCnt && cmd != cmdVetchSingle && !iDShorts) { register int iHack; /* We don't get here for ${FIRST} in the case where * ${FIRST}/${SECOND} is presented and ${SECOND} is * also 1-limited because of the cmd trap. Check. */ for (iHack = 1; '$' != pcScan[-iHack]; ++iHack) ; fprintf(stderr, "%s: %s: %s:%d: %.*s is limited to single value (%s); why make the Customer specify it?\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iHack, pcScan-iHack, pcForced); cmdVetchSingle = cmd; } else if (0 != FR.iexcluded && (char *)0 != pcExcludes) { fprintf(stderr, "%s: %s: %s:%d: all matches excluded by %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcExcludes); } else if (0 != FR.inegated) { fprintf(stderr, "%s: %s: %s:%d: all matches excluded by negative matches\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); } /* Reset $d flag and free memory we may have lost the * pointer to in a recursive call, like ${TOP}/${CMD}, */ iDShorts = 0; if ((char *)0 != pcFreeNeg) free((void *)pcFreeNeg); if ((char *)0 != pcFreePos) free((void *)pcFreePos); apcNegE[SKIP_WHOLE_PATS] = (char *)0; apcPosE[SKIP_WHOLE_PATS] = (char *)0; apcNegP[SKIP_WHOLE_PATS] = (char *)0; apcPosP[SKIP_WHOLE_PATS] = (char *)0; /* We (or recursion) may have reset this, put it back */ pSP->freal = fKeep; return; } } if (!pSP->freal && '$' == pSP->accmd[0]) { if (cmd != cmdVetchFake) fprintf(stderr, "%s: %s: %s:%d: cannot verify program path (at least for %s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pSP->accmd); cmdVetchFake = cmd; return; } if ('\000' == pSP->accmd[0]) { fprintf(stderr, "%s: %s: %s:%d: null command?\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); pSP->iexit = EX_CONFIG; return; } /* dir and chroot may be either: set, runtime (RT), not set (NULL), * or any (*). The command may be relative to . (.), absolute (/) * or path-searched (p). That gives us 27 the combinations: * {set, RT, NULL} * {set, RT, NULL} * {p, /, .}: * pthd chroot dir disposition * p * * carp when any $PATH elements are relative to . * / RT * carp that chroot is controlled by client * / * * check file * . set|NULL set check file * . set|NULL RT|NULL carp * . RT * carp */ { register char *pcPath, *pcOpt; register int cPathed; /* Strange shell rule, if it doesn't start with a slash, but * there is a slash in it, then it must be relative. */ pcPath = pSP->accmd; cPathed = pcPath[0]; if ('/' != cPathed && (char *)0 != strchr(pcPath, '/')) { cPathed = '.'; } switch (cPathed) { default: /* case pathed (p) */ /* Why would you escalate an echo? Well, a forced * stdout might make that worth the effort. (ksb) * No shell I know path-searches for "echo", so. */ if (0 == strcmp(acEcho, pSP->accmd)) { /* not really searching $PATH */ } else if (cmd == cmdVetchPath) { /* don't carp again about it */ } else if ((char *)0 == (pcOpt = FindOpt(cmd, acOptPATH))) { fprintf(stderr, "%s: %s: %s:%d: trusts the client's %s (at least for %s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptPATH, pSP->accmd); cmdVetchPath = cmd; } else if (!CheckPATHTrust(pcOpt)) { fprintf(stderr, "%s: %s: %s:%d: trusts a relative %s (at least for %s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptPATH, pSP->accmd); cmdVetchPath = cmd; } return; case '.': /* relative */ /* We already carped about being relative to an unspec'd * dir, so just return. If it is shell built-in we were * wrong to vetch, I think. */ if (acFakeRoot == pSP->pcroot || acFakeDir == pSP->pcdir) { if (cmd == cmdVetchRel) /* nada */; else if (acFakeDir == pSP->pcdir && '~' == acFakeDir[0]) fprintf(stderr, "%s: %s: %s:%d: trusts an unknown logins home directory (at least for %s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pSP->accmd); else fprintf(stderr, "%s: %s: %s:%d: trusts a relative file in an unknown directory (at least for %s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pSP->accmd); cmdVetchRel = cmd; return; } strcpy(acToExec, pSP->acpwd); if ((char *)0 == FullPath(acToExec, pSP->accmd)) { return; } return; case '/': if (acFakeRoot == pSP->pcroot) { if (cmd != cmdVetchAbs) fprintf(stderr, "%s: %s: %s:%d: trusts an absolute path against a run-time chroot (at least for %s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcPath); cmdVetchAbs = cmd; return; } if (!pSP->freal) break; if ((char *)0 != pSP->pcroot) { strcpy(acToExec, pSP->pcroot); pcPath = FullPath(acToExec, pSP->accmd); } else { pcPath = strcpy(acToExec, pcPath); } if ((char *)0 == pcPath) { fatal("Internal error: full path failed"); } break; } } /* We may not be real, be we can be sure any directory prefix * any exists (e.g. /usr/local/sbin/foo.$1), even if $1 has a * slash in it, the base still should still exist. We think it is * Poor Form to use "/opt/local/$u/bin/$n", but we'll check for * "/opt/local" none-the-less. */ { register char *pcCut = (char *)0; register const char *pcRE; if (!pSP->freal) { if ((char *)0 == (pcCut = strchr(acToExec, '$'))) return; do { *pcCut-- = '\000'; } while (pcCut > acToExec && '/' != *pcCut); if (pcCut == acToExec) return; } if (cmd != cmdVetchRej && (const char *)0 != (pcRE = FindOpt(cmd, acPos_CmdP)) && !GenMatch(acPos_CmdP, pSP->accmd, pcRE)) { fprintf(stderr, "%s: %s: %s:%d: command %s rejected by %s non-match\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pSP->accmd, acPos_CmdP); cmdVetchRej = cmd; } if (cmd != cmdVetchRej && (const char *)0 != (pcRE = FindOpt(cmd, acPos_Cmd)) && !GenMatch(acPos_Cmd, pSP->accmd, pcRE)) { fprintf(stderr, "%s: %s: %s:%d: command %s rejected by %s non-match\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pSP->accmd, acPos_Cmd); cmdVetchRej = cmd; } if (cmd != cmdVetchRej && (const char *)0 != (pcRE = FindOpt(cmd, acNeg_CmdP)) && GenMatch(acNeg_CmdP, pSP->accmd, pcRE)) { fprintf(stderr, "%s: %s: %s:%d: command %s rejected by %s match\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pSP->accmd, acNeg_CmdP); cmdVetchRej = cmd; } if (cmd != cmdVetchRej && (const char *)0 != (pcRE = FindOpt(cmd, acNeg_Cmd)) && GenMatch(acNeg_Cmd, pSP->accmd, pcRE)) { fprintf(stderr, "%s: %s: %s:%d: command %s rejected by %s match\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pSP->accmd, acNeg_Cmd); cmdVetchRej = cmd; } if (cmd == cmdVetchRej) { return; } /* Make sure we can stat the command (or directory) we're * going to run (consult) * XXX checking all the %_. and !_. attrs is diminishing returns */ if (0 != stat(pSP->accmd, & stExec)) { if ((char *)0 == pSP->pcexecmiss) { pSP->pcexecmiss = strdup(pSP->accmd); pSP->ierrno = errno; } return; } if ((char *)0 == pSP->pcexecok) { pSP->pcexecok = strdup(pSP->accmd); } if ((char *)0 != pcCut) { if (S_ISDIR(stExec.st_mode)) return; if (cmd != cmdMissedDir) fprintf(stderr, "%s: %s: %s:%d: %s: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acToExec, strerror(ENOTDIR)); cmdMissedDir = cmd; return; } } if (pSP->freal) { CheckProg(cmd, acToExec, &pSP->iexit, &stExec); } } /* Find the program(s) we are going to run, if possible from chroot and (ksb) * the dir spec. Either root or dir can be acFake.... for an unknown path * specified at runtime, NULL for none set. */ static void CheckPossible(const cmd_t *cmd, SPEC *pSP) { register int iLen; register const char *pcDir; auto const char *pcScan; pcDir = pSP->pcdir; #if defined(OP_EXTRA_VERBOSE) /* Documented in the man page, don't read it to me. */ if ((char *)0 != pSP->pcroot && (char *)0 == pcDir) { static int fOnce = 0; if (!fOnce) fprintf(stderr, "%s: %s: %s:%d: %s: without a dir specification implies dir=/\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptChroot); fOnce = 1; } #endif /* Look for the directory we'll be in for the command. */ pSP->acpwd[0] = '\000'; if (acFakeRoot == pSP->pcroot && (char *)0 != pcDir && '/' == pcDir[0]) { iLen = strlen(acFakeRoot); while (iLen > 0 && '/' == acFakeRoot[iLen-1]) --iLen; snprintf(pSP->acpwd, sizeof(pSP->acpwd), "%.*s%s", iLen, acFakeRoot, pcDir); } else if ((char *)0 == pSP->pcroot && (char *)0 == pcDir) { /* we don't know anything, we don't cd or chroot */ } else if (((char *)0 == pSP->pcroot || '/' == *pSP->pcroot) && ((char *)0 == pcDir || '/' == *pcDir)) { strncpy(pSP->acpwd, (char *)0 == pSP->pcroot ? acSlash : pSP->pcroot, sizeof(pSP->acpwd)); iLen = strlen(pSP->acpwd); while (iLen > 0 && '/' == pSP->acpwd[iLen-1]) --iLen; if ((char *)0 != pcDir) strncpy(pSP->acpwd+iLen, pcDir, sizeof(pSP->acpwd)-iLen); } pcScan = cmd->args[0]; if ('.' == pcScan[0] && ('/' == pcScan[1] || ('.' == pcScan[1] && '/' == pcScan[2]))) { if (acFakeDir == pSP->pcdir && '~' == acFakeDir[0]) fprintf(stderr, "%s: %s: %s:%d: trusts an unknown logins home directory (at least for %s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pSP->accmd); else if ('\000' == pSP->acpwd[0]) fprintf(stderr, "%s: %s: %s:%d: %s: explicitly forces a relative path to an unspecified directory\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcScan); } /* For the next steps we might have to recurses -- get it started. */ pSP->accmd[0] = '\000'; FinishExec(cmd, pSP, pcScan, pSP->accmd); } /* Looks like the dir is a constant string (ksb) */ static void ConstDir(const cmd_t *cmd, SPEC *pSP) { register const char *pcDir; auto struct stat stDir; pcDir = pSP->pcdir; if ('/' != *pcDir) { if (!pSP->fwabsdir) { fprintf(stderr, "%s: %s: %s:%d: %s: should be an absolute path (%s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptDir, pcDir); pSP->fwabsdir = 1; } } else if (-1 == stat(pcDir, &stDir)) { /* -1 handles root longer than MAXPATHLEN too */ fprintf(stderr, "%s: %s: %s:%d: %s: stat: %s: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptDir, pcDir, strerror(errno)); pSP->iexit = EX_NOINPUT; } else if (!S_ISDIR(stDir.st_mode)) { fprintf(stderr, "%s: %s: %s:%d: %s: not a directory %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptDir, pcDir); pSP->iexit = EX_NOINPUT; } CheckPossible(cmd, pSP); } /* If %f.mode or %f.perms forces the -f specification to be a (ksb) * directory ('d') or nonexistant file ('n') return true. The logic here * is too naive because we can provide a list and/or disjunction REs to * match. For example %f of "^[df]" and !f of "^[f]" would actually force * the right mode. Don't config that. --ksb */ static int Forced_F(const cmd_t *cmd, const int cType) { register const char *pc; if (((char *)0 != (pc = FindOpt(cmd, "%f.mode")) && '^' == pc[0] && cType == pc[1]) || ((char *)0 != (pc = FindOpt(cmd, "!f.mode")) && '^' == pc[0] && '[' == pc[1] && '^' == pc[2] && cType == pc[3] && ']' == pc[4])) return 1; if (((char *)0 != (pc = FindOpt(cmd, "%f.perms")) && '^' == pc[0] && cType == pc[1]) || ((char *)0 != (pc = FindOpt(cmd, "!f.perms")) && '^' == pc[0] && '[' == pc[1] && '^' == pc[2] && cType == pc[3] && ']' == pc[4])) return 1; return 0; } /* Same checks as above for %d/-d (ksb) */ static int Forced_D(const cmd_t *cmd, const int cType) { register const char *pc; if (((char *)0 != (pc = FindOpt(cmd, "%d.mode")) && '^' == pc[0] && cType == pc[1]) || ((char *)0 != (pc = FindOpt(cmd, "!d.mode")) && '^' == pc[0] && '[' == pc[1] && '^' == pc[2] && cType == pc[3] && ']' == pc[4])) return 1; if (((char *)0 != (pc = FindOpt(cmd, "%d.perms")) && '^' == pc[0] && cType == pc[1]) || ((char *)0 != (pc = FindOpt(cmd, "!d.perms")) && '^' == pc[0] && '[' == pc[1] && '^' == pc[2] && cType == pc[3] && ']' == pc[4])) return 1; return 0; } /* We allow dir=%u starting in 2.104, because the home directory of (ksb) * the target login is very useful. */ static void CheckUserDir(const cmd_t *cmd, SPEC *pSP, FORCE_RE *pFR) { register int iCnt, iBad; register struct passwd *pw; register const char *pcNewLogin, *pcLastForced; auto int fKeep; auto char acHome[MAXPATHLEN+4]; fKeep = pSP->freal; iBad = iCnt = 0; pcLastForced = acDevNull; /* hush lint */ while ((char *)0 != (pcNewLogin = ForceRE(cmd, pFR))) { pcLastForced = pcNewLogin; ++iCnt; if ((struct passwd *)0 == (pw = getpwnam(pcNewLogin))) { ++iBad; continue; } snprintf(acHome, sizeof(acHome), "%s", pw->pw_dir); pSP->freal = fKeep; pSP->pcdir = acHome; ConstDir(cmd, pSP); } if (ForceEnd(cmd, pFR)) { pSP->freal = 0; pSP->pcdir = acFakeDir; CheckPossible(cmd, pSP); } else if (1 == iCnt) { fprintf(stderr, "%s: %s: %s:%d: %s: forces an explicit -u option? (%s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptDir, pcLastForced); } else if (0 != pFR->inegated) { fprintf(stderr, "%s: %s: %s:%d: %s: patterns suggest an explicit -u option, but that login is excluded\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptDir); } else if (iBad == iCnt && 0 != iCnt) { fprintf(stderr, "%s: %s: %s:%d: %s: none of the %d possible logins presently exist\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptDir, iBad); } pSP->freal = fKeep; } /* We passed the chroot checks, now look at the directory spec (ksb) * look for fixed paths too. We can be called multiple times (for * alternate %f/%d expansion). */ static void CheckDir(const cmd_t *cmd, SPEC *pSP) { auto const char *pcChDir; auto FORCE_RE FR; auto int fKeep; static const cmd_t *cmdUsesF = (cmd_t *)0, *cmdHome = (cmd_t *)0; /* The code for chroot forces a cd to / even if chroot is empty */ fKeep = pSP->freal; if ((char *)0 == (pcChDir = FindOpt(cmd, acOptDir))) { pSP->pcdir = ((char *)0 != FindOpt(cmd, acOptChroot)) ? acSlash : (char *)0; CheckPossible(cmd, pSP); return; } if ('\000' == *pcChDir) { pcChDir = acSlash; } /* %u -> ~$u */ snprintf(acFakeDir, sizeof(acFakeDir), "$%c", pcChDir[1]); if (0 == strcmp(acSpecU, pcChDir)) { /* A pseudo-user grouping a password and dir is OK, * and doesn't conflict with anything but chroot. */ if (acOptChroot == pSP->pcspecU) { fprintf(stderr, "%s: %s: %s:%d: -u used for both %s and %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptDir, pSP->pcspecU); } else if ((char *)0 == pSP->pcspecU) { pSP->pcspecU = acOptDir; } snprintf(acFakeDir, sizeof(acFakeDir), "~$%c", pcChDir[1]); if (ForceInit(cmd, &FR, "login as a directory", apcPosU+SKIP_WHOLE_PATS, apcNegU+SKIP_WHOLE_PATS, (const char **)0, (const char **)0, 0)) { CheckUserDir(cmd, pSP, &FR); } else if (cmdHome != cmd) { cmdHome = cmd; fprintf(stderr, "%s: %s: %s:%d: %s: %s may not match any logins at runtime\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptDir, acFakeDir); pSP->freal = 0; pSP->pcdir = acFakeDir; CheckPossible(cmd, pSP); } pSP->freal = fKeep; return; } if (0 == strcmp(acSpecMe, pcChDir)) { snprintf(acFakeDir, sizeof(acFakeDir), "~$%c", pcChDir[1]); pSP->freal = 0; pSP->pcdir = acFakeDir; CheckPossible(cmd, pSP); pSP->freal = fKeep; return; } if (0 != strcmp(acSpecF, pcChDir) && 0 != strcmp(acSpecD, pcChDir)) { pSP->pcdir = pcChDir; ConstDir(cmd, pSP); return; } if ((char *)0 != pSP->pcspecF) { if (cmd != cmdUsesF) fprintf(stderr, "%s: %s: %s:%d: -f used by both dir and %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pSP->pcspecF); cmdUsesF = cmd; pSP->iexit = EX_SOFTWARE; /* we could pile-on here: set pcspecF = "%s and dir" */ } else { pSP->pcspecF = acOptDir; } if ('d' == pcChDir[1]) { /* Else %d, which is always a directory, unless %f is forced * to be a nonexistent file. So see if %d is forced to exist. */ if (Forced_D(cmd, 'd')) { /* OK */ } else if (Forced_D(cmd, 'n')) { fprintf(stderr, "%s: %s: %s:%d: %s: forced to be a nonexistent directory (expanded from dir=%s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptDir, pcChDir); } else if (Forced_F(cmd, 'n')) { fprintf(stderr, "%s: %s: %s:%d: %s: nonexistent files might be in a nonexistent directory (expanded from dir=%s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptDir, pcChDir); } } else { /* LLL could be forced to be a link to a directory */ if (!Forced_F(cmd, 'd')) { fprintf(stderr, "%s: %s: %s:%d: %s: is not forced to be a directory (expanded from dir=%s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptDir, pcChDir); } } /* If %f.path doesn't forces a litteral directory clain runtime. * When %f.path forces a single literal dir why make them * type it? */ if (ForceInit(cmd, &FR, "file as a directory", apcPosF+SKIP_WHOLE_PATS, apcNegF+SKIP_WHOLE_PATS, apcPosD, apcNegD, 1)) { register int iCnt; register char *pcChop; register const char *pcNewPwd, *pcLastForced; iCnt = 0; while ((char *)0 != (pcNewPwd = ForceRE(cmd, &FR))) { pcLastForced = pcNewPwd; if (0 != strcmp(acSpecD, pcChDir)) { /* OK take the whole */ } else if ((char *)0 == (pcChop = strrchr(pcNewPwd, '/'))) { fprintf(stderr, "%s: %s: %s:%d: %s: explicit value forces dir=. (%s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptDir, pcNewPwd); pcNewPwd = acCurDir; } else { while (pcChop > pcNewPwd && '/' == *pcChop) *pcChop-- = '\000'; if (pcChop == pcNewPwd) pcChop[1] = '\000'; } pSP->freal = fKeep; pSP->pcdir = pcNewPwd; ConstDir(cmd, pSP); ++iCnt; } if (ForceEnd(cmd, &FR)) { /* We don't know the value, but it must start from root */ if (FR.frooted) { snprintf(acFakeDir, sizeof(acFakeDir), "/$%c", pcChDir[1]); } pSP->freal = 0; pSP->pcdir = acFakeDir; CheckPossible(cmd, pSP); } else if (1 == iCnt) { fprintf(stderr, "%s: %s: %s:%d: %s: forces an explicit -f option? (%s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptDir, pcLastForced); } else if (0 != FR.iexcluded || 0 != FR.inegated) { fprintf(stderr, "%s: %s: %s:%d: %s: patterns suggest an explicit -f option, but that path is excluded\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptDir); } pSP->freal = fKeep; return; } pSP->freal = 0; pSP->pcdir = acFakeDir; CheckPossible(cmd, pSP); pSP->freal = fKeep; } /* Looks like the chroot is a constant string (ksb) * We could go crazy here looking to see if the new slash looked like * a valid root directory. I think I'll leave that for someone else. :-] */ static void ConstRoot(const cmd_t *cmd, SPEC *pSP) { auto struct stat stDir; register const char *pcChRoot; pcChRoot = pSP->pcroot; if ('/' != *pcChRoot) { if (!pSP->fwabsroot) { fprintf(stderr, "%s: %s: %s:%d: %s: should be an absolute path (%s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptChroot, pcChRoot); pSP->fwabsroot = 1; } } else if (-1 == stat(pcChRoot, &stDir)) { /* -1 handles root longer than MAXPATHLEN too */ fprintf(stderr, "%s: %s: %s:%d: %s: stat: %s: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptChroot, pcChRoot, strerror(errno)); pSP->iexit = EX_NOINPUT; } else if (!S_ISDIR(stDir.st_mode)) { fprintf(stderr, "%s: %s: %s:%d: %s: not a directory %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptChroot, pcChRoot); pSP->iexit = EX_NOINPUT; } /* Yes, slash should have a /etc, and /bin, and /libexec on most * modern systems. If a jacket is going to fix it we can't tell * here, so I guess we won't complain. The jacket could mount * a new filesystem under us before we start the process. */ CheckDir(cmd, pSP); } /* We allow chroot=%u starting in 2.107, because the home directory of (ksb) * the target login is very useful as a variable root. */ static void CheckUserRoot(const cmd_t *cmd, SPEC *pSP, FORCE_RE *pFR) { register int iCnt, iBad; register struct passwd *pw; register const char *pcNewLogin, *pcLastForced; auto int fKeep; auto char acHome[MAXPATHLEN+4]; fKeep = pSP->freal; iBad = iCnt = 0; pcLastForced = acDevNull; /* hush lint */ while ((char *)0 != (pcNewLogin = ForceRE(cmd, pFR))) { pcLastForced = pcNewLogin; ++iCnt; if ((struct passwd *)0 == (pw = getpwnam(pcNewLogin))) { ++iBad; continue; } snprintf(acHome, sizeof(acHome), "%s", pw->pw_dir); pSP->freal = fKeep; pSP->pcroot = acHome; ConstRoot(cmd, pSP); } if (ForceEnd(cmd, pFR)) { pSP->freal = 0; pSP->pcroot = acFakeRoot; ConstRoot(cmd, pSP); } else if (1 == iCnt) { fprintf(stderr, "%s: %s: %s:%d: %s: forces an explicit -u option? (%s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptChroot, pcLastForced); } else if (0 != pFR->inegated) { fprintf(stderr, "%s: %s: %s:%d: %s: patterns suggest an explicit -u option, but that login is excluded\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptChroot); } else if (iBad == iCnt && 0 != iCnt) { fprintf(stderr, "%s: %s: %s:%d: %s: none of the %d possible logins presently exist\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptChroot, iBad); } pSP->freal = fKeep; } /* Pass in the cmd (viz. root, dir, exec, and search path) we check (ksb) * possible the target chroot directories. Note (char *)0 is none * N.B. chroot and dir can be %f/%d or literal text only. * If 2 specifications use %f/%d we might vetch about that. * we really don't want a relative path (we can't check any below) * world +w, or in a world +w dir (group +w we don't check) * not available to the user running the request * exist and not be +x to anyone * We set the proposed exit code, but never "fail" (the program might * legitimately not be installed yet, for example). */ static void CheckRoot(const cmd_t *cmd, SPEC *pSP) { auto const char *pcChRoot; auto FORCE_RE FR; auto int fKeep; static const cmd_t *cmdHome = (cmd_t *)0; fKeep = pSP->freal; if ((char *)0 == (pcChRoot = FindOpt(cmd, acOptChroot)) || '\000' == *pcChRoot) { pSP->pcroot = (char *)0; CheckDir(cmd, pSP); return; } pSP->pcroot = pcChRoot; if (0 == strcmp(pcChRoot, acSlash)) { if ((char *)0 == FindOpt(cmd, acOptDir)) fprintf(stderr, "%s: %s: %s:%d: using chroot=/ to force a dir=/ is poor form\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); else fprintf(stderr, "%s: %s: %s:%d: chroot=/ is redundant\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); /* When we want to turn off a DEFAULT chroot use "chroot=", * which is trapped above. */ CheckDir(cmd, pSP); } if (0 == strcmp(acSpecU, pcChRoot)) { /* A pseudo-user home specification for a chroot is clever. * and doesn't conflict with any pSP->pcspecU. */ if ((char *)0 == pSP->pcspecU) { pSP->pcspecU = acOptChroot; } snprintf(acFakeRoot, sizeof(acFakeRoot), "~$%c", pcChRoot[1]); if (ForceInit(cmd, &FR, "login for chroot", apcPosU+SKIP_WHOLE_PATS, apcNegU+SKIP_WHOLE_PATS, (const char **)0, (const char **)0, 0)) { CheckUserRoot(cmd, pSP, &FR); } else if (cmdHome != cmd) { cmdHome = cmd; fprintf(stderr, "%s: %s: %s:%d: %s: %s may not match any logins at runtime\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptChroot, acFakeDir); pSP->freal = 0; pSP->pcroot = acFakeDir; CheckDir(cmd, pSP); } pSP->freal = fKeep; return; } if (0 == strcmp(acSpecMe, pcChRoot)) { snprintf(acFakeDir, sizeof(acFakeRoot), "~$%c", pcChRoot[1]); pSP->freal = 0; pSP->pcroot = acFakeRoot; CheckDir(cmd, pSP); pSP->freal = fKeep; return; } if (0 != strcmp(acSpecF, pcChRoot) && 0 != strcmp(acSpecD, pcChRoot)) { ConstRoot(cmd, pSP); return; } snprintf(acFakeRoot, sizeof(acFakeRoot), "$%c", pcChRoot[1]); /* This can't happen here in the current flow, defensive coding */ if ((char *)0 != pSP->pcspecF) { fprintf(stderr, "%s: %s: %s:%d: -f used by both chroot and %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pSP->pcspecF); pSP->iexit = EX_SOFTWARE; } else { pSP->pcspecF = acOptChroot; } /* If we are looking for the dirname check some perms forced */ if ('d' == pcChRoot[1]) { if (Forced_D(cmd, 'd')) { /* OK */ } else if (Forced_D(cmd, 'n')) { fprintf(stderr, "%s: %s: %s:%d: %s: forced to be a nonexistent directory (expanded from chroot=%s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptChroot, pcChRoot); } else if (Forced_F(cmd, 'n')) { fprintf(stderr, "%s: %s: %s:%d: %s: nonexistent files might be in a nonexistent directory (from chroot=%s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptChroot, pcChRoot); } } else if (!Forced_F(cmd, 'd')) { fprintf(stderr, "%s: %s: %s:%d: %s: is not forced to be a directory (from chroot=%s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptChroot, pcChRoot); } /* If %f.path forces a litteral directory we shall check it. * But if that's the case why make them type the -f? * For %d this is 1 componentn too long, we'll chomp it later */ if (ForceInit(cmd, &FR, "file as a chroot", apcPosF+SKIP_WHOLE_PATS, apcNegF+SKIP_WHOLE_PATS, apcPosD, apcNegD, 1)) { register int iCnt; register char *pcChop; register const char *pcNewSlash, *pcLastForced; iCnt = 0; while ((char *)0 != (pcNewSlash = ForceRE(cmd, &FR))) { pcLastForced = pcNewSlash; if (0 != strcmp(acSpecD, pcChRoot)) { /* OK take the whole */ } else if ((char *)0 == (pcChop = strrchr(pcNewSlash, '/'))) { fprintf(stderr, "%s: %s: %s:%d: %s: explicit value forces chroot=. (%s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptChroot, pcNewSlash); pcNewSlash = acCurDir; } else { while (pcChop > pcNewSlash && '/' == *pcChop) *pcChop-- = '\000'; if (pcChop == pcNewSlash) pcChop[1] = '\000'; } pSP->freal = fKeep; pSP->pcroot = pcNewSlash; ConstRoot(cmd, pSP); ++iCnt; } /* Also matches a run-time string */ if (ForceEnd(cmd, &FR)) { /* We don't know the value, but it must hang from slash */ pSP->freal = 0; if (FR.frooted) { snprintf(acFakeRoot, sizeof(acFakeRoot), "/$%c", pcChRoot[1]); } pSP->pcroot = acFakeRoot; CheckDir(cmd, pSP); } else if (1 == iCnt) { fprintf(stderr, "%s: %s: %s:%d: %s: forces an explicit -f option? (%s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptChroot, pcLastForced); } else if (0 != FR.iexcluded || 0 != FR.inegated) { fprintf(stderr, "%s: %s: %s:%d: %s: patterns suggest an explicit -f option, but that path is excluded\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptChroot); } pSP->freal = fKeep; return; } pSP->pcroot = acFakeRoot; CheckDir(cmd, pSP); } /* Find any RE in the list that doesn't compile. (ksb) */ static char * CheckCompile(const char *pcList, char **ppcCarp) { register regex_t *reg; register char *pcRet; auto char *pcField; if ((char *)0 == pcList || '\000' == *pcList) { return (char *)0; } pcRet = (char *)0; while ((char *)0 != (pcList = GetField(pcList, &pcField))) { if ((regex_t *)0 != (reg = ReComp(pcField, ppcCarp))) { regfree(reg); continue; } pcRet = strdup(pcField); break; } EndField(); return pcRet; } /* Convert the RE into a usage strings (which must be shorter than the (ksb) * original RE or the word "number"). The expression must be anchored. * An expression is easy to convert to a usage: * "^start$" => "start", * "^(start|stop|end)$" or * "^start$|^stop$|^end$" => "start|stop|end" * "(^start$)|(^stop)$" => start|stop * nested parens stop us because we don't expand them: * "^bl(y|ue|t|x)$" => default since "bly|blue|blt|blx" is longer * For more complex things return the default. We scribble over the RE * to prodce a suggested usage message. * * ForceRE is the wrong tool to use here, it is too strict. */ static const char * IsAnchored(char *pcRE, const char *pcDef) { register size_t i, iOut, iLen; register int fParens, iDots, iSlash; register const char *pcCheck; fParens = 0; if ('(' == pcRE[0] && '^' == pcRE[1]) /*)*/ { fParens = 1; pcRE += 2; } else if ('^' != pcRE[0]) { return pcDef; } else if ('(' == pcRE[1]) /*)*/ { fParens = 2; pcRE += 2; } else { ++pcRE; } if (0 == (iLen = strlen(pcRE))) { return pcDef; } switch (fParens) { case 1: /* "(^ .... $)" */ if (/*(*/ ')' != pcRE[--iLen]) return pcDef; /*FALLTHROUGH*/ case 0: /* ^foo$ */ if ('$' != pcRE[--iLen]) return pcDef; break; case 2: /* "^( ... )$ */ if ('$' != pcRE[--iLen]) return pcDef; if (/*(*/ ')' != pcRE[--iLen]) return pcDef; break; } /* A pattern that matches all digits is a "number" * digit | '[' digit ']' | '[' digit '-' digit ']' | '*' | '+' * so we accept '**1' as a number: don't give us a bad RE, we also * can match an IP address (a number with 3 dots in it) or a CIDR. */ iOut = 1; iSlash = iDots = 0; for (pcCheck = pcRE, i = 0; i < iLen; ) { pcCheck = pcRE+i; if (isdigit(*pcCheck)) i += 1; else if ('/' == *pcCheck) iOut = 0, i += 1, iSlash += 1; else if ('\\' == pcCheck[0] && '/' == pcCheck[1]) iOut = 0, i += 2, iSlash += 1; else if ('+' == *pcCheck || '*' == *pcCheck) iOut = 0, i += 1; else if ('\\' == pcCheck[0] && '.' == pcCheck[1]) iOut = 0, iDots += 1, i += 2; else if ('[' != pcCheck[0] || ']' == pcCheck[1]) break; else { /* [0-9] | [.0-9] | [0-9.] | [234] */ register int iSawDot = 0; do { ++i; if ('-' == pcRE[i] || isdigit(pcRE[i])) /* nada */; else if ('/' == pcRE[i]) iSlash += 1; else if ('.' == pcRE[i]) iSawDot = 1; else break; } while (']' != pcRE[i]); if (']' != pcRE[i]) break; if (iSawDot) { iDots = '*' == pcRE[i+1] || '+' == pcRE[i+1] ? 3 : iDots+1; } ++i, iOut = 0; } } if (i == iLen) { pcRE[iLen] = '\000'; return iOut ? pcRE : strcpy(pcRE, 3 == iDots ? 1 == iSlash ? "CIDR" : "IP" : "number"); } iOut = 0; for (i = 0; i < iLen; /* below */) { switch ((pcRE[iOut++] = pcRE[i++])) { case '(': case ')': /* assume balanced if -S ran, allow it */ continue; case '$': switch (fParens) { case 0: case 1: if ('|' != pcRE[i] || '^' != pcRE[i+1]) return pcDef; i += 2; pcRE[iOut-1] = '|'; break; case 2: /* a literal dollar */ default: break; } continue; case '|': /* is ok if in parens, type 2 */ /* ^foo|bar$ -S will pitch about missing anchors * should be ^foo$|^bar$, but allow it: carping at * the Customer won't help anyone. */ continue; case '*': case '+': case '?': case '.': case '[': case ']': case '{': case '}': return pcDef; case '\\': if (i == iLen) /* missparse the $, its a \$ */ return pcDef; if (isdigit(pcRE[i])) return pcDef; pcRE[iOut-1] = pcRE[i]; continue; case ',': /* outside of {a,b} is ok */ default: continue; } } pcRE[iOut] = '\000'; return pcRE; } /* Compile the REs in the list return 1 if any match a string in data. (ksb) * Vetch about things we don't like, and for non-zero exits on errors. */ static int CheckMatch(const cmd_t *cmd, const char *pcWhich, char **ppcData, const char *pcList, int *piRet) { register int i, iMatch; register int fSawAnchor, fAllAnchors; register regex_t *reg; register const char *pcCheck; auto char *pcField, *pcCarp; static const cmd_t *cmdCarp = (cmd_t *)0; iMatch = 0; fSawAnchor = 0; fAllAnchors = 1; pcCarp = (char *)0; while ((char *)0 != (pcList = GetField(pcList, &pcField))) { if ('\000' == *pcField) { if (cmd != cmdCarp) fprintf(stderr, "%s: %s: %s:%d: %s: empty RE\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcWhich); *piRet = EX_DATAERR; cmdCarp = cmd; continue; } /* A # pattern matches a numeric form of a login/group, for * a netgroup it doesn't, but we're not perfect and a hash * can't really ever be in a netgroup name as it is a comment. */ if ('#' == *pcField) { ++pcField; } if ((regex_t *)0 == (reg = ReComp(pcField, &pcCarp))) { if (cmd != cmdCarp) fprintf(stderr, "%s: %s: %s:%d: %s: regcomp: %s: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcWhich, pcField, pcCarp); *piRet = EX_DATAERR; cmdCarp = cmd; continue; } /* If the patern is a .* we do match the whole string, * which is what we mean by "anchored" here --ksb */ pcCheck = IsDotStar(pcField) ? acDevNull : IsAnchored(pcField, (char *)0); fAllAnchors &= (const char *)0 != pcCheck; fSawAnchor |= (const char *)0 != pcCheck; for (i = 0; (char *)0 != ppcData[i]; ++i) { if (ReMatch(reg, ppcData[i])) { iMatch = 1; break; } } regfree(reg); } EndField(); if (cmd == cmdCarp) { /* do not clutter the output */ } else if (!fSawAnchor) { fprintf(stderr, "%s: %s: %s:%d: %s: no anchors in any REs%s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcWhich, acSure); /* might be worth a non-zero exit */ } else if (!fAllAnchors) { fprintf(stderr, "%s: %s: %s:%d: %s: some REs unanchored%s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcWhich, acSure); /* not be worth a non-zero exit */ } return iMatch; } /* See if this program measures up to our standards. (ksb) */ static void CheckProg(const cmd_t *cmd, const char *pcProg, int *piRet, const struct stat *pstProg) { auto struct stat stProg; if (acDefault == cmd->pcname) { return; } if ('/' != pcProg[0]) { fprintf(stderr, "%s: %s: %s:%d: %s: should be an absolute path\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcProg); *piRet = EX_PROTOCOL; } if ((struct stat *)0 == pstProg) { if (-1 == stat(pcProg, &stProg)) { fprintf(stderr, "%s: %s: %s:%d: stat: %s: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcProg, strerror(errno)); *piRet = EX_NOINPUT; return; } pstProg = &stProg; } /* LLL When the binary is setuid (0 != (pstProg->st_mode&S_ISUID)) * (or setgid with S_ISGID) we might complain if we are setting a * uid/gid too, unless it is /bin/su or /bin/login (to defeat the * PAM password checks); we should suggest MAGIC_SHELL in that case. * I don't want to recode access(2) here to check to see if the * proposed euid/egid can run the binary, but if access(2) took the * uid and gid as parameters I would use it. */ if (0 == (pstProg->st_mode&0111)) { fprintf(stderr, "%s: %s: %s:%d: %s: not executable\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcProg); *piRet = EX_DATAERR; } else if (0 != (pstProg->st_mode&0002)) { fprintf(stderr, "%s: %s: %s:%d: %s: world writable!\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcProg); *piRet = EX_NOPERM; } } /* Check a stdin, stdout, stderr redir for sanity. (ksb) * See PrivOpen for specs (below). */ static int CheckRedir(const cmd_t *cmd, const char *pcRedirExpected, const char *pcAttr) { register char *pcSpec; auto struct stat stCheck; if ((char *)0 == (pcSpec = FindOpt(cmd, pcAttr))) { return 1; } if ('<' == *pcSpec || '>' == *pcSpec) { if (*pcSpec != *pcRedirExpected) { fprintf(stderr, "%s: %s: %s:%d: %s: I/O direction is not standard (%c presented, %c expected)%s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcAttr, *pcSpec, *pcRedirExpected, acSure); /* not worth a non-zero exit code */ } if ('>' == *++pcSpec) ++pcSpec; } /* We fail in %d as well as %f, we'll not actually process %d (yet) */ if (0 == strcmp(acSpecF, pcSpec) || 0 == strcmp(acSpecD, pcSpec)) { return 1; } if ('/' == *pcSpec) { if (-1 == stat(pcSpec, &stCheck)) { fprintf(stderr, "%s: %s: %s:%d: %s: stat: %s: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcAttr, pcSpec, strerror(errno)); return 0; } /* don't check for type, etc */ return 1; } if ((char *)0 == FindOpt(cmd, acOptDir) && (char *)0 == FindOpt(cmd, acOptChroot)) { fprintf(stderr, "%s: %s: %s:%d: %s is relative to $PWD\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcAttr); /* not worth a non-zero exit code */ } return 0; } /* Verify that each configuration option in this cmd is unique (ksb) */ static void CheckUniq(const cmd_t *cmd, int *piRet) { register int i, j; register size_t wLen; register const char *pcEq, *pcI, *pcJ; for (i = 0; i < cmd->nopts; ++i) { pcI = cmd->opts[i]; if ((const char *)0 == (pcEq = strchr(pcI, '='))) wLen = strlen(pcI); else wLen = pcEq - pcI; for (j = i+1; j < cmd->nopts; ++j) { pcJ = cmd->opts[j]; if (0 != strncmp(pcI, pcJ, wLen)) continue; if ('\000' != pcJ[wLen] && '=' != pcJ[wLen]) continue; fprintf(stderr, "%s: %s: %s:%d: %.*s given more than once\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, (int)wLen, pcI); *piRet = EX_CONFIG; return; } } } /* Netgroups, pam application names and such are not specified as (ksb) * regular expressions, but some people try that. Check them at the door. * N.B. A single dot (acMyself) doesn't look like an RE to this function. */ static int LooksLikeRE(const char *pcThis) { return (char *)0 != strchr(pcThis, '^') || (char *)0 != strchr(pcThis, '$') || (char *)0 != strchr(pcThis, '|') || (char *)0 != strchr(pcThis, '['/*]*/) || (char *)0 != strchr(pcThis, '('/*)*/); } /* $PATH is set as a option, check the entries (ksb) * We could do more here if we kept track of a bit for "is escalated" in * the caller (uid, euid, gid, egid, initgroups or the like is set to * someone other than %l or .), and chroot status. Actually that same * set could be used in FinishExec, in both cases to check for a value that * might not work in the chroot, or is allowed to be relative because there * is no escalattion of identity (maybe just "nice" value or "stdout"). * When we only run built-in command (e.g. echo) we shouldn't vetch at all. */ static void CheckPath(const cmd_t *cmd, const char *pcExp) { register int c; register const char *pcScan, *pcQ; auto const char *pcRoot; auto struct stat stComp; auto char acComp[MAXPATHLEN+4]; static const cmd_t *cmdVetchQuote = (cmd_t *)0; if ('\000' == *pcExp) { fprintf(stderr, "%s: %s: %s:%d: %s= sets the path to the empty string: remove the \"=\" markup to copy the client value, or make it \"=$|\" to force an explicitly empty value\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptPATH); return; } pcRoot = FindOpt(cmd, acOptChroot); pcScan = pcExp; do { c = *pcScan; if (':' != c && '\000' != c) { ++pcScan; continue; } snprintf(acComp, sizeof(acComp), "%.*s", (int)(pcScan-pcExp), pcExp); if ('\000' == acComp[0]) strncpy(acComp, acCurDir, sizeof(acComp)); pcExp = pcScan; if ('\000' != c) ++pcScan, ++pcExp; if ((char *)0 == (pcQ = strchr(acComp, '"')) && (char *)0 == (pcQ = strchr(pcExp, '\''))) { /* OK no quotes */ } else if (cmd == cmdVetchQuote) { /* already complained */ continue; } else { fprintf(stderr, "%s: %s: %s:%d: %s literal quote (%c) in a path entry?\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptPATH, *pcQ); cmdVetchQuote = cmd; continue; } /* Don't check inside chroot, or expanded paths */ if ((char *)0 != strchr(acComp, '$') || (char *)0 != pcRoot) { continue; } if ('/' != acComp[0]) { fprintf(stderr, "%s: %s: %s:%d: %s explicitly set a relative entry (%s)?\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptPATH, acComp); continue; } if (-1 == stat(acComp, &stComp)) { fprintf(stderr, "%s: %s: %s:%d: %s %s: stat: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptPATH, acComp, strerror(errno)); continue; } if (0 != (stComp.st_mode & 0002)) { fprintf(stderr, "%s: %s: %s:%d: %s entry %s is world writable\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptPATH, acComp); } } while ('\000' != c); } /* Given an RE that is supposed to match a perms string, see if it may (ksb) * This does loop for 45,035 itterations, but that takes almost no time. * If the RE is a list and any single 1 doesn't match anything we ignore it. */ static int CheckPermRE(const char *pcList, char **ppcCarp) { static const char acFirst[]= "-dlbcnpsDPw"; register int i; register const char *pcFirst; register regex_t *pRE; auto char *pcRE; auto char acCheck[16]; /* "drwxrwxrwx" */ *ppcCarp = (char *)0; while ((char *)0 != (pcList = GetField(pcList, &pcRE))) { if ((regex_t *)0 == (pRE = ReComp(pcRE, ppcCarp))) { EndField(); return -1; /* should never happen, caller checked */ } acCheck[10] = '\000'; for (pcFirst = acFirst; '\000' != (acCheck[0] = *pcFirst++); ) { for (i = 0; i < 010000; ++i) { acCheck[1] = i & 0400 ? 'r' : '-'; acCheck[2] = i & 0200 ? 'w' : '-'; acCheck[3] = i & 0100 ? (i & S_ISUID) ? 's' : 'x' : (i & S_ISUID) ? 'S' : '-'; acCheck[4] = i & 0040 ? 'r' : '-'; acCheck[5] = i & 0020 ? 'w' : '-'; acCheck[6] = i & 0010 ? (i & S_ISGID) ? 's' : 'x' : (i & S_ISGID) ? 'S' : '-'; acCheck[7] = i & 0004 ? 'r' : '-'; acCheck[8] = i & 0002 ? 'w' : '-'; acCheck[9] = i & 0001 ? (i & S_ISVTX) ? 't' : 'x' : (i & S_ISVTX) ? 'T' : '-'; if (ReMatch(pRE, acCheck)) { EndField(); return 1; } } } } EndField(); return 0; } /* same thing for mode RE, does it match and octal 0 to 7777 (ksb) */ static int CheckModeRE(const char *pcList, char **ppcCarp) { register int i; register regex_t *pRE; auto char *pcRE; auto char acCheck[16]; /* "0000" to "7777" */ *ppcCarp = (char *)0; while ((char *)0 != (pcList = GetField(pcList, &pcRE))) { if ((regex_t *)0 == (pRE = ReComp(pcRE, ppcCarp))) { EndField(); return -1; /* should never happen, caller checked */ } for (i = 0; i < 010000; ++i) { snprintf(acCheck, sizeof(acCheck), "%04lo", (long)i); if (ReMatch(pRE, acCheck)) { EndField(); return 1; } } } EndField(); return 0; } /* See if -f use abused and overloaded, this is not always right (ksb) * but it does find some bugs */ static void DupCheck(const cmd_t *cmd, const char *pcOpt, const char **ppcCur, const char *pcUse, const char *pcOK) { auto char *pcField; if ((const char *)0 == *ppcCur) { *ppcCur = pcUse; return; } while ((const char *)0 != (pcOK = GetField(pcOK, &pcField))) { if (0 == strcmp(pcField, *ppcCur)) return; } EndField(); fprintf(stderr, "%s: %s: %s:%d: %s used for both %s and %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcOpt, pcUse, *ppcCur); } /* Sanity check our rule database. (ksb/petef) * We've read the default configuration files, what would break * if we added the others? */ static void SanityCheck(int argc, char **argv) { register int iIndex; register char *pcUser, *pcGroup, **ppcGroups, **ppcUsers; auto int iRet; auto NEEDS Need; iRet = EX_OK; if (0 != argc) { register int uOrig, uPriv, fdRead; uOrig = getuid(); uPriv = geteuid(); if (setreuid(uPriv, uOrig) < 0) { fatal("setreuid: %ld,%ld: %s", (long)uPriv, (long)uOrig, strerror(errno)); } for (iIndex = 0; iIndex < argc; ++iIndex) { if (-1 == (fdRead = open(argv[iIndex], O_RDONLY, 0400))) { fatal("open: %s: %s", argv[iIndex], strerror(errno)); } OneFile(fdRead, argv[iIndex]); } /* If we put the euid back then anyone can stat(2) any file on * our filesystem for existance. Radiates too much info. --ksb */ } /* yuck: we have to get a cache of all possible users + groups to * make sure that users= and groups= matches something */ { register struct passwd *ppassThis; register struct group *pgroupThis; register int iMax; setgrent(); iMax = 512; iIndex = 0; if ((char **)0 == (ppcGroups = (char **)calloc(iMax, sizeof(char *)))) fatal(acBadAlloc); while ((struct group *)0 != (pgroupThis = getgrent())) { if ((char *)0 == (ppcGroups[iIndex++] = strdup(pgroupThis->gr_name))) fatal(acBadAlloc); if (iMax == iIndex) { iMax *= 2; ppcGroups = (char **)realloc(ppcGroups, iMax*sizeof(char *)); if ((char **)0 == ppcGroups) fatal(acBadAlloc); } } ppcGroups[iIndex] = (char *)0; endgrent(); setpwent(); iMax = 512; iIndex = 0; if ((char **)0 == (ppcUsers = (char **)calloc(iMax, sizeof(char *)))) fatal(acBadAlloc); while ((struct passwd *)0 != (ppassThis = getpwent())) { if ((char *)0 == (ppcUsers[iIndex++] = strdup(ppassThis->pw_name))) fatal(acBadAlloc); if (iMax == iIndex) { iMax *= 2; ppcUsers = (char **)realloc(ppcUsers, iMax*sizeof(char *)); if ((char **)0 == ppcUsers) fatal(acBadAlloc); } } endpwent(); } /* Sanity check complied options */ if ('/' != acAccess[0]) { fprintf(stderr, "%s: configuration path is not absolute (%s)\n", Progname, acAccess); iRet = EX_CONFIG; } #if USE_PAM { static const char acPam[] = OP_PAM_IN; static const char acPamConf[] = OP_PAM_FILE; register FILE *fpPam; auto struct stat stCheck; auto char acPamPolicy[MAXPATHLEN+4]; snprintf(acPamPolicy, sizeof(acPamPolicy), "%s/%s", acPam, acDefPam); if ((FILE *)0 != (fpPam = fopen(acPamConf, "r"))) { register char *pcFetch; auto size_t wLen; while ((char *)0 != (pcFetch = fgetln(fpPam, & wLen))) { if ('#' == *pcFetch || isspace(*pcFetch)) continue; if (0 == strncmp(acDefPam, pcFetch, sizeof(acDefPam)-2) && isspace(pcFetch[sizeof(acDefPam)-1])) break; } fclose(fpPam); if ((char *)0 == pcFetch) { fprintf(stderr, "%s: %s: doesn't list %s\n", Progname, acPamConf, acDefPam); } } else if (-1 == stat(acPam, &stCheck) || S_IFDIR != (stCheck.st_mode & S_IFMT)) { fprintf(stderr, "%s: PAM configuration directory: stat: %s: %s\n", Progname, acPam, strerror(errno)); iRet = EX_CONFIG; } else if (-1 == stat(acPamPolicy, &stCheck)) { fprintf(stderr, "%s: PAM policy: stat: %s: %s \n", Progname, acPamPolicy, strerror(errno)); iRet = EX_CONFIG; } } #endif /* Sanity check each command */ { register cmd_t *cmd, *cmdScan; /* only so we can set the carp flag */ register const cmd_t *cmdDup; register char *pcCur; register const char *pcRead, *pcDup; register long lCur, lNot, lDup; auto char *pcEnd, *pcField, *pcCarp; auto const char *pcExplInitGrp; auto SPEC SP; auto int iMatch; cmd = (cmd_t *)0; for (cmdScan = NewCmd((const char *)0); (cmd_t *)0 != cmdScan; cmdScan = cmdScan->pODnext) { /* a DEFAULT cannot have $#, $1, $2... because we check * them before we Build the command */ if (acDefault == cmdScan->pcname) { if (0 == iIndex && 0 == strcmp(cmdScan->pcfile, pcAccess)) { /* always OK */ } else if ((cmd_t *)0 == cmdScan->pODnext || acDefault == cmdScan->pODnext->pcname) { fprintf(stderr, "%s: %s: %s:%d non-empty, but applies to no rules\n", Progname, acDefault, cmdScan->pcfile, cmdScan->iline); /* not worth a non zero exit code */ } for (iIndex = 0; iIndex < cmdScan->nopts; ++iIndex) { pcRead = cmdScan->opts[iIndex]; if ('$' != pcRead[0] && '!' != pcRead[0]) { continue; } if ('#' == pcRead[1]) { fprintf(stderr, "%s: %s: %s:%d: mnemonic matching option %c# misplaced?\n", Progname, acDefault, cmdScan->pcfile, cmdScan->iline, pcRead[0]); iRet = EX_CONFIG; continue; } if (!isdigit(pcRead[1])) { continue; } fprintf(stderr, "%s: %s: %s:%d: mnemonic matching option %c", Progname, cmdScan->pcname, cmdScan->pcfile, cmdScan->iline, pcRead[0]); while (isdigit(*++pcRead)) { fputc(*pcRead, stderr); } fprintf(stderr, " misplaced?\n"); iRet = EX_CONFIG; } } /* Make sure all options are unique, we don't process * duplicate options well. */ CheckUniq(cmdScan, & iRet); /* We have to check all merged DEFAULT options */ cmd = Build(cmdScan); /* See if the -f, -g, -u options are asked for and checked. * Also look for the program we provision, if possible. */ SP.pcspecF = SP.pcspecG = SP.pcspecU = SP.pcspecM = (char *)0; SP.freal = 1; SP.iexit = EX_OK; SP.ierrno = 0; SP.fwabsroot = SP.fwabsdir = 0; SP.pcroot = SP.pcexecok = SP.pcexecmiss = (char *)0; SP.pNE = (NEEDS *)0; (void)memset(SP.acpwd, '\000', sizeof(SP.acpwd)); (void)memset(SP.accmd, '\000', sizeof(SP.accmd)); /* If there is no args[0] then it's a syntax error, only * happens at the end-of-file when missing all args */ if (0 == cmd->nargs && acDefault != cmd->pcname) { static int fTold = 0; if (! fTold) { fprintf(stderr, "%s: %s: %s:%d: configuration file syntax error (%s)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acDefault == cmd->pcname ? "the default option must be first" : "file truncated?"); fTold = 1; } iRet = EX_CONFIG; continue; } if (0 != cmdScan->nopts) { /* ok */ } else if (acDefault == cmdScan->pcname) { /* This is the documented way to remove the global * DEFAULT in access.cf, so not an error. --ksb */ } else { register int iLook; for (iLook = 0; iLook < cmdScan->nargs; ++iLook) { if ((char *)0 != strchr(cmdScan->args[iLook], '=')) break; } fprintf(stderr, "%s: %s: %s:%d: a missing semicolon may have consumed all options", Progname, cmdScan->pcname, cmdScan->pcfile, cmdScan->iline); if (0 != iLook && iLook < cmdScan->nargs) { fprintf(stderr, " (maybe after \"%s\")", cmdScan->args[iLook-1]); } fprintf(stderr, "\n"); } lNot = lCur = -2; if ((char *)0 != (pcCur = FindOpt(cmd, "!#"))) { lNot = strtol(pcCur, &pcEnd, 0); if ((char *)0 != pcEnd && '\000' != *pcEnd) { fprintf(stderr, "%s: %s: %s:%d: cannot parse count in \"!#=%s\"\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcCur); lNot = -1; } else if (0 >= lNot) { fprintf(stderr, "%s: %s: %s:%d: !# value %ld won't reject any mnemonic rules\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, lNot); } } if ((char *)0 != (pcCur = FindOpt(cmd, acOptCount))) { lCur = strtol(pcCur, &pcEnd, 0); if ((char *)0 != pcEnd && '\000' != *pcEnd) { fprintf(stderr, "%s: %s: %s:%d: cannot parse count in \"$#=%s\"\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcCur); lCur = -1; } else if (0 > lCur) { fprintf(stderr, "%s: %s: %s:%d: $# value %ld won't match any mnemonic rules\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, lCur); } } if (lNot == lCur && -2 != lCur) { fprintf(stderr, "%s: %s: %s:%d: $# and !# have the same value (%ld) which won't match any mnemonic rules\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, lCur); iRet = EX_DATAERR; } /* We don't look at lNot here and we could, but the logic * is really tricky (maybe later LLL) --ksb * Start loop with the original's next pointer, not the copy's. */ for (cmdDup = cmdScan->pODnext; (cmd_t *)0 != cmdDup && acDefault != cmd->pcname; cmdDup = cmdDup->pODnext) { if (0 != strcmp(cmd->pcname, cmdDup->pcname)) continue; if (!cmd->fcarp && 0 != strcmp(cmd->pcfile, cmdDup->pcfile)) { fprintf(stderr, "%s: %s should only be defined in a single configuration file (not both %s and %s)\n", Progname, cmd->pcname, cmd->pcfile, cmdDup->pcfile); cmd->fcarp = 1; } pcDup = FindOpt(cmdDup, acOptCount); if ((char *)0 == pcCur && (char *)0 == pcDup) { if (SamePats(cmd, cmdDup)) fprintf(stderr, "%s: %s: duplicated in %s:%d and %s:%d\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, cmdDup->pcfile, cmdDup->iline); continue; } if ((char *)0 == pcCur || (char *)0 == pcDup) { continue; } if (lCur != (lDup = strtol(pcDup, &pcEnd, 0))) { continue; } if (SamePats(cmd, cmdDup)) { fprintf(stderr, "%s: %s: duplicated in %s:%d and %s:%d, argument count %ld\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, cmdDup->pcfile, cmdDup->iline, lDup); } } if (acDefault == cmd->pcname) { if ((char *)0 != (pcDup = FindOpt(cmd, acOptNolog))) { fprintf(stderr, "%s: %s: %s:%d: including %s here is presumptuous, since logging cannot enabled per command\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptNolog); /* no exit code for this */ } continue; } if (0 != Craving(cmd, & Need)) { iRet = EX_DATAERR; } SP.pNE = & Need; CheckRoot(cmd, &SP); /* When we failed to stat(2) a command and we never saw * an alternate that we could stat, then we complain. * Don't complain for every failed stat call: you may have * a Linux path and an Solaris path as alternatives, viz.: * $1=^(/sbin|/usr/sbin)/foo$ */ if ((char *)0 != SP.pcexecmiss && (char *)0 == SP.pcexecok) { fprintf(stderr, "%s: %s: %s:%d: stat: %s: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, SP.pcexecmiss, strerror(SP.ierrno)); iRet = EX_UNAVAILABLE; } if ((char *)0 != SP.pcexecok) { free((void *) SP.pcexecok); } if ((char *)0 != SP.pcexecmiss) { free((void *)SP.pcexecmiss); } if (EX_OK != SP.iexit) { iRet = SP.iexit; } /* Give up because we'll not match any FindOpts below, and * we don't need any options to work */ if (0 == cmd->nopts && !Need.fUid && !Need.fGid && !Need.fSecFile) { continue; } /* check to see we have a valid uid= and gid= */ if ((char *)0 == (pcDup = pcUser = FindOpt(cmd, acOptUid))) { if (acDefault != cmd->pcname) fprintf(stderr, "%s: %s: %s:%d: uid defaults to the superuser, make that explicit\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); } else if (IsMyself(pcUser) || '\000' == *pcUser) { /* OK */ } else if (0 == strcmp(pcUser, acSpecF) || 0 == strcmp(pcUser, acSpecD)) { DupCheck(cmd, "-f", &SP.pcspecF, acOptUid, acDupU); } else if (0 == strcmp(pcUser, acSpecU)) { DupCheck(cmd, "-u", &SP.pcspecU, acOptUid, acDupU); } else if (0 == strcmp(pcUser, acSpecG)) { fprintf(stderr, "%s: %s: %s:%d: -g cannot use specification as a login\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); iRet = EX_NOUSER; } else if ((char *)0 != strchr(pcUser, ',')) { fprintf(stderr, "%s: %s: %s:%d: %s=%s: should not be a list -- just a single login specification\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptUid, pcUser); iRet = EX_CONFIG; } else if (LooksLikeRE(pcUser)) { fprintf(stderr, "%s: %s: %s:%d: %s %s: looks like a regular expression, should be a login\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptUid, pcUser); iRet = EX_CONFIG; } else if (isdigit(*pcUser) && (struct passwd *)0 == getpwuid(atoi(pcUser))) { fprintf(stderr, "%s: %s: %s:%d: no such uid \"%s\"\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcUser); iRet = EX_NOUSER; } else if (!isdigit(*pcUser) && (struct passwd *)0 == getpwnam(pcUser)) { fprintf(stderr, "%s: %s: %s:%d: %s: no such user \"%s\"\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptUid, pcUser); iRet = EX_NOUSER; } if ((char *)0 == (pcUser = FindOpt(cmd, acOptEuid))) { /* We already carped about default to the superuser */ } else if (IsMyself(pcUser)) { /* nothing going on here */ } else if (0 == strcmp(pcUser, acSpecF) || 0 == strcmp(pcUser, acSpecD)) { DupCheck(cmd, "-f", &SP.pcspecF, acOptEuid, acDupU); } else if (0 == strcmp(acSpecU, pcUser)) { DupCheck(cmd, "-u", &SP.pcspecU, acOptEuid, acDupU); } else if (0 == strcmp(acSpecG, pcUser)) { fprintf(stderr, "%s: %s: %s:%d: cannot use -g's specification as a login\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); iRet = EX_NOUSER; } else if ((char *)0 != strchr(pcUser, ',')) { fprintf(stderr, "%s: %s: %s:%d: %s=%s should not be a list -- just a single login specification\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptEuid, pcUser); iRet = EX_CONFIG; } else if (LooksLikeRE(pcUser)) { fprintf(stderr, "%s: %s: %s:%d: %s: %s: looks like a regular expression, should be a login\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptEuid, pcUser); iRet = EX_CONFIG; } else if (isdigit(*pcUser) && (struct passwd *)0 == getpwuid(atoi(pcUser))) { fprintf(stderr, "%s: %s: %s:%d: %s: no such euid \"%s\"\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptEuid, pcUser); iRet = EX_NOUSER; } else if (!isdigit(*pcUser) && (struct passwd *)0 == getpwnam(pcUser)) { fprintf(stderr, "%s: %s: %s:%d: %s: no such user \"%s\"\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptEuid, pcUser); iRet = EX_NOUSER; } if ((char *)0 == (pcExplInitGrp = FindOpt(cmd, acOptInitgrps))) { if ((char *)0 == FindOpt(cmd, acOptUid) && (char *)0 == FindOpt(cmd, acOptEuid) && (char *)0 == FindOpt(cmd, acOptGid) && (char *)0 == FindOpt(cmd, acOptEgid)) fprintf(stderr, "%s: %s: %s:%d: %s: groups defaults to the original real gid, use \"gid=.\" to make that explicit\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptInitgrps); } else if ('\000' == *(pcRead = pcExplInitGrp)) { if ((char *)0 == pcDup && (char *)0 == pcUser) { fprintf(stderr, "%s: %s: %s:%d: %s: no uid or euid set to copy\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptInitgrps); iRet = EX_NOUSER; } } else if (IsMyself(pcRead)) { /* nothing special */ } else if (0 == strcmp(acSpecF, pcRead) || 0 == strcmp(acSpecD, pcRead)) { DupCheck(cmd, "-f", &SP.pcspecF, acOptInitgrps, acDupU); } else if (0 == strcmp(acSpecU, pcRead)) { DupCheck(cmd, "-u", &SP.pcspecU, acOptInitgrps, acDupU); } else if (0 == strcmp(acSpecG, pcRead)) { fprintf(stderr, "%s: %s: %s:%d: %s: %%g cannot be used as a login name\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptInitgrps); iRet = EX_NOUSER; } else if ((char *)0 != strchr(pcRead, ',')) { fprintf(stderr, "%s: %s: %s:%d: %s=%s: should not be a list -- just a single login name\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptInitgrps, pcRead); iRet = EX_CONFIG; } else if (LooksLikeRE(pcRead)) { fprintf(stderr, "%s: %s: %s:%d: %s %s: looks like a regular expression, should be a login\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptInitgrps, pcRead); } else if (isdigit(*pcRead)) { fprintf(stderr, "%s: %s: %s:%d: %s %s: must be a login name, %%f, %%u, %%l, or .\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptInitgrps, pcRead); iRet = EX_NOUSER; } else if ((struct passwd *)0 == getpwnam(pcRead)) { fprintf(stderr, "%s: %s: %s:%d: no such %s login \"%s\"\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptInitgrps, pcRead); iRet = EX_NOUSER; } /* N.B. we use pcDup to avoid fetching the session again below */ if ((char *)0 == (pcRead = pcDup = FindOpt(cmd, acOptSession)) || '\000' == *pcRead) { /* no session set, which is usually OK */ pcDup = (char *)0; } else if (IsMyself(pcRead)) { /* noting special */ } else if (0 == strcmp(pcRead, acKeyInitgroup)) { if ((const char *)0 == pcExplInitGrp) fprintf(stderr, "%s: %s: %s:%d: session %s: no explicit initgroups to copy\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acKeyInitgroup); } else if (0 == strcmp(pcRead, acSpecF) || 0 == strcmp(pcRead, acSpecD)) { DupCheck(cmd, "-f", &SP.pcspecF, acOptSession, acDupU); } else if (0 == strcmp(acSpecU, pcRead)) { DupCheck(cmd, "-u", &SP.pcspecU, acOptSession, acDupU); } else if (0 == strcmp(pcRead, acSpecG)) { fprintf(stderr, "%s: %s: %s:%d: %s: cannot use -g as a login name\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptSession); iRet = EX_NOUSER; } else if ((char *)0 != strchr(pcRead, ',')) { fprintf(stderr, "%s: %s: %s:%d: session %s: is not a list -- just a single login specification\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcRead); iRet = EX_CONFIG; } else if (LooksLikeRE(pcRead)) { fprintf(stderr, "%s: %s: %s:%d: %s %s: looks like a regular expression, should be a login\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptSession, pcRead); iRet = EX_CONFIG; } else if (isdigit(*pcRead)) { fprintf(stderr, "%s: %s: %s:%d: session %s: must be a login name, %%f, %%i, %%u, %%l, or %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcRead, acSpecMe); iRet = EX_NOUSER; } else if ((struct passwd *)0 == getpwnam(pcRead)) { fprintf(stderr, "%s: %s: %s:%d: no such session login \"%s\"\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcRead); iRet = EX_NOUSER; } if ((char *)0 == (pcRead = FindOpt(cmd, acOptCleanup)) || '\000' == *pcRead) { /* no cleanup, which is ok, even if we session'd */ } else if (IsMyself(pcRead)) { if ((char *)0 == pcDup) { fprintf(stderr, "%s: %s: %s:%d: %s %s: no matching %s specified\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptCleanup, pcRead, acOptSession); /* not worth the non-zero exit code */ } } else if (0 == strcmp(pcRead, acKeyInitgroup)) { if ((const char *)0 == pcExplInitGrp) fprintf(stderr, "%s: %s: %s:%d: %s %s: no explicit initgroups to copy\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptCleanup, acKeyInitgroup); } else if (0 == strcmp(acSpecF, pcRead) || 0 == strcmp(acSpecD, pcRead)) { DupCheck(cmd, "-f", &SP.pcspecF, acOptCleanup, acDupU); } else if (0 == strcmp(acSpecU, pcRead)) { DupCheck(cmd, "-u", &SP.pcspecU, acOptCleanup, acDupU); } else if (0 == strcmp(acSpecG, pcRead)) { fprintf(stderr, "%s: %s: %s:%d: %s: cannot use -g as a login name\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptSession); iRet = EX_NOUSER; } else if ((char *)0 != strchr(pcRead, ',')) { fprintf(stderr, "%s: %s: %s:%d: %s %s: is not a list -- just a single application specification or %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptCleanup, pcRead, acMyself); iRet = EX_CONFIG; } else if (LooksLikeRE(pcRead)) { fprintf(stderr, "%s: %s: %s:%d: %s %s: looks like a regular expression, should be a login\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptCleanup, pcRead); iRet = EX_CONFIG; } else if ((struct passwd *)0 == getpwnam(pcRead)) { fprintf(stderr, "%s: %s: %s:%d: no such cleanup login \"%s\"\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcRead); iRet = EX_NOUSER; } if ((char *)0 != (pcRead = FindOpt(cmd, acOptGid))) { while ((char *)0 != (pcRead = GetField(pcRead, &pcField))) { if (IsMyself(pcField)) { /* nada */ } else if (0 == strcmp(acSpecF, pcField) || 0 == strcmp(acSpecD, pcField)) { DupCheck(cmd, "-f", &SP.pcspecF, acOptGid, acDupU); } else if (0 == strcmp(acSpecU, pcField)) { DupCheck(cmd, "-u", &SP.pcspecU, acOptGid, acDupU); } else if (0 == strcmp(acSpecG, pcField)) { DupCheck(cmd, "-g", &SP.pcspecG, acOptGid, acDupU); } else if (isdigit(*pcField)) { if ((struct group *)0 == getgrgid(atoi(pcField))) { fprintf(stderr, "%s: %s: %s:%d: no such gid %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcField); iRet = EX_NOUSER; } } else if ((struct group *)0 == getgrnam(pcField)) { fprintf(stderr, "%s: %s: %s:%d: no such group %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcField); iRet = EX_NOUSER; } } EndField(); } if ((char *)0 != (pcGroup = FindOpt(cmd, acOptEgid))) { if (IsMyself(pcGroup)) { /* nada */ } else if (0 == strcmp(acSpecF, pcGroup) || 0 == strcmp(acSpecD, pcGroup)) { DupCheck(cmd, "-f", &SP.pcspecF, acOptEgid, acDupU); } else if (0 == strcmp(acSpecU, pcGroup)) { DupCheck(cmd, "-u", &SP.pcspecU, acOptEgid, acDupU); } else if (0 == strcmp(acSpecG, pcGroup)) { DupCheck(cmd, "-g", &SP.pcspecG, acOptEgid, acDupU); } else if (isdigit(*pcGroup)) { if ((struct group *)0 == getgrgid(atoi(pcGroup))) { fprintf(stderr, "%s: %s: %s:%d: no such egid %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcGroup); iRet = EX_NOUSER; } } else if ((struct group *)0 == getgrnam(pcGroup)) { fprintf(stderr, "%s: %s: %s:%d: no such effective group %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcGroup); iRet = EX_NOUSER; } } /* Look for any posible groups, users, that might match this * rule and carp when there is not even a possible match, also * look through each specified netgroup, try to guess if this * group might match someone. If we match the host and the * host allows anyone in the current login list, for example. * We don't look at domain. We do debug your /etc/netgroup. */ iMatch = 0; if ((char *)0 != (pcGroup = FindOpt(cmd, acOptGroups))) { iMatch |= CheckMatch(cmd, acOptGroups, ppcGroups, pcGroup, &iRet); } if ((char *)0 != (pcRead = FindOpt(cmd, acOptUsers))) { iMatch |= CheckMatch(cmd, acOptUsers, ppcUsers, pcRead, &iRet); } if (((char *)0 != (pcGroup = FindOpt(cmd, acOptNetgroups)))) { auto char *pcHost, *pcLogin, *pcDomain; auto char acMyName[512]; /* traditionally 256 */ auto char acPat[128]; /* temp RE: m/^$login$/ */ if (-1 == gethostname(acMyName, sizeof(acMyName))) { (void)strncpy(acMyName, ":", sizeof(acMyName)); } pcRead = pcGroup; while ((char *)0 != (pcRead = GetField(pcRead, &pcField))) { if (LooksLikeRE(pcField)) { fprintf(stderr, "%s: %s: %s:%d: netgroup %s: looks like a regular expression, should be a name\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcField); /* is not 100% sure, don't fail */ } if ('-' == pcField[0] && '\000' == pcField[1]) { continue; } setnetgrent(pcField); while (getnetgrent(&pcHost, &pcLogin, &pcDomain)) { pcGroup = (char *)0; if ((char *)0 != pcHost && 0 != strcmp(acMyName, pcHost)) { if ('*' == pcHost[0]) { fprintf(stderr, "%s: netgroup %s: use the empty field for any host (not an explicit '*')\n", Progname, pcField); continue; } continue; } if ((char *)0 == pcLogin) { iMatch |= 1; continue; } if ('*' == pcLogin[0]) { fprintf(stderr, "%s: netgroup %s: use the empty field for anyone (not an explicit '*')\n", Progname, pcField); continue; } snprintf(acPat, sizeof(acPat), "^%s$", pcLogin); iMatch |= CheckMatch(cmd, acOptNetgroups, ppcUsers, acPat, &iRet); } endnetgrent(); } EndField(); if ((char *)0 != pcGroup) { fprintf(stderr, "%s: %s: %s:%d none of these netgroups exist: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcGroup); /* not worth a nonzero exit code */ } } if (((char *)0 != (pcRead = FindOpt(cmd, acOptHelmet)))) { CheckProg(cmd, pcRead, &iRet, (struct stat *)0); } if (((char *)0 != (pcRead = FindOpt(cmd, acOptJacket)))) { CheckProg(cmd, pcRead, &iRet, (struct stat *)0); } if (acDefault != cmd->pcname && 0 == iMatch) { fprintf(stderr, "%s: %s: %s:%d: no users, groups, or netgroups match\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); iRet = EX_NOUSER; } if ((char *)0 != (pcDup = FindOpt(cmd, acOptFib))) { register long lFib; auto int iMaxFib = 1; #if HAVE_SETFIB auto int aiMib[16]; auto size_t iMibLen, iDataLen; static const char acNetFibs[] = "net.fibs"; #endif lFib = strtol(pcDup, &pcEnd, 0); if ((char *)0 != pcEnd && '\000' != *pcEnd) { fprintf(stderr, "%s: %s: %s:%d: %s: \"%s\" badly formatted counting number\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptFib, pcDup); iRet = EX_DATAERR; } #if HAVE_SETFIB iMibLen = sizeof(aiMib)/sizeof(aiMib[0]); iDataLen = sizeof(iMaxFib); if (-1 == sysctlnametomib(acNetFibs, aiMib, &iMibLen)) { fprintf(stderr, "%s: %s: %s:%d: %s: %s: no such mib?\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptFib, acNetFibs); } else if (-1 == sysctl(aiMib, iMibLen, &iMaxFib, &iDataLen, NULL, 0)) { fprintf(stderr, "%s: %s: %s:%d: %s: sysctl: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptFib, strerror(errno)); } #endif if (0 > lFib) { fprintf(stderr, "%s: %s: %s:%d: %ld: fib values are counting numbers\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, lFib); iRet = EX_PROTOCOL; } else if (iMaxFib <= lFib) { fprintf(stderr, "%s: %s: %s:%d: %ld: fib value out of range (limit %ld)\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, lFib, (long)iMaxFib); iRet = EX_PROTOCOL; } } if ((char *)0 != (pcDup = FindOpt(cmd, acOptNice))) { register long lNice; lNice = strtol(pcDup, &pcEnd, 0); if ((char *)0 != pcEnd && '\000' != *pcEnd) { fprintf(stderr, "%s: %s: %s:%d: %s: \"%s\" should be an integer from -20..20\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptNice, pcDup); iRet = EX_DATAERR; } if (-20 > lNice || 20 < lNice) { fprintf(stderr, "%s: %s: %s:%d: %ld: nice value out of range\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, lNice); iRet = EX_PROTOCOL; } } if (!CheckRedir(cmd, "<", acOptStdin)) { iRet = EX_PROTOCOL; } if (!CheckRedir(cmd, ">", acOptStdout)) { iRet = EX_PROTOCOL; } if (!CheckRedir(cmd, ">", acOptStderr)) { iRet = EX_PROTOCOL; } if ((char *)0 == (pcDup = FindOpt(cmd, acPatFowners)) || '\000' == pcDup[0]) { /* OK */ } else if ((char *)0 != (pcDup = CheckCompile(pcDup, &pcCarp))) { fprintf(stderr, "%s: %s: %s:%d: %s: regcomp: %s: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acPatFowners, pcDup, pcCarp); } if ((char *)0 == (pcDup = FindOpt(cmd, acPatPerms)) || '\000' == pcDup[0]) { /* OK */ } else if ((char *)0 != (pcDup = CheckCompile(pcDup, &pcCarp))) { fprintf(stderr, "%s: %s: %s:%d: %s: regcomp: %s: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acPatPerms, pcDup, pcCarp); } if ((char *)0 != (pcDup = FindOpt(cmd, acOptMask))) { register long lMask; lMask = strtol(pcDup, &pcEnd, 8); if ((char *)0 != pcEnd && '\000' != *pcEnd) { fprintf(stderr, "%s: %s: %s:%d: %s: %s: should be an octal number\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptMask, pcDup); iRet = EX_DATAERR; } if (0 > lMask || 0777 < lMask) { fprintf(stderr, "%s: %s: %s:%d: %s: %04lo: value out of range\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptMask, lMask); iRet = EX_PROTOCOL; } } if ((char *)0 != (pcDup = FindOpt(cmd, acOptEnv)) && '\000' != *pcDup && (char *)0 != (pcField = CheckCompile(pcDup, &pcCarp))) { fprintf(stderr, "%s: %s: %s:%d: %s: regcomp: %s: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptEnv, pcField, pcCarp); iRet = EX_DATAERR; } if ((char *)0 == (pcRead = FindOpt(cmd, acOptPam)) || '\000' == *pcRead) { /* no pam requested */ } else if (IsMyself(pcRead)) { /* default pam requested */ } else if ((char *)0 != strchr(pcRead, ',')) { fprintf(stderr, "%s: %s: %s:%d: pam %s: specify a single application name or %s, not a list\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcRead, acMyself); iRet = EX_CONFIG; } else if (LooksLikeRE(pcRead)) { fprintf(stderr, "%s: %s: %s:%d: pam %s: specify a single application name or %s, not a regular expression\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcRead, acMyself); iRet = EX_CONFIG; #if !USE_PAM } else { fprintf(stderr, "%s: %s: %s:%d: pam unsupported\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); iRet = EX_UNAVAILABLE; #endif } /* else we could try to open a PAM context with it */ if ((char *)0 != (pcRead = FindOpt(cmd, acOptPass))) { iMatch = 0; if ('\000' == *pcRead) { pcRead = acMyself; } while ((char *)0 != (pcRead = GetField(pcRead, &pcField))) { if (0 == strcmp(acSpecU, pcField)) { if ((char *)0 != SP.pcspecU) { fprintf(stderr, "%s: %s: %s:%d: -u used for both %s and %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptPass, SP.pcspecU); } SP.pcspecU = acOptPass; iMatch = 1; } else if (0 == strcmp(pcField, acSpecF) || 0 == strcmp(pcField, acSpecD)) { if ((char *)0 != SP.pcspecF) { fprintf(stderr, "%s: %s: %s:%d: -f used for both %s and %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acOptPass, SP.pcspecF); } SP.pcspecF = acOptPass; iMatch = 1; } else if (IsMyself(pcField) || (struct passwd *)0 != getpwnam(pcField)) { iMatch = 1; } } EndField(); if (0 == iMatch) { fprintf(stderr, "%s: %s: %s:%d: no passwords available from logins in list\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); iRet = EX_NOUSER; } } for (iIndex = 0; iIndex < cmd->nopts; ++iIndex) { register const char *pcScan, *pcAttr; register int iLen, i; pcRead = cmd->opts[iIndex]; if ((char *)0 == (pcScan = strchr(pcRead, '='))) { iLen = strlen(pcRead); } else { iLen = pcScan-pcRead; } /* Just ignore $ENVs, $# should be a number (above), * compile $1,$2, etc and $*, all $N <= lHelpParam, * fall into !g, !u, !f checks (they match isalpha). */ if (ENV_OPEN(pcRead)) { /* N.B. "$$l=$f" is an env */ if (0 == strncmp(pcRead, acOptPATH, sizeof(acOptPATH)-1) && pcScan+1 == pcRead+sizeof(acOptPATH)) { CheckPath(cmd, pcScan+1); } continue; } else if ('%' == pcRead[0]) { /* see next */ } else if ('!' == pcRead[0] && isalpha(pcRead[1])) { /* see the next stanza */ } else if ('#' == pcRead[1]) { /* did above */ continue; } else if (isdigit(pcRead[1])) { lDup = strtol(pcRead+1, &pcEnd, 0); /* lTotalParam or lHelpParam XXX? */ if (-1 != Need.lHelpParam && Need.lHelpParam < lDup) { fprintf(stderr, "%s: %s: %s:%d: %.*s: parameter too large to match forced limit of %ld\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen, pcRead, Need.lHelpParam); iRet = EX_UNAVAILABLE; } if (pcScan != pcEnd && '\000' != *pcEnd) { fprintf(stderr, "%s: %s: %s:%d: %.*s: junk on the end of parameter number\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen, pcRead); iRet = EX_CONFIG; } if (0 == lDup) { fprintf(stderr, "%s: %s: %s:%d: %.*s: misleading '0': positional parameters are not numbered in octal\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen, pcRead); } else if ('0' == pcRead[1]) { fprintf(stderr, "%s: %s: %s:%d: %.*s: misleading '0': positional parameters are not numbered in octal\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen, pcRead); iRet = EX_CANTCREAT; } if ((const char *)0 == pcScan) { continue; } pcCarp = (char *)0; if ((char *)0 != (pcField = CheckCompile(pcScan+1, &pcCarp))) { fprintf(stderr, "%s: %s: %s:%d: %.*s: regcomp: %s: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen, pcRead, pcField, pcCarp); iRet = EX_DATAERR; } continue; } else if ('*' == pcRead[1] && 2 == iLen) { if ((const char *)0 == pcScan) { fprintf(stderr, "%s: %s: %s:%d: %.*s: is meaningless without an RE\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen, pcRead); continue; } ++pcScan; if ((char *)0 != (pcField = CheckCompile(pcScan, &pcCarp))) { fprintf(stderr, "%s: %s: %s:%d: %.*s: regcomp: %s: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen, pcRead, pcField, pcCarp); iRet = EX_DATAERR; } if (IsDotStar(pcScan)) { fprintf(stderr, "%s: %s: %s:%d: %.*s: rejects all parameters\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen, pcRead); iRet = EX_DATAERR; } continue; } /* $ in the environment is ok by me */ if (!('!' == pcRead[0] || '%' == pcRead[0])) { /* nada */ } else if ('{' == pcRead[1]) { /*} %{ENV} !{ENV} */ register char *pcMem; if ((char *)0 == pcScan && '%' == pcRead[0]) { fprintf(stderr, "%s: %s: %s:%d: %s: has no default value\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcRead); iRet = EX_DATAERR; } else if (/*{*/'}' != pcRead[iLen-1]) {/*{*/ fprintf(stderr, "%s: %s: %s:%d: %.*s: missing close curly (`}')\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen-2, pcRead+2); iRet = EX_DATAERR; } else if ((char *)0 != (pcField = CheckCompile((char *)0 == pcScan ? acDefNotEnv : pcScan+1, &pcCarp))) { fprintf(stderr, "%s: %s: %s:%d: %.*s: regcomp: %s: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen, pcRead, pcField, pcCarp); iRet = EX_DATAERR; } else if ((char *)0 != (pcMem = calloc((iLen|7)+1, sizeof(char)))) { snprintf(pcMem, iLen+1, "$%.*s", iLen-3, pcRead+2); if (0 != CheckTrust(cmd, pcMem)) { free((void *)pcMem); continue; } snprintf(pcMem, iLen+1, "{%.*s}", iLen-3, pcRead+2); fprintf(stderr, "%s: %s: %s:%d: $%s: is limited but not allowed in the escalated environment\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcMem); free((void *)pcMem); } continue; } else if ('m' == pcRead[1]) { SP.pcspecM = "read"; if ((char *)0 != (pcField = CheckCompile(pcScan+1, &pcCarp))) { fprintf(stderr, "%s: %s: %s:%d: %.2s: regcomp: %s: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcRead, pcField, pcCarp); iRet = EX_DATAERR; } if (! Need.fMacs) { fprintf(stderr, "%s: %s: %s:%d: %.*s: specification never used\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen, pcRead); /* not worth a non-zero exit code */ } continue; } else if ('g' == pcRead[1]) { SP.pcspecG = "read"; switch (iLen) { case 2: /* %g, !g */ break; case 4: /* %g@u, !g@u */ if ('@' != pcRead[2] || 'u' != pcRead[3]) { /*FALLTHROUGH*/ default: fprintf(stderr, "%s: %s: %s:%d: %%u unknown qualifier \"%.*s\"\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen-2, pcRead+2); iRet = EX_USAGE; break; } break; } if ((char *)0 != (pcField = CheckCompile(pcScan+1, &pcCarp))) { fprintf(stderr, "%s: %s: %s:%d: %.2s: regcomp: %s: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcRead, pcField, pcCarp); iRet = EX_DATAERR; } if (! Need.fGid) { fprintf(stderr, "%s: %s: %s:%d: %.*s: specification never used\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen, pcRead); /* not worth a non-zero exit code */ } continue; } else if ('u' == pcRead[1]) { SP.pcspecU = "read"; switch (iLen) { case 2: /* %u, !u */ break; case 4: /* %u@g, !u@g */ if ('@' != pcRead[2] || 'g' != pcRead[3]) { /*FALLTHROUGH*/ default: fprintf(stderr, "%s: %s: %s:%d: %%u unknown qualifier \"%.*s\"\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen-2, pcRead+2); iRet = EX_USAGE; break; } break; } if ((char *)0 != (pcField = CheckCompile(pcScan+1, &pcCarp))) { fprintf(stderr, "%s: %s: %s:%d: %.*s: regcomp: %s: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen, pcRead, pcField, pcCarp); iRet = EX_DATAERR; } if (! Need.fUid) { fprintf(stderr, "%s: %s: %s:%d: %.*s: specification never used\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen, pcRead); /* not worth a non-zero exit code */ } continue; } else if ('f' == pcRead[1] || 'd' == pcRead[1] || '_' == pcRead[1]) { register int fPos, fForD; auto char acCheckF[64]; /* %c%c.attr */ static cmd_t *aacmdDupLimit[2][3] = { {(cmd_t *)0, (cmd_t *)0, (cmd_t *)0}, {(cmd_t *)0, (cmd_t *)0, (cmd_t *)0} }; if ('_' != pcRead[1]) { SP.pcspecF = "limited"; } if (!('.' == pcRead[2] || '=' == pcRead[2])) { fprintf(stderr, "%s: %s: %s:%d: %.*s: unknown specification\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen, pcRead); continue; } if ('_' != pcRead[1] && ! Need.fSecFile) { fprintf(stderr, "%s: %s: %s:%d: %%f: specification never used\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); /* not worth a non-zero exit code */ Need.fSecFile = 1; } fPos = '%' == pcRead[0]; fForD = 'f' == pcRead[1] ? 1 : '_' == pcRead[1] ? 2 : 0; if ('=' == pcRead[2]) { snprintf(acCheckF, sizeof(acCheckF), "%c%c.%s", pcRead[0], pcRead[1], asFKnown); if ((char *)0 != FindOpt(cmd, acCheckF)) { if (cmd != aacmdDupLimit[fPos][fForD]) fprintf(stderr, "%s: %s: %s:%d: both %.2s and %s limit the same path\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acCheckF, acCheckF); aacmdDupLimit[fPos][fForD] = cmd; } continue; } if (3 == iLen) { fprintf(stderr, "%s: %s: %s:%d: %.*s: dot is missing an attr, or remove the dot?\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen, pcRead); iRet = EX_USAGE; continue; } snprintf(acCheckF, sizeof(acCheckF), "%c%c", pcRead[0], pcRead[1]); for (pcAttr = asFKnown; '\000' != *pcAttr; pcAttr += strlen(pcAttr)+1) { if (0 == strncmp(pcAttr, pcRead+3, iLen-3)) break; } if ('\000' == *pcAttr) { fprintf(stderr, "%s: %s: %s:%d: %%f: unknown attribute \"%.*s\"\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen-3, pcRead+3); iRet = EX_USAGE; } else if ((char *)0 != (pcField = CheckCompile(pcScan+1, &pcCarp))) { fprintf(stderr, "%s: %s: %s:%d: %.*s: regcomp: %s: %s\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen, pcRead, pcField, pcCarp); iRet = EX_DATAERR; } else if (0 == strcmp(asFKnown, pcAttr) && (char *)0 != FindOpt(cmd, acCheckF)) { if (cmd != aacmdDupLimit[fPos][fForD]) fprintf(stderr, "%s: %s: %s:%d: both %.2s and %.3s%s limit the same path\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acCheckF, pcRead, pcAttr); aacmdDupLimit[fPos][fForD] = cmd; } else if (0 == strcmp("perms", pcAttr) && 0 == CheckPermRE(pcScan+1, &pcCarp)) { fprintf(stderr, "%s: %s: %s:%d: %s.%s limit RE \"%s\" never matches\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acCheckF, pcAttr, pcScan+1); iRet = EX_DATAERR; } else if (0 == strcmp("mode", pcAttr) && 0 == CheckModeRE(pcScan+1, &pcCarp)) { fprintf(stderr, "%s: %s: %s:%d: %s.%s limit RE \"%s\" never matches\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, acCheckF, pcAttr, pcScan+1); iRet = EX_DATAERR; } continue; } else { fprintf(stderr, "%s: %s: %s:%d: %.2s: unknown parameter limiter\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, pcRead); continue; } for (i = 0; (char *)0 != (pcScan = apcConfHelp[i]); ++i) { if (0 == strncmp(pcRead, pcScan, iLen)) break; } if ((char *)0 == pcScan) { fprintf(stderr, "%s: %s: %s:%d: unknown keyword \"%.*s\"\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline, iLen, pcRead); iRet = EX_USAGE; } } if (acDefault == cmd->pcname) { continue; } if ((char *)0 == SP.pcspecU && Need.fUid) { fprintf(stderr, "%s: %s: %s:%d: %%u: is unlimited\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); iRet = EX_NOPERM; } if ((char *)0 == SP.pcspecG && Need.fGid) { fprintf(stderr, "%s: %s: %s:%d: %%g: is unlimited\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); iRet = EX_NOPERM; } if ((char *)0 == SP.pcspecF && Need.fSecFile) { fprintf(stderr, "%s: %s: %s:%d: %%f: is unlimited\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); iRet = EX_NOPERM; } /* When we didn't see %m or !m and you used the raw -m as * a the whole process mac label we carp about it. */ if ((char *)0 == SP.pcspecM && Need.fMacs) { fprintf(stderr, "%s: %s: %s:%d: %%m: is unlimited\n", Progname, cmd->pcname, cmd->pcfile, cmd->iline); iRet = EX_TEMPFAIL; } } if ((cmd_t *)0 == cmd) { fprintf(stderr, "%s: no rules configured%s\n", Progname, acSure); iRet = EX_TEMPFAIL; } } exit(iRet); } /* If a NEEDs structure wants more than 1 argument return "s" (ksb) * else the empty string. */ static const char * NeedsPlural(const NEEDS *pNE) { static const char acS[] = "s"; register const char *pcPlural = acS+1; if (-1 == pNE->lTotalParam) { pcPlural = acS; } else if (0 == pNE->lCmdParam) { if (1 != pNE->lTotalParam) pcPlural = acS; } else if (pNE->lCmdParam < pNE->lTotalParam) { pcPlural = acS; } return pcPlural; } /* Given the complete RE-list for a parameter make a usage string. (ksb) * If we build the string you can free it, if we return pcDef you must * know what to do with it. Since the return from all the fields must * be at most the size of all the fields minus the anchors we know it must * fit in a buffer as long as the input. No other overrun checks needed. */ static const char * FieldToUsage(const char *pcREList, const char *pcDef, void **ppvAlloc ) { register char *pcRet; register const char *pcCheck; register size_t wMin; auto char *pcField; auto void *pvLocal; if ((void **)0 != ppvAlloc) { *ppvAlloc = (void *)0; } /* We know that the longest fixed string IsAnchored returns is number: * we should never overflow be number of commas times 8 + length(RE). */ wMin = 1; if ((char *)0 != (pcCheck = pcREList)) while ((char *)0 != (pcCheck = strchr(pcCheck, ','))) { ++pcCheck, ++wMin; } if ((const char *)0 == pcREList || (void *)0 == (pvLocal = malloc((strlen(pcREList)|15)+wMin*8+1))) { return pcDef; } pcRet = pvLocal; *pcRet = '\000'; while ((char *)0 != (pcREList = GetField(pcREList, &pcField))) { pcCheck = IsAnchored(pcField, (const char *)0); if ((char *)0 == pcCheck) { free(pvLocal); return pcDef; } if ('\000' != *pcRet) { (void)strcat(pcRet, "|"); } (void)strcat(pcRet, pcCheck); } EndField(); if ((void **)0 != ppvAlloc) { *ppvAlloc = pvLocal; } return pcRet; } /* We split the difference by default we'll show you the rules, but you * can remove it with a -DOP_SHOW_RULES=0 */ #if !defined(OP_SHOW_RULES) #define OP_SHOW_RULES 1 #endif /* Output $0 [-g group] [-u user] [-f file] mnemonic... (ksb) * We drop for this, as we shouldn't have to do anything super-ish. * We can view the used (-l), or the run-time (-r) or that plus why (-w). */ static void RulesAllowed(int fWhichView, int argc, char **argv) { #if OP_SHOW_RULES register cmd_t *cmd, *cmdScan; register long lScan, lEnd; register const char *pcRead, *pcCodeCheck, *pcLook, *pcDef; register size_t i; auto const char *pcUserMay; auto int uOrig, fTold, iExit; auto struct passwd *pw; auto struct group *gr; auto void *pvDispose; auto char *pcField, *pcCarp; auto char acCvt[OP_MAX_LDWIDTH]; /* "$%ld" or "#%ld" */ auto char acMyName[512]; auto NEEDS Need; static const char acArg[] = "arg", acScript[] = "script", acCommand[] = "command", acArgs[] = "argument"; pcCarp = (char *)0; iExit = EX_OK; uOrig = getuid(); switch (argc) { case 0: if ((struct passwd *)0 == (pw = getpwuid(uOrig))) fatal("getpwuid: %ld: %s", (long)uOrig, strerror(errno)); break; case 1: if (0 != uOrig) fatal(acBadAllow); if ((struct passwd *)0 == (pw = getpwnam(argv[0]))) fatal("getpwnam: %s: %s", argv[0], strerror(errno)); if (0 != initgroups(argv[0], pw->pw_gid)) fatal("%s: %s", acOptInitgrps, strerror(errno)); #if HAVE_setresgid if (0 != setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid)) fatal("setresgid: %ld,%ld,%ld: %s", (long)pw->pw_gid, (long)pw->pw_gid, (long)pw->pw_gid, strerror(errno)); #else if (0 != setgid(pw->pw_gid)) fatal("setgid: %ld: %s", (long)pw->pw_gid, strerror(errno)); #endif uOrig = pw->pw_uid; break; default: fatal("usage: -%c login", fWhichView); } #if HAVE_setresuid if (setresuid(uOrig, uOrig, uOrig) < 0) { fatal("setreuid: %ld,%ld: %s", (long)uOrig, (long)uOrig, strerror(errno)); } #else if (setreuid(uOrig, uOrig) < 0) { fatal("setreuid: %ld,%ld: %s", (long)uOrig, (long)uOrig, strerror(errno)); } #endif if (-1 == gethostname(acMyName, sizeof(acMyName))) { fatal("gethostname: %s", strerror(errno)); } fTold = 0; for (cmdScan = NewCmd((const char *)0); (cmd_t *)0 != cmdScan; cmdScan = cmdScan->pODnext) { if (acDefault == cmdScan->pcname) continue; if ((cmd_t *)0 == (cmd = Build(cmdScan))) continue; /* Can the Customer run it? Allow root to see them all, * but flag ones she can't run (with a hash comment). */ pcUserMay = (const char *)0; if ((const char *)0 == pcUserMay && (char *)0 != (pcRead = FindOpt(cmd, acOptGroups))) { if (IsDotStar(pcRead+('#' == pcRead[0]))) { pcUserMay = '#' == pcRead[0] ? "any gid" : "any group"; } else if (AnyMatch(pcRead, ":", pw->pw_gid, &pcCarp)) { pcUserMay = "login group by gid"; } else if ((struct group *)0 == (gr = getgrgid(pw->pw_gid))) { /* Can't reverse login group, tell Admin */ syslog(LOG_ERR, "login %s gid #%ld: doesn't reverse", pw->pw_name, (long)pw->pw_gid); } else if (AnyMatch(pcRead, gr->gr_name, gr->gr_gid, &pcCarp)) { pcUserMay = "login group name"; } setgrent(); while ((const char *)0 == pcUserMay && (struct group *)0 != (gr = getgrent())) { for (i = 0; (char *)0 != gr->gr_mem[i]; ++i) { if (0 == strcmp(gr->gr_mem[i], pw->pw_name)) break; } if (((char *)0 != gr->gr_mem[i]) && AnyMatch(pcRead, gr->gr_name, gr->gr_gid, &pcCarp)) { pcUserMay = "group membership"; break; } } /* We ignore pcCarp becasue we don't want to tell the * Customer that the config has a bad RE. (ksb) */ } if ((const char *)0 == pcUserMay && ((char *)0 != (pcRead = FindOpt(cmd, acOptUsers)))) { if (IsDotStar(pcRead+('#' == pcRead[0]))) { pcUserMay = '#' == pcRead[0] ? "any uid" : "any login"; } else if (AnyMatch(pcRead, ":", pw->pw_uid, &pcCarp)) { pcUserMay = acOptUid; } else if (AnyMatch(pcRead, pw->pw_name, pw->pw_uid, &pcCarp)) { pcUserMay = "login name"; } } if ((const char *)0 == pcUserMay && ((char *)0 != (pcRead = FindOpt(cmd, acOptNetgroups)))) { while ((char *)0 != (pcRead = GetField(pcRead, &pcField))) { if (innetgr(pcField, acMyName, pw->pw_name, (char *)0)) { pcUserMay = "netgroup membership"; break; } } EndField(); } if ((const char *)0 == pcUserMay && 0 != uOrig) { continue; } /* if craving fails it is a format error in the params */ if (0 != Craving(cmd, & Need)) { if (!fTold) { fTold = 1; syslog(LOG_ERR, "%s: %s:%d: user notified that you need to run -S more often", cmd->pcname, cmd->pcfile, cmd->iline); fprintf(stderr, "%s: Please ask the administrator to run -S and fix our configuration.\n", Progname); } iExit = EX_CONFIG; printf("#"); } else if (0 == uOrig && (const char *)0 == pcUserMay) { printf("#"); } /* under -r or -w list the command we are talking about */ if ('r' == fWhichView || 'w' == fWhichView) { auto const char *pcLazen; if ((const char *)0 == (pcCodeCheck = FindOpt(cmd, acOptJacket))) { pcCodeCheck = FindOpt(cmd, acOptHelmet); } printf("%s", cmd->pcname); pvDispose = (void *)0; for (lScan = 0; lScan < cmd->nargs; ++lScan) { pcRead = pcLook = cmd->args[lScan]; if ('$' != pcLook[0]) { pcRead = pcLook; } else if (isdigit(pcLook[1]) && (strtol(pcLook+1, & pcField, 0), '\000' == *pcField) && (char *)0 != (pcRead = FindOpt(cmd, pcLook))) { pcRead = FieldToUsage(pcRead, pcLook, &pvDispose); } else if ('\000' == pcLook[1] || '\000' != pcLook[2]) { /* as is */ } else switch (pcLook[1]) { case '0': printf(" %s", cmd->pcname); continue; case '@': case '*': pcRead = FieldToUsage(FindOpt(cmd, "$*"), acArg, &pvDispose); printf(" %s%s", pcRead, NeedsPlural(&Need)); goto release; case 's': printf(" {script}"); continue; case 'S': printf(" %s", acShell); continue; case '$': printf(" $"); continue; case '|': /* a forced empty word */ printf(" ''"); break; default: break; } if ((char *)0 == pcRead) { printf(" %s", pcLook); continue; } printf(" %s", pcRead); release: if ((void *)0 != pvDispose) { free(pvDispose); pvDispose = (void *)0; } } if (1 == cmd->nargs && 0 == strcmp(acMagicShell, cmd->args[0])) { printf(" [-c command]"); } if ((char *)0 != (pcLazen = FindOpt(cmd, acOptDaemon))) { pcLazen = acDevNull; } if ((char *)0 != (pcRead = FindOpt(cmd, acOptStdin)) || (char *)0 != (pcRead = pcLazen)) { printf(" "); if ('<' != *pcRead && '>' != *pcRead) printf("<"); printf("%s", pcRead); } if ((char *)0 != (pcRead = FindOpt(cmd, acOptStdout)) || (char *)0 != (pcRead = pcLazen)) { printf(" "); if ('<' != *pcRead && '>' != *pcRead) printf(">"); printf("%s", pcRead); } if ((char *)0 != (pcRead = FindOpt(cmd, acOptStderr)) || (char *)0 != (pcRead = pcLazen)) { printf(" 2"); if ('<' != *pcRead && '>' != *pcRead) printf(">"); printf("%s", pcRead); } if ((char *)0 != pcLazen) { printf(" &"); } if ('w' == fWhichView) { register const char *pcUserAuth; if ((const char *)0 == pcUserMay) { printf(" [may not"); } else if ((const char *)0 != pcCodeCheck) { printf(" [%s plus %s", pcUserMay, pcCodeCheck); } else { printf(" [by %s", pcUserMay); } pcRead = FindOpt(cmd, acOptPam); if ((char *)0 != pcRead && '\000' == *pcRead) { pcRead = (const char *)0; } if ((char *)0 == FindOpt(cmd, acOptPass)) { pcUserAuth = ((char *)0 == pcRead) ? (const char *)0 : acOptPam; } else { pcUserAuth = ((char *)0 == pcRead) ? "password" : "either pam or password"; } if ((const char *)0 != pcUserAuth && (const char *)0 != pcUserMay) { printf(" with %s authentication", pcUserAuth); } printf("]"); } printf("\n"); continue; } /* under -l list the command-line form */ printf("%s", Progname); if (Need.fUid) printf(" -u uid"); if (Need.fGid) printf(" -g gid"); if (Need.fSecFile) /* doing more here might radiate */ printf(" -f %s", Forced_F(cmd, 'd') ? "dir" : "file"); if (Need.fMacs) printf(" -m mac"); printf(" %s", cmd->pcname); if (1 == cmd->nargs && 0 == strcmp(acMagicShell, cmd->args[0])) printf(" [command]"); pcDef = FindOpt(cmd, "$*"); if (-1 == (lEnd = Need.lHelpParam)) lEnd = Need.lCmdParam; for (lScan = 1; lScan <= lEnd; ++lScan) { register const char *pcBetter; static const char acFiction[] = "argv[1]"; snprintf(acCvt, sizeof(acCvt), "$%ld", lScan); /* Look for a better tag based on using a shell as * the command, which is really common in practice. * You can fool us, e.g. /etc/nologin as a shell. */ pcBetter = acCvt; if (2 > cmd->nargs || !GenMatch(acCvt, cmd->args[0], "^[$](S|[{]SHELL[}]|k|K)$,^/bin/.*sh$")) /* not a shell -c or shell -x */; else if (0 == strcmp(cmd->args[1], acCvt)) pcBetter = acScript; else if (0 != strcmp(cmd->args[2], acCvt)) /* not too clever here */; else if (GenMatch(acFiction, cmd->args[1], "^-[^cs]+$")) pcBetter = acScript; else if (GenMatch(acFiction, cmd->args[1], "^-.*s")) pcBetter = acArgs; /* sh says string, bah */ else if (GenMatch(acFiction, cmd->args[1], "^-.*c")) pcBetter = acCommand; /* sh says string, bah */ else if ('$' != cmd->args[1][0] || !isdigit(cmd->args[1][1]) || (char *)0 == (pcRead = FindOpt(cmd, cmd->args[1])) || (char *)0 == (pcRead = FieldToUsage(pcRead, (char *)0, &pvDispose))) /* out of luck on RE match too */; else { if (GenMatch(acFiction, pcRead, "^-[^cs]+$")) pcBetter = acScript; else if (GenMatch(acFiction, pcRead, "^-.*s")) pcBetter = acArgs; else if (GenMatch(acFiction, pcRead, "^-.*c")) pcBetter = acCommand; free((void *)pvDispose); pvDispose = (void *)0; } if ((char *)0 == (pcRead = FindOpt(cmd, acCvt))) pcRead = pcDef; if ((char *)0 == pcRead) { printf(" %s", pcBetter); continue; } /* If the pattern looks like a usage "^(a|b|c)$" * or "^word$" then we'll output it filtered, else * just output $n (or somethig better). */ pcRead = FieldToUsage(pcRead, pcBetter, &pvDispose); printf(" %s", pcRead); if ((void *)0 != pvDispose) { free(pvDispose); pvDispose = (void *)0; } } /* If we have a $* figure out if it is plural: * viz. when no total param set, also when no spec. max and * total param > 1, else when max is less than the forced total */ if (Need.fDStar && (-1 == Need.lTotalParam || Need.lHelpParam < Need.lTotalParam)) { pcRead = FieldToUsage(pcDef, acArg, &pvDispose); printf(" [%s%s]", pcRead, NeedsPlural(&Need)); if ((void *)0 != pvDispose) { free(pvDispose); pvDispose = (void *)0; } } printf("\n"); } exit(iExit); #else fatal("Sorry, that option is forbidden by site policy"); exit(EX_UNAVAILABLE); #endif } #if USE_PAM /* call-back to us from pam_* functions for pam option (ksb) * Another Sun compiler botch: because I mark this static to limit the * scope the compiler tells me the type is different in the assignment to * any (struct pam_conv).conv, storage class is not a type, bozos. */ static int OpConv(int iMsg, const struct pam_message **ppmsg, struct pam_response **ppresp, void *pvAppData) { register int i; register char *pcFetch; auto size_t wLen; if ((struct pam_response *)0 == (*ppresp = calloc(iMsg, sizeof(struct pam_response)))) { fatal(acBadAlloc); } (void)pvAppData; /* the cmd structure we don't need (yet) */ for (i = 0; i < iMsg; ++i) { ppresp[i]->resp = (char *)0; ppresp[i]->resp_retcode = 0; switch (ppmsg[i]->msg_style) { case PAM_PROMPT_ECHO_ON: if ((char *)0 == (pcFetch = fgetln(stdin, & wLen))) fatal("fgetln: %s", strerror(errno)); if (wLen > 0 && '\n' == pcFetch[wLen-1]) --wLen, pcFetch[wLen] = '\000'; break; case PAM_PROMPT_ECHO_OFF: if ((char *)0 == (pcFetch = getpass("Password:"))) fatal("getpass: %s", strerror(errno)); wLen = strlen(pcFetch); break; case PAM_TEXT_INFO: if ((char *)0 != ppmsg[i]->msg) puts(ppmsg[i]->msg); continue; case PAM_ERROR_MSG: if ((char *)0 != ppmsg[i]->msg) fprintf(stderr, "%s\n", ppmsg[i]->msg); continue; default: fatal("pam sent a message type (%d)", ppmsg[i]->msg_style); return PAM_CONV_ERR; } if ((char *)0 == (ppresp[i]->resp = calloc((wLen|15)+1, sizeof(char)))) fatal(acBadAlloc); (void)strncpy(ppresp[i]->resp, pcFetch, wLen+1); } return PAM_SUCCESS; } #endif /* Provide a dynamic copy of the reason this access is being granted, (ksb) * viz. cred:value\000 */ static char * KeepMay(const char *pcCred, const char *pcValue) { register size_t iLen; register char *pcRet; iLen = ((strlen(pcCred)+1+strlen(pcValue))|15)+1; if ((char *)0 == (pcRet = malloc(iLen))) fatal(acBadAlloc); snprintf(pcRet, iLen, "%s:%s", pcCred, pcValue); return pcRet; } /* Find the mnemonic based on the command-line arguments given. (ksb) * When both pam and password are set, must pass one of them (not both). */ static const char * Verify(const cmd_t *cmd, const int argc, const char **argv, const char **ppcFail) { register int iParam, i; register const char *pcPass, *pcScan, *pcAskPass, *pcPam; register struct passwd *pw; register struct group *gr; #if USE_GETSPNAM register struct spwd *spw; #endif auto char *pcField, *pcRest, *pcCarp; auto const char *pcUserMay; auto char acCvt[OP_MAX_LDWIDTH]; /* "#%ld" */ static char acDummy[] = "=."; pcUserMay = (const char *)0; *ppcFail = acBadAllow; #if OP_MAC_SUPPORT if ((char *)0 != pcMac && (char *)0 != (pcScan = FindOpt(cmd, "%m"))) { if (!GenMatch("-m", pcMac, pcScan)) return acMacDeny; } if ((char *)0 != pcMac && (char *)0 != (pcScan = FindOpt(cmd, "!m"))) { if (GenMatch("-m", pcMac, pcScan)) return acMacDeny; } #endif pcAskPass = FindOpt(cmd, acOptPass); if ((const char *)0 == (pcPam = FindOpt(cmd, acOptPam))) { /* nada */ } else if ('\000' == *pcPam) { /* allow a reset to no pam, when DEFAULT is pam auth */ pcPam = (const char *)0; } else { #if USE_PAM static char acPromptTempl[] = "%s credentials:"; auto struct pam_conv local_conv; auto int iStatus, iMem; auto pam_handle_t *pPCLocal; register char *pcPrompt; local_conv.appdata_ptr = (void *)cmd; local_conv.conv = OpConv; if ('.' == pcPam[0] && '\000' == pcPam[1]) { pcPam = acDefPam; } if (PAM_SUCCESS != (iStatus = pam_start(pcPam, pcPerp, &local_conv, &pPCLocal))) { fatal("pam_start: %s: %s", pcPam, pam_strerror(pPCLocal, errno)); } (void)pam_set_item(pPCLocal, PAM_TTY, acDevTty); (void)pam_set_item(pPCLocal, PAM_RHOST, acMyHost); (void)pam_set_item(pPCLocal, PAM_RUSER, pcPerp); iMem = (sizeof(acPromptTempl)+strlen(pcPam))|15; if ((char *)0 == (pcPrompt = calloc(iMem+1, sizeof(char)))) fatal(acBadAlloc); snprintf(pcPrompt, iMem, acPromptTempl, pcPam); (void)pam_set_item(pPCLocal, PAM_USER_PROMPT, pcPrompt); switch (iStatus = pam_authenticate(pPCLocal, 0)) { case PAM_SUCCESS: /* pam likes us, don't ask for a password either */ pcPam = pcAskPass = (const char *)0; break; case PAM_AUTH_ERR: case PAM_CONV_ERR: case PAM_CRED_INSUFFICIENT: case PAM_MAXTRIES: case PAM_PERM_DENIED: /* check password below */ break; default: /* PAM_{AUTHINFO_UNAVAIL,ABORT,BUF_ERR,SERVICE_ERR} * PAM_{SYMBOL_ERR,SYSTEM_ERR,USER_UNKNOWN}: */ fatal("%s: %s: %s", acOptPam, pcPam, pam_strerror(pPCLocal, iStatus)); /*NOTREACHED*/ } if (PAM_SUCCESS != (iStatus = pam_end(pPCLocal, iStatus))) { fatal("pam_end: %s: %s", pcPam, pam_strerror(pPCLocal, iStatus)); } #else fprintf(stderr, "%s: %s: %s: no support on this host\n", Progname, acOptPam, pcPam); #endif } /* Must know a password on one of these logins, default to ours */ if ((const char *)0 != pcAskPass) { register const char *pcIttr; auto struct stat stPass; extern char *crypt(); if ((char *)0 == (pcPass = getpass("Password:"))) { return "No password available"; } if ('\000' == *pcAskPass) { pcAskPass = acMyself; } pcScan = pcAskPass; while ((char *)0 != (pcScan = GetField(pcScan, &pcField))) { static const char acNoDashF[] = "No -f specified"; register char *pcSlash; register const char *pcStatMe; if (IsMyself(pcField) || '\000' == *pcField) { pcIttr = pcPerp; } else if (0 == strcmp(pcField, acSpecD)) { if ((char *)0 == pcSecFile) { *ppcFail = acNoDashF; continue; } /* -f /dev/ -> strip off dev/ for %d */ if ((char *)0 != (pcSlash = strrchr(pcSecFile, '/')) && '\000' == pcSlash[1]) { while (pcSlash > pcSecFile) { if ('/' != pcSlash[-1]) break; --pcSlash; } while (pcSlash > pcSecFile) { if ('/' == pcSlash[-1]) break; --pcSlash; } } if ((char *)0 == pcSlash) { pcStatMe = acCurDir; } else if (pcSecFile == pcSlash) { pcStatMe = acSlash; pcSlash = (char *)0; } else { *pcSlash = '\000'; pcStatMe = pcSecFile; } i = stat(pcStatMe, &stPass); if ((char *)0 != pcSlash) { *pcSlash = '/'; } if (-1 == i) { *ppcFail = "Cannot stat -f's directory for password lookup"; continue; } if ((struct passwd *)0 == (pw = getpwuid(stPass.st_uid))) { *ppcFail = "-f owner unmapped for password check"; continue; } /* small memory leak here, we don't care */ if ((char *)0 == (pcIttr = strdup(pw->pw_name))) { pcIttr = pw->pw_name; } } else if (0 != strcmp(pcField, acSpecF)) { if ((char *)0 == pcSecFile) { *ppcFail = acNoDashF; continue; } if (-1 == stat(pcSecFile, &stPass)) { *ppcFail = "Cannot stat -f target for password lookup"; continue; } if ((struct passwd *)0 == (pw = getpwuid(stPass.st_uid))) { *ppcFail = "-f owner unmapped for password check"; continue; } /* small memory leak here */ if ((char *)0 == (pcIttr = strdup(pw->pw_name))) pcIttr = pw->pw_name; } else if (0 != strcmp(pcField, acSpecU)) { pcIttr = pcScan; } else if ((char *)0 == (pcIttr = pcUid)) { *ppcFail = "No -u specified"; continue; } #if USE_GETSPNAM if ((struct spwd *)0 == (spw = getspnam(pcIttr))) continue; #else if ((struct passwd *)0 == (pw = getpwnam(pcIttr))) continue; #endif #if USE_GETSPNAM if (0 != strcmp(crypt(pcPass, spw->sp_pwdp), spw->sp_pwdp)) continue; #else if (0 != strcmp(crypt(pcPass, pw->pw_passwd), pw->pw_passwd)) continue; #endif break; } EndField(); if ((char *)0 == pcScan) { return (const char *)0; } pcPam = (const char *)0; } if ((char *)0 != pcPam) { return (const char *)0; } /* I don't think we ever should allow running as an unmapped uid. * But if your primary login group is unmapped we'll call it ":", * which can't really be a group, but you could match in a rule. * Why you'd want a rule to allow an unmapped primary group, I don't * really know. Be liberal with what we allow in the configuration. */ if ((struct passwd *)0 == (pw = getpwuid(getuid()))) { return (const char *)0; } pcCarp = (char *)0; if ((const char *)0 == pcUserMay && (char *)0 != (pcScan = FindOpt(cmd, acOptGroups))) { if ((struct group *)0 == (gr = getgrgid(pw->pw_gid))) { if (AnyMatch(pcScan, ":", pw->pw_gid, &pcCarp)) { snprintf(acCvt, sizeof(acCvt), "#%ld", (long)pw->pw_gid); pcUserMay = KeepMay(acOptGroups, acCvt); } } else if (AnyMatch(pcScan, gr->gr_name, gr->gr_gid, &pcCarp)) { pcUserMay = KeepMay(acOptGroups, gr->gr_name); } setgrent(); while ((const char *)0 == pcUserMay && (struct group *)0 != (gr = getgrent())) { for (i = 0; (char *)0 != gr->gr_mem[i]; ++i) { if (0 == strcmp(gr->gr_mem[i], pw->pw_name)) break; } if (((char *)0 != gr->gr_mem[i]) && AnyMatch(pcScan, gr->gr_name, gr->gr_gid, &pcCarp)) { pcUserMay = KeepMay(acOptGroups, gr->gr_name); break; } } } if ((const char *)0 == pcUserMay && ((char *)0 != (pcScan = FindOpt(cmd, acOptUsers)))) { if (AnyMatch(pcScan, pw->pw_name, pw->pw_uid, &pcCarp)) pcUserMay = KeepMay(acOptUsers, pw->pw_name); } if ((char *)0 != pcCarp) { syslog(LOG_ERR, "%s: %s:%d: Verify: regcomp: %s (run -S)", cmd->pcname, cmd->pcfile, cmd->iline, pcCarp); } if ((const char *)0 == pcUserMay && ((char *)0 != (pcScan = FindOpt(cmd, acOptNetgroups)))) { auto char acMyName[512]; if (-1 == gethostname(acMyName, sizeof(acMyName))) { fatal("gethostname: %s", strerror(errno)); } while ((char *)0 != (pcScan = GetField(pcScan, &pcField))) { if (innetgr(pcField, acMyName, pw->pw_name, (char *)0)) { pcUserMay = KeepMay(acOptNetgroups, pcField); break; } } EndField(); } /* One might think that the superuser (real uid) might be able * to do anything -- they don't need op to do that! */ if ((const char *)0 == pcUserMay) { return (const char *)0; } *ppcFail = acOK; /* Look for $*, $[0-9]*, and repeat the check from Find. * $5 w/o an R.E. means must be non-empty (default RE of /./) */ for (i = 0; i < cmd->nopts; ++i) { pcScan = cmd->opts[i]; if ('$' != pcScan[0] && '!' != pcScan[0]) { continue; } /* look for $*=REs */ if ('*' == pcScan[1]) { /* must check in Go */ continue; } if (!isdigit(cmd->opts[i][1])) { continue; } /* Found $[0-9]+=REs, $0 is configuration error, dude. * I thought about using it... too unclear (match our name) * "$5" is the same as "$5=." (has at least 1 character) * "!5" is the same as $# < 5 */ if (0 == (iParam = strtoul(pcScan+1, & pcRest, 10))) { return acBadConfig; } if ('\000' == *pcRest) { /* set the deault and allow the termination code * to write over the '=' above, as if to end the * number in "$7=." for the tag */ pcRest = acDummy+1; } else if ('=' != *pcRest++) { return acBadConfig; } if (iParam >= argc) { if ('!' == pcScan[0] && acDummy+1 == pcRest) continue; return acBadFew; } pcRest[-1] = '\000'; if (! GenMatch(pcScan, argv[iParam], pcRest)) { pcRest[-1] = '='; if ('!' == pcScan[0]) continue; return acBadParam; } pcRest[-1] = '='; if ('!' == pcScan[0]) { return acBadParam; } } return pcUserMay; } /* Append the tail onto the dir to make an absolute path. (ksb) * I wish every OS had realpath. Sigh. * Inv.: pcDir points to at least MAXPATHLEN characters. */ static char * FullPath(char *pcDir, const char *pcTail) { register int iStrip, iLen, c; register char *pcSnip, *pcLast; auto char acLink[MAXPATHLEN+4]; if ('\000' == *pcTail) { return pcDir; } for (iStrip = 0; '.' == pcTail[0]; /* nada */) { if ('.' == pcTail[1] && ('\000' == pcTail[2] || '/' == pcTail[2])) ++iStrip, pcTail += 2; else if ('/' == pcTail[1] || '\000' == pcTail[1]) pcTail += 1; else break; while ('/' == *pcTail) ++pcTail; } while (iStrip-- > 0) { if ('\000' == pcDir[1]) break; if ((char *)0 == (pcSnip = strrchr(pcDir, '/'))) break; while ('/' == *pcSnip && pcDir != pcSnip) *pcSnip-- = '\000'; if (pcDir == pcSnip) pcDir[1] = '\000'; } iLen = strlen(pcDir); if (iLen+2 >= MAXPATHLEN) { /* out of space */ return (char *)0; } pcLast = pcDir+iLen; if ('/' != pcDir[iLen-1] && '\000' != *pcTail) { pcDir[iLen++] = '/'; } while ('\000' != (c = *pcTail++)) { if ('/' != c) { pcDir[iLen++] = c; if (iLen >= MAXPATHLEN) { /* out of space */ return (char *)0; } continue; } pcDir[iLen] = '\000'; while ('/' == *pcTail) ++pcTail; if (-1 != (iLen = readlink(pcDir, acLink, sizeof(acLink)-1))) { *pcLast = '\000'; acLink[iLen] = '\000'; if ('/' == acLink[0]) { pcDir[0] = '/'; pcDir[1] = '\000'; for (pcSnip = acLink; '/' == *pcSnip; ++pcSnip) /* nada */; pcDir = FullPath(pcDir, pcSnip); } else { pcDir = FullPath(pcDir, acLink); } if ((char *)0 == pcDir) { return (char *)0; } } return FullPath(pcDir, pcTail); } pcDir[iLen] = '\000'; return pcDir; } /* Convert a file mode into a type character. (ksb) * [-cbdpsl] like common ls, * n for a non-existant file, * D doors from Solaris, * P ports from Solaris/Xenix, * w white-outs, * I removed contiguous file support as I've not seen it in decades */ static char FileType(const struct stat *pstThis) { if ((struct stat *)0 == pstThis) return 'n'; #if defined(S_IFIFO) if (S_IFIFO == (pstThis->st_mode&S_IFMT)) return 'p'; #endif #if defined(S_IFDOOR) if (S_IFDOOR == (pstThis->st_mode&S_IFMT)) return 'D'; #endif #if defined(S_IFSOCK) if (S_IFSOCK == (pstThis->st_mode&S_IFMT)) return 's'; #endif #if defined(S_IFBLK) if (S_IFBLK == (pstThis->st_mode&S_IFMT)) return 'b'; #endif #if defined(S_IFCHR) if (S_IFCHR == (pstThis->st_mode&S_IFMT)) return 'c'; #endif #if defined(S_IFREG) if (S_IFREG == (pstThis->st_mode&S_IFMT)) return '-'; #endif #if defined(S_IFDIR) if (S_IFDIR == (pstThis->st_mode&S_IFMT)) return 'd'; #endif #if defined(S_IFLNK) if (S_IFLNK == (pstThis->st_mode&S_IFMT)) return 'l'; #endif #if defined(S_IFWHT) if (S_IFWHT == (pstThis->st_mode&S_IFMT)) return 'w'; #endif #if defined(S_IFPORT) if (S_IFPORT == (pstThis->st_mode&S_IFMT)) return 'P'; #endif return '-'; } /* Is the given directory a mount-point? (ksb) * In this case we know that the path is an absolute path to a dir, but we * are pretty general. On some OS's st_ino == 2 is a tell, on others * it is not :-(. We check ../ being a different st_dev, or being "/" * (even in a chroot op should think of slash as being a mount-point). * N.B.: We can write on pcPath, but we always put it back. * INV: strlen(pcPath) <= MAXPATHLEN */ static int IsMountPt(const char *pcPath, const struct stat *pstPath) { register char *pcTail; register int iKeep; auto char acAbove[MAXPATHLEN+8]; /* +"/..\000" */ auto struct stat stAbove; if ((struct stat *)0 == pstPath) { return 0; } while ('/' == pcPath[0] && '/' == pcPath[1]) { ++pcPath; } if ('/' == pcPath[0] && '\000' == pcPath[1]) { return 1; } if ((char *)0 == (pcTail = strrchr(pcPath, '/'))) { snprintf(acAbove, sizeof(acAbove), "%s/..", pcPath); if (-1 == stat(acAbove, &stAbove)) return 0; } else if (pcTail == pcPath) { if (-1 == stat(acSlash, &stAbove)) return 0; } else { while (pcTail > pcPath && '/' == pcTail[-1]) --pcTail; *pcTail = '\000'; iKeep = stat(pcPath, &stAbove); *pcTail = '/'; if (-1 == iKeep) return 0; } return stAbove.st_dev != pstPath->st_dev; } /* Return 1 if the directory is empty. (ksb) */ static int IsEmpty(const char *pcDir) { register struct dirent *pDE; register DIR *pDR; register int iRet; if ((DIR *)0 == (pDR = opendir(pcDir))) { return 0; } iRet = 1; while ((struct dirent *)0 != (pDE = readdir(pDR))) { if ('.' != pDE->d_name[0]) /* found one */; else if ('\000' == pDE->d_name[1]) continue; else if ('.' != pDE->d_name[1]) /* found a hidden file */; else if ('\000' == pDE->d_name[2]) continue; iRet = 0; break; } closedir(pDR); return iRet; } /* Make the "type" string for This stat pointer (ksb) * pcHere must have at least 5 char's avaliable worst-case ("ldme\000"). */ char * MakeFileType(const char *pcFull, char *pcHere, size_t wLen, const struct stat *pstThis) { auto struct stat stIndir; snprintf(pcHere, wLen, "%c", FileType(pstThis)); /* add the type of the other end on a 'l' (symbolic link) */ if ('l' == pcHere[0]) { if (-1 == stat(pcFull, &stIndir)) { pcHere[1] = FileType((struct stat *)0); } else { pcHere[1] = FileType(& stIndir); } pcHere[2] = '\000'; } if ('d' == pcHere[0] && IsMountPt(pcFull, pstThis)) { strncat(pcHere, "m", wLen); } else if ('d' == pcHere[1] && IsMountPt(pcFull, &stIndir)) { strncat(pcHere, "m", wLen); } if (('d' == pcHere[0] || 'd' == pcHere[1]) && IsEmpty(pcFull)) { strncat(pcHere, "e", wLen); } return pcHere; } /* Copied from install's special.c, which I wrote. (ksb) */ static void EBit(char *pcBit, const int bSet, const int cSet, const int cNotSet) /* pcBit pointer the the alpha bit */ /* bSet should it set over struck */ /* cSet overstrike with this */ /* cNotSet it should have been this, else upper case */ { if (0 != bSet) { *pcBit = (cNotSet == *pcBit) ? cSet : toupper(cSet); } } /* Process the R.E. magic for a %f.$attr expression (or !f.$attr). (ksb) * The largest text attribute a file can have is the path (MAXPATHLEN). * When changing asFKnown add code here to match the new member. */ static int FileAttr(const char *pcFull, const struct stat *pstThis, const char *pcRE, const char *pcAttr) { register struct passwd *pw; register struct group *gr; register int i, iBit; auto char acTarget[MAXPATHLEN+256]; static const char acOwners[] = "owners"; if (0 == strcmp(pcAttr, "path")) { snprintf(acTarget, sizeof(acTarget), "%s", pcFull); } else if (0 == strcmp(pcAttr, "perms")) { /* copied from my install code -- the ls(1) symbolic mode */ (void)strcpy(acTarget, "-rwxrwxrwx"); acTarget[0] = FileType(pstThis); for (i = 0, iBit = 0400; i < 9; ++i, iBit >>= 1) { if ((struct stat *)0 == pstThis || 0 == (pstThis->st_mode & iBit)) acTarget[i+1] = '-'; } /* Sun changes plain files group 'x' to 'l' -- but we don't * it would just make it even harder to RE match -- ksb */ if ((struct stat *)0 != pstThis) { EBit(acTarget+3, S_ISUID & pstThis->st_mode, 's', 'x'); EBit(acTarget+6, S_ISGID & pstThis->st_mode, 's', 'x'); EBit(acTarget+9, S_ISVTX & pstThis->st_mode, 't', 'x'); } } else if (0 == strcmp(pcAttr, "type")) { MakeFileType(pcFull, acTarget, sizeof(acTarget), pstThis); } else if (0 == strcmp(pcAttr, "access")) { (void)strcpy(acTarget, "rwxf"); if (0 != access(pcFull, R_OK)) acTarget[0] = '-'; if (0 != access(pcFull, W_OK)) acTarget[1] = '-'; if ((struct stat *)0 == pstThis || 0 == (0111 & pstThis->st_mode) || 0 != access(pcFull, X_OK)) acTarget[2] = '-'; if (0 != access(pcFull, F_OK)) acTarget[2] = '-'; } else if ((struct stat *)0 == pstThis) { fatal("%s: %s", pcFull, strerror(ENOENT)); } else if (0 == strcmp(pcAttr, "dev")) { snprintf(acTarget, sizeof(acTarget), "%ld", (long)pstThis->st_dev); } else if (0 == strcmp(pcAttr, "ino")) { snprintf(acTarget, sizeof(acTarget), "%ld", (long)pstThis->st_ino); } else if (0 == strcmp(pcAttr, "nlink")) { snprintf(acTarget, sizeof(acTarget), "%ld", (long)pstThis->st_nlink); } else if (0 == strcmp(pcAttr, "atime")) { snprintf(acTarget, sizeof(acTarget), "%ld", (long)pstThis->st_atime); } else if (0 == strcmp(pcAttr, "mtime")) { snprintf(acTarget, sizeof(acTarget), "%ld", (long)pstThis->st_mtime); } else if (0 == strcmp(pcAttr, "ctime")) { snprintf(acTarget, sizeof(acTarget), "%ld", (long)pstThis->st_ctime); } else if (0 == strcmp(pcAttr, "birthtime") || 0 == strcmp(pcAttr, "btime")) { #if HAVE_BIRTHTIME snprintf(acTarget, sizeof(acTarget), "%ld", (long)pstThis->st_birthtime); #else snprintf(acTarget, sizeof(acTarget), "%ld", (long)pstThis->st_mtime); #endif } else if (0 == strcmp(pcAttr, "size")) { snprintf(acTarget, sizeof(acTarget), "%ld", (long)pstThis->st_size); } else if (0 == strcmp(pcAttr, "blksize")) { snprintf(acTarget, sizeof(acTarget), "%ld", (long)pstThis->st_blksize); } else if (0 == strcmp(pcAttr, "blocks")) { snprintf(acTarget, sizeof(acTarget), "%ld", (long)pstThis->st_blocks); } else if (0 == strcmp(pcAttr, "uid")) { snprintf(acTarget, sizeof(acTarget), "%ld", (long)pstThis->st_uid); } else if (0 == strcmp(pcAttr, "login@g")) { if ((struct passwd *)0 == (pw = getpwuid(pstThis->st_uid))) fatal("getpwuid: %ld: no such login", (long)pstThis->st_uid); return MemberMatch(pcRE, pw->pw_name, & pw->pw_gid, "login@g"); } else if (0 == strcmp(pcAttr, "login") || 0 == strcmp(pcAttr, acOwners)) { if ((struct passwd *)0 == (pw = getpwuid(pstThis->st_uid))) fatal("getpwuid: %ld: no such login", (long)pstThis->st_uid); snprintf(acTarget, sizeof(acTarget), "%s", pw->pw_name); if (0 == strcmp(pcAttr, acOwners)) { strncat(acTarget, ":", sizeof(acTarget)); if ((struct group *)0 == (gr = getgrgid(pstThis->st_gid))) fatal("getgrgid: %ld: no such group", (long)pstThis->st_gid); strncat(acTarget, gr->gr_name, sizeof(acTarget)); } } else if (0 == strcmp(pcAttr, acOptGid)) { snprintf(acTarget, sizeof(acTarget), "%ld", (long)pstThis->st_gid); } else if (0 == strcmp(pcAttr, "group")) { if ((struct group *)0 == (gr = getgrgid(pstThis->st_gid))) fatal("getgrgid: %ld: no such group", (long)pstThis->st_gid); snprintf(acTarget, sizeof(acTarget), "%s", gr->gr_name); } else if (0 == strcmp(pcAttr, "mode")) { snprintf(acTarget, sizeof(acTarget), "%04lo", (long)(07777&pstThis->st_mode)); } else { fatal("%%f.%s: unknown attribute", pcAttr); } return GenMatch(pcAttr, acTarget, pcRE); } /* Check %{ENV} and !{ENV} patterns against the proposed environ (ksb) * Find any options that look like envronment limits and check them. */ static int CheckEnv(const cmd_t *cmd, const char **ppcNewEnv) { register int i, iLen, fMatch; register const char *pc, *pcRead, **ppcScan, *pcCCurly, *pcEnv; for (i= 0; i< cmd->nopts; ++i) { pcRead = cmd->opts[i]; if (('%' != pcRead[0] && '!' != pcRead[0]) || '{' != pcRead[1]) continue; if ((char *)0 == (pcCCurly = strchr(pcRead, '}'))) fatal("configuration error in environment limit"); pcEnv = pcRead+2; iLen = pcCCurly - pcEnv; if ('\000' != *++pcCCurly) ; else if ('!' == pcRead[0]) pcCCurly = acDefNotEnv; else continue; /* no default positive env RE match */ ppcScan = ppcNewEnv; while ((char *)0 != (pc = *ppcScan++)) { if (0 != strncmp(pcEnv, pc, iLen) || '=' != pc[iLen]) continue; fMatch = GenMatch("environment limit", pc+iLen+1, pcCCurly); if (('%' == *pcRead) == (0 != fMatch)) continue; return 1; } } return 0; } /* Approve the direct file and return the full-path to it. (ksb) * By the way: -f "" == -f `pwd`. */ static char * DirectFile(const cmd_t *cmd, const char *pcOrig, struct stat *pstThis) { register char *pcLim, *pcAttr, *pcFull, *pcThis; register int fRemove, iLen, iCheck; register const char *pcRead; auto struct stat *pstD, stFull; auto char acLook[MAXPATHLEN+256+4]; auto char acLead[4]; auto char acTail[256+4]; static char acFPerms[] = "-f: Permission denied"; /* Remove trailing slashes from the input filename, which is not NULL * also remove multiple leading slashes for remove checks below. */ if ((const char *)0 == pcOrig) { /* premission denied error in caller */ return (char *)0; } while ('/' == pcOrig[0]) { if ('.' == pcOrig[1] && '.' == pcOrig[2] && '/' == pcOrig[3]) pcOrig += 3; /* "/../" -> "/" */ else if ('.' == pcOrig[1] && '/' == pcOrig[2]) pcOrig += 2; /* "/./" -> "/" */ else if ('/' == pcOrig[1]) pcOrig += 1; /* "//" -> "/" */ else break; } if ((char *)0 == (pcThis = strdup(pcOrig))) fatal("-f %s", acBadAlloc); while ((char *)0 != (pcLim = strrchr(pcThis, '/')) && pcLim > pcThis && '\000' == pcLim[1]) { *pcLim = '\000'; } /* When the file doesn't exist, or is an allowed symbolic link * compress the path w/o the last component. * When they OK a symbolic link (or mandaite it) then we allow it, * otherwise map it to the contents of the link */ acTail[0] = '\000'; fRemove = 0; /* 0 = no, 1 = was_tail_link, 2 = no_such_file */ if (-1 == lstat(pcThis, pstThis)) { fRemove = 2; } else if (S_ISLNK(pstThis->st_mode)) { auto char acTempType[8]; MakeFileType(pcOrig, acTempType, sizeof(acTempType), pstThis); fRemove = 1; if ((char *)0 != (pcRead = FindOpt(cmd, "%f.type"))) { fRemove = !GenMatch("type", acTempType, pcRead); } if ((char *)0 != (pcRead = FindOpt(cmd, "!f.type")) && GenMatch("type", acTempType, pcRead)) { fRemove = 1; } } if (0 != fRemove) { if ((char *)0 == pcLim) { (void)strncpy(acTail, pcThis, sizeof(acTail)); strcpy(acLead, acCurDir); pcThis = acLead; } else if (pcLim == pcThis) { (void)strncpy(acTail, pcThis+1, sizeof(acTail)); acLead[0] = pcThis[0]; acLead[1] = '\000'; pcThis = acLead; } else { (void)strncpy(acTail, pcLim+1, sizeof(acTail)); do { *pcLim-- = '\000'; } while (pcThis < pcLim && '/' == *pcLim); } } if ('/' == pcThis[0]) { acLook[0] = '/'; acLook[1] = '\000'; while ('/' == *pcThis) ++pcThis; } else if ((char *)0 == getcwd(acLook, sizeof(acLook))) { fatal("getcwd: %s", strerror(errno)); } if ((char *)0 == (pcLim = FullPath(acLook, pcThis))) { fatal("$f: no such path"); } /* Put the part we took off back on then end, if we did that */ if (0 != fRemove) { iLen = strlen(pcLim); if (strlen(acTail)+1+iLen > sizeof(acLook)) fatal("-f resolved path too long"); /* checked above, don't need any strncat check below */ if ('/' != pcLim[iLen-1]) { (void)strcat(pcLim, acSlash); } (void)strcat(pcLim, acTail); } iCheck = lstat(pcLim, & stFull); switch (fRemove) { case 0: /* must be the same node */ if (-1 == iCheck) { fatal("-f: fullpath doesn't find any file at %s", pcLim); } if (stFull.st_dev != pstThis->st_dev || stFull.st_ino != pstThis->st_ino) { fatal("-f: %s is not the same file %s", pcThis, pcLim); } break; case 1: /* must be the same symbilic link */ if (!S_ISLNK(stFull.st_mode)) { fatal("-f: %s: is not a symbolic link", pcLim); } if (stFull.st_dev != pstThis->st_dev || stFull.st_ino != pstThis->st_ino) { fatal("-f: %s is not the same symbolic link %s", pcThis, pcLim); } break; case 2: /* should still not exist */ if (-1 != iCheck) { fatal("-f: missing file race for %s", pcLim); } pstThis = (struct stat *)0; break; } if ((char *)0 == (pcFull = strdup(pcLim))) fatal("-f %s", acBadAlloc); /* When the config provides %f=RE we assume it means %f.path, which * is the first one in the asFKnown list. */ if ((char *)0 != (pcLim = FindOpt(cmd, acSpecF))) { if (!FileAttr(pcFull, pstThis, pcLim, asFKnown)) fatal(acFPerms); } if ((char *)0 != (pcLim = FindOpt(cmd, "!f"))) { if (FileAttr(pcFull, pstThis, pcLim, asFKnown)) fatal(acFPerms); } for (pcAttr = asFKnown; '\000' != *pcAttr; pcAttr += strlen(pcAttr)+1) { snprintf(acLook, sizeof(acLook), "%%f.%s", pcAttr); if ((char *)0 != (pcLim = FindOpt(cmd, acLook))) { if (!FileAttr(pcFull, pstThis, pcLim, pcAttr)) fatal(acFPerms); } snprintf(acLook, sizeof(acLook), "!f.%s", pcAttr); if ((char *)0 != (pcLim = FindOpt(cmd, acLook))) { if (FileAttr(pcFull, pstThis, pcLim, pcAttr)) fatal(acFPerms); } } /* OK, check %d as well. The allows a check of dir perms, which * is the same pattern as the checks above. */ pcThis = strrchr(pcFull, '/'); if (pcFull == pcThis) { pcRead = acSlash; pcThis = (char *)0; } else { *pcThis = '\000'; } pstD = &stFull; if (-1 == lstat(pcFull, & stFull)) { pstD = (struct stat *)0; } if ((char *)0 != (pcLim = FindOpt(cmd, acSpecD))) { if (!FileAttr(pcFull, pstD, pcLim, asFKnown)) fatal(acFPerms); } if ((char *)0 != (pcLim = FindOpt(cmd, "!d"))) { if (FileAttr(pcFull, pstD, pcLim, asFKnown)) fatal(acFPerms); } for (pcAttr = asFKnown; '\000' != *pcAttr; pcAttr += strlen(pcAttr)+1) { snprintf(acLook, sizeof(acLook), "%%d.%s", pcAttr); if ((char *)0 != (pcLim = FindOpt(cmd, acLook))) { if (!FileAttr(pcFull, pstD, pcLim, pcAttr)) fatal(acFPerms); } snprintf(acLook, sizeof(acLook), "!d.%s", pcAttr); if ((char *)0 != (pcLim = FindOpt(cmd, acLook))) { if (FileAttr(pcFull, pstD, pcLim, pcAttr)) fatal(acFPerms); } } if ((char *)0 != pcThis) { *pcThis = '/'; } return pcFull; } /* Allow the configuration to specfiy a forced input/output fd. (ksb) * Accpet: file <>file >>file * as: RDONLY, WRONLY, RDWR, RW|O_APPEND (like the shell) * * We do _NOT_ create files. That's the point. The empty file is /dev/null. * We will try to connect to a socket, for wrapper logic. How nice. * If you want to create the file with the correct modes, etc, use * a separate op rule with install(1) -c /dev/null, or installus. You * could even put mk(1) markup in the config file to install the file * when the configuration file is installed. How cool is that? */ static int PrivOpen(const cmd_t *cmd, const char *pcRedirExpected, const char *pcSpec, const char *pcSecPath) { register const char *pcMode; register int iOpts, iRet, iLen; auto struct sockaddr_un run; if ('<' == *pcSpec || '>' == *pcSpec) { pcMode = pcSpec; if ('>' == *++pcSpec) ++pcSpec; } else { pcMode = pcRedirExpected; } iOpts = '<' == pcMode[0] ? ('>' == pcMode[1] ? O_RDWR : O_RDONLY) : ('>' == pcMode[0] ? ('>' == pcMode[1] ? (O_APPEND|O_WRONLY) : O_WRONLY|O_TRUNC) : O_RDONLY); if (0 == strcmp(acSpecD, pcSpec)) { syslog(LOG_ERR, "%s: %s:%d: redirection to/from a directory (via %s)", cmd->pcname, cmd->pcfile, cmd->iline, acSpecD); fatal("%s: redirection to a directory forbidden (use $D for a readonly descriptor", cmd->pcname); } else if (0 != strcmp(acSpecF, pcSpec)) /* fine, we have it */; else if ((char *)0 == (pcSpec = pcSecPath)) fatal("%s: redirection requires -f", cmd->pcname); if ('\000' == *pcSpec) { pcSpec = acDevNull; if (O_RDONLY == iOpts) iOpts = O_RDWR; } if (-1 != (iRet = open(pcSpec, iOpts, 0600))) { return iRet; } if (EOPNOTSUPP != errno) { fatal("%s: open: %s: %s", cmd->pcname, pcSpec, strerror(errno)); } if ((iLen = strlen(pcSpec)) >= sizeof(run.sun_path)) { fatal("%s: connect: %s", cmd->pcname, strerror(ENAMETOOLONG)); } if (-1 == (iRet = socket(AF_UNIX, SOCK_STREAM, 0))) { fatal("%s: socket: %s", cmd->pcname, strerror(errno)); } #if HAVE_SUN_LEN run.sun_len = iLen; ++iLen; #endif run.sun_family = AF_UNIX; (void)strncpy(run.sun_path, pcSpec, sizeof(run.sun_path)); if (-1 == connect(iRet, (struct sockaddr *)&run, iLen+sizeof(run.sun_family))) { fatal("%s: connect: %s: %s", cmd->pcname, pcSpec, strerror(errno)); } /* We could shutdown the socket to honor the redir, nah. */ return iRet; } /* Find the right slot the current value, and the current list. (ksb) * The list holds the max count hidden in ppcList[-1], since the environment * before the first slot is never used by anyone else :-). We find a proposed * value from the OldEnv (if we have one) then see is we can overwrite an * old value in the list, else we add it on the end. * * Init the list to a (const char **)0, then call me to add elements. */ static void AddVec(const char ***pppcList, const char *pcEnv, char **ppcOldEnv, int fUniq) { #define DEF_ENV_SLOT 768 /* cannot be 0 */ #define ADD_ENV_SLOTS 256 /* how many to add as we run out */ register int j, iMax, iWas, iFound; register size_t iLen; register char const **ppcSetEnv, *pcUserEnv, *pcEq; register char **ppcMem; if ((const char **)0 == (ppcSetEnv = *pppcList)) { iWas = 0; iMax = DEF_ENV_SLOT; ppcMem = (char **)calloc(iMax+1, sizeof(char *)); if ((char **)0 == ppcMem) fatal(acBadAlloc); if ((char *)0 == (*ppcMem = (char *)malloc(OP_MAX_LDWIDTH))) fatal(acBadAlloc); snprintf(ppcMem[0], OP_MAX_LDWIDTH, "%d", iMax); ppcSetEnv = (const char **)(ppcMem+1); } else { iWas = iMax = strtol(ppcSetEnv[-1], (char **)0, 0); } if ((const char *)0 == pcEnv) { iLen = 0; } else if ((const char *)0 != (pcEq = strchr(pcEnv, '='))) { iLen = pcEq-pcEnv; } else { iLen = strlen(pcEnv); } for (iFound = 0; (const char *)0 != pcEnv && (const char *)0 != (pcEq = ppcSetEnv[iFound]); ++iFound) { if (fUniq && 0 == strncmp(pcEq, pcEnv, iLen) && ('\000' == pcEq[iLen] || '=' == pcEq[iLen])) break; } /* If we didn't have a value given for the variable, look for * one from the OldEnv, if there is not one clear the request. */ if ((const char *)0 == pcEnv) { /* trying to put the (char *)0 on the end, I guess */ } else if (fUniq && '\000' == pcEnv[iLen]) { pcUserEnv = (const char *)0; for (j = 0; (char **)0 != ppcOldEnv && (const char *)0 != (pcUserEnv = ppcOldEnv[j]); ++j) { if (0 == strncmp(pcUserEnv, pcEnv, iLen) && '=' == pcUserEnv[iLen]) break; } pcEnv = pcUserEnv; } if ((const char *)0 == pcEnv) { /* nothing */ } else if ((const char *)0 != ppcSetEnv[iFound]) { ppcSetEnv[iFound] = pcEnv; } else { if (iFound == iMax-1) { iMax += ADD_ENV_SLOTS; ppcMem = (char **)realloc((void *)(ppcSetEnv-1), sizeof(char *)*iMax); if ((char **)0 == ppcMem) fatal(acBadAlloc); snprintf(ppcMem[0], OP_MAX_LDWIDTH, "%d", iMax); ppcSetEnv = (const char **)(ppcMem+1); for (j = iWas; j < iMax; ++j) { ppcSetEnv[j] = (const char *)0; } } ppcSetEnv[iFound] = pcEnv; } *pppcList = ppcSetEnv; #undef DEF_ENV_SLOT #undef ADD_ENV_SLOTS } /* Use the same code to build argv and environ */ #define AddEnv(Mpppc, MpcEnv, MppcOldEnv) AddVec((Mpppc), (MpcEnv), (MppcOldEnv), 1) #define AddArg(Mpppc, MpcArg) AddVec((Mpppc), (MpcArg), (char **)0, 0) /* Delete instances of an environment varible. (ksb) * Works on the same structure as the AddEnv above. */ static void RmEnv(const char ***pppcList, char *pcEnv) { register const char **ppcRmEnv, *pcCur, *pcEq; register size_t iLen; register int j, iFold; if ((const char **)0 == (ppcRmEnv = *pppcList) || (char *)0 == pcEnv) { return; } if ((char *)0 != (pcEq = strchr(pcEnv, '='))) { iLen = pcEq-pcEnv; } else { iLen = strlen(pcEnv); } for (iFold = j = 0; (char *)0 != (pcCur = ppcRmEnv[j]); ++j) { if (0 == strncmp(pcCur, pcEnv, iLen) && '=' == pcCur[iLen]) continue; ppcRmEnv[iFold++] = pcCur; } ppcRmEnv[iFold] = (char *)0; } /* Reveal environment variables hidden by a prefix so "foo_NAME=agt" (ksb) * becomes "NAME=agt" with a token of "foo_". The name "foo_" goes * away, of course. Works on the same structure as above. */ static void RevealEnv(const char ***pppcList, const char *pcToken) { register const char **ppcFixEnv, *pcCur; register size_t iLen; register int j, iKeep; if ((const char **)0 == (ppcFixEnv = *pppcList) || (char *)0 == pcToken) { return; } iLen = strlen(pcToken); for (iKeep = j = 0; (char *)0 != (pcCur = ppcFixEnv[j]); ++j) { if (0 != strncmp(pcCur, pcToken, iLen)) { ppcFixEnv[iKeep++] = pcCur; continue; } /* delete the whole prefix name */ if ('\000' == pcCur[iLen] || '=' == pcCur[iLen]) { continue; } ppcFixEnv[iKeep++] = pcCur+iLen; } ppcFixEnv[iKeep] = (char *)0; } /* Add or remove an environment variable based on the the output from (ksb) * a helmet/jacket process. Commands are whole lines: * #comment a comment * "cmd" C quote command with embedded newlines (see below) * -VAR remove this from the environment * $VAR=value set this in the environment * $VAR copy this from the old environment * ~prefix remove prefix from any matching environment vars * [0-9]+ exit code for failure * .* why we don't love you, fail for me * &0 uLines) { fprintf(stderr, "%s: %s: incomplete quoted external command\n", Progname, cmd->pcname); return EX_IOERR; } if ('\000' == *pcSrc) pcSrc[0] = '\n'; else pcSrc[1] = '\n'; } if ('\\' != pcSrc[0]) { *pcDst++ = *pcSrc++; continue; } switch (*++pcSrc) { /* \n, \", \^M \o \\ */ case '\n': /* \^M is the empty string in sh and C */ break; case 'a': *pcDst++ = '\007'; break; case 'b': *pcDst++ = '\b'; break; case 'd': *pcDst++ = '"'; break; /* op special */ case 'f': *pcDst++ = '\f'; break; case 'n': *pcDst++ = '\n'; break; case 'o': *pcDst++ = '`'; break; /* op special */ case 'q': *pcDst++ = '\''; break; /* op special */ case 'r': *pcDst++ = '\r'; break; case 's': *pcDst++ = ' '; break; case 't': *pcDst++ = '\t'; break; case 'v': *pcDst++ = '\v'; break; default: /* includes \\ */ *pcDst++ = *pcSrc; break; } ++pcSrc; } while (isspace(*pcLine)) ++pcLine; if ('"' == *pcLine) { /* we found a C string, processed */ pcQuote = pcLine--; pcDst = pcSrc = pcQuote+1; goto retry; } skip: #if DEBUG > 1 fprintf(stderr, "external input: `%s'\n", pcScan); #endif switch (*pcScan) { case '\000': /* take a blank line, why not? */ break; case '#': #if DEBUG fprintf(stderr, "%s: %s\n", pcFrom, pcScan); #endif break; case '&': /* jacket fd manipulation */ /* '&' (0|1|2) [privopen-args] * '&' 3|4|5... close all above this fd */ iFd = strtol(pcScan+1, (char **)&pcEol, 0); if ('\000' == *pcEol) pcEol = acDevNull; if (iFd < 3) piFds[iFd] = PrivOpen(cmd, 0 == iFd ? "<" : ">", pcEol, pcSecPath); else if (-1 == piFds[3] || iFd < piFds[3]) piFds[3] = iFd; break; case '$': AddEnv(pppcEnviron, pcScan+1, environ); break; case '-': RmEnv(pppcEnviron, pcScan+1); break; case '~': RevealEnv(pppcEnviron, pcScan+1); break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': iRet = strtol(pcScan, (char **)&pcEol, 0); if ('\000' == *pcEol) break; /*FALLTHROUGH*/ default: /* I guess it is misdirected errors? */ fprintf(stderr, "%s: %s: %s\n", Progname, pcFrom, pcScan); iRet = EX_PROTOCOL; break; } } return iRet; } /* The string in *ppcMem is split into a prefix and a suffix. Between (ksb) * them we need to build "$(prefix)$1$.$2$.$3...$(iArgc-1). */ static char * Replace(char **ppcMem, char *pcScan, int iStart, int iArgc) { register char *pcHad, *pcRet, *pcCursor; register const char *pcSep; register size_t wLen; static const char acDollarDot[] = "$."; auto char acCvt[OP_MAX_LDWIDTH]; /* "$%ld" */ /* Find the max field width for any positional parameter * That makes the space required about: * (pcScan-pcHad)-1 + (iArgc-iStart)*(strlen(acCvt)+2) + strlen(pcScan) * the +2 for each "$." separator. The total is + 4 chars too big, * worst case. */ pcHad = *ppcMem; snprintf(acCvt, sizeof(acCvt), "$%ld", (long)iArgc); wLen = (pcScan-pcHad)+(iArgc-iStart)*(strlen(acCvt)+2)+strlen(pcScan); wLen = (wLen|15)+(OP_MAX_LDWIDTH+1); if ((char *)0 == (pcRet = malloc(wLen))) { fatal(acBadAlloc); /*NOTREACHED*/ } pcScan[-1] = '\000'; ++pcScan; pcCursor = pcRet; pcSep = acDollarDot+(sizeof(acDollarDot)-1); for (strcpy(pcCursor, pcHad); iStart < iArgc; ++iStart) { strcat(pcRet, pcSep); snprintf(acCvt, sizeof(acCvt), "$%ld", (long)iStart); strcat(pcRet, acCvt); pcSep = acDollarDot; } /* stash the new dynamic buffer start, pick up at the suffix, and * free the old (shorter) buffer. */ *ppcMem = pcRet; pcRet += strlen(pcRet); strcpy(pcRet, pcScan); free(pcHad); return *ppcMem; } /* Find any word breaking expander that would make multiple words, (ksb) * return the new argv[0] expander template and update the template * for $_ in *ppcUnder (which is a mutable copy of the original argv[0]). * N.B. *ppcUnder must be malloc'd space, because we may realloc or free it. */ static char * FindUnder(const cmd_t *cmd, const NEEDS *pNE, const int iArgc, char **ppcUnder) { register int c; register char *pcScan, *pcD_; /* Replace all the expanders that make lists from argv: $@, $-, $+ */ pcScan = *ppcUnder; while ('\000' != (c = *pcScan++)) { if ('$' != c) continue; switch (c = *pcScan) { case '@': /* $n$.$n+1$.$n+2... used for filter lists*/ pcScan = Replace(ppcUnder, pcScan, pNE->lCmdParam+1, iArgc); continue; case '-': /* $0$.$1$.$2$.$3... used largely for {script} */ pcScan = Replace(ppcUnder, pcScan, 0, iArgc); continue; case ',': /* $1$.$2$.$3... used largely for {script} */ pcScan = Replace(ppcUnder, pcScan, 1, iArgc); continue; case '\\': if ('\000' != pcScan[1]) ++pcScan; /* $\$ mayhap */ /*FALLTHROUGH*/ case '_': /* do not have to replace, possible error below */ default: ++pcScan; continue; case '.': /* word break here, see loop below */ break; } break; } pcScan = *ppcUnder; while ('\000' != (c = *pcScan++)) { if ('$' != c) continue; switch (c = *pcScan) { case '_': /* that's Bad $_ inside itself */ syslog(LOG_ERR, "%s: %s:%d uses $_ as part of itself", cmd->pcname, cmd->pcfile, cmd->iline); fatal("$_ may not be used as part of itself"); /*NOTREACHED*/ case '.': /* a cut point! Yay! */ /* "bar$l$." => $_ = foo$l , argv[0] = $_, if you * Force an empty string as s/[$][.]$/$.$|/1, friend. */ if ('\000' == pcScan[1]) { pcScan[-1] = '\000'; return "$_"; } /* "foo$1$.$2" => $_ = foo$1 , argv[0] = $_$.$2 * If (strlen(foo) < 1) we'd make buffer bigger, * but the called added OP_MAX_LDWIDTH for us. */ pcScan[-1] = '\000'; if ((char *)0 == (pcD_ = strdup(*ppcUnder))) fatal(acBadAlloc); *--pcScan = '$'; memmove((*ppcUnder)+2, pcScan, strlen(pcScan)+1); pcScan = *ppcUnder; pcScan[0] = '$'; pcScan[1] = '_'; *ppcUnder = pcD_; return pcScan; case '\\': if ('\000' != pcScan[1]) ++pcScan; /* $\$ mayhap */ break; default: break; } ++pcScan; } /* no breaks, this is all 1 word, return it as-is */ return "$_"; } /* return the strdup of the login's homedir path (ksb) */ static const char * HomeDir(const char *pcLogin) { register struct passwd *pw; register const char *pcRet; if ((struct passwd *)0 == (pw = getpwnam(pcLogin))) fatal("%s: no such login", pcLogin); if ((const char *)0 == (pcRet = strdup(pw->pw_dir))) fatal(acBadAlloc); return pcRet; } /* Expand command $_, $1, $2.., $*, $i , $u -u username, * $U (-u uid), $g (-g groupname), $G (-g uid), $$, $0, $f/$F, $| (empty string) * then execute the command (overlay this program with it). This function * is broken up into blocks that share a common set of local variables. */ static int Go(cmd_t *cmd, int argc, const char **argv, const char *pcMay) { extern char **environ; register char *pcLook; register const char *pcRead; register int i, j, iHeTail; auto const char *pcSetGroups; auto int iSecFd, iDirFd, iNewGroups, iOrigAccess, aiNewIO[4]; auto int iOrigPpid, iOrigMask; auto const char *pcExecPath, *pcSecPath, *pcSecDir; auto const char *pcSetSession, *pcEndSession, *pcNewMac; auto const char **ppcNewArgs, **ppcNewEnv, **ppcHelmet; auto const char *pcNewDir, *pcNewRoot; auto uid_t uReal, uEffective, uOrig, uPriv; auto gid_t gReal, gEffective, gOrig, gPriv; auto const char *pcNewHome; auto gid_t agNewList[OP_GIDLIST_MAX], agOrig[OP_GIDLIST_MAX]; auto struct stat stSecStat, stSecDirStat; auto NEEDS Need; static char *apcMagic[4] = { "$S", "-c", "$*", (char *)0 }; if (0 != Craving(cmd, & Need)) { exit(EX_DATAERR); } if (argc < Need.lCmdParam+1) { fatal(acBadNumber); } /* The max helmet argv is: * jacket-path -P # 2 * pid|helmet-path # + 1 = 3 * -u user -g group -f file # + 6 = 9 * -R root -C $cf # + 4 = 13 * -- # + 1 = 14 * $mnemonic $program $euid:$egid $cred_type:$cred # + 4 = 19 * (char *)0 # + 1 = 20 * so I allocated 48. */ iOrigPpid = getppid(); pcNewMac = (char *)0; ppcNewEnv = ppcNewArgs = (const char **)0; ppcHelmet = (const char **)0; iHeTail = 0; pcLook = (char *)0; if ((char *)0 != (pcRead = FindOpt(cmd, acOptHelmet)) || ((char *)0 != (pcLook = FindOpt(cmd, acOptJacket)))) { if ((const char **)0 == (ppcHelmet = (const char **)calloc(48, sizeof(const char *)))) fatal(acBadAlloc); ppcHelmet[iHeTail++] = pcLook; ppcHelmet[iHeTail++] = "-P"; ppcHelmet[iHeTail++] = pcRead; } /* Check the parameter count and $g/$u actions before we commit. * N.B. $* is funny in the code, we have to find the highest explicit * number, $* starts after that (this replaces CountArgs from lex.l). * So $2.$1 $* means replace the $* with "$3" "$4" "$5"... strange * DWIM compression. More odd $g/$G trip %g on, also $u/$U, $f/$F. */ { if (!fUid && Need.fUid) { fatal("%s: command requires -u", cmd->pcname); } if (!fGid && Need.fGid) { fatal("%s: command requires -g", cmd->pcname); } #if OP_MAC_SUPPORT if ((char *)0 == pcMac && Need.fMacs) { fatal("%s: command requires -m", cmd->pcname); } #endif if (!fSecFile && (Need.fSecFile || Need.fSecDir)) { fatal("%s: command requires -f", cmd->pcname); } if (fUid && !Need.fUid) { fatal("Command line -u %s not allowed", pcUid); } #if OP_MAC_SUPPORT if ((char *)0 != pcMac && !Need.fMacs) { fatal("Command line -m %s not allowed", pcMac); } #endif if (fGid && !Need.fGid) { fatal("Command line -g %s not allowed", pcGid); } pcSecPath = pcSecDir = (const char *)0; if (fSecFile) { register char *pcMem, *pcCursor; if (!(Need.fSecFile || Need.fSecDir)) fatal("Command line -f %s not allowed", pcSecFile); if ((char *)0 == (pcSecPath = DirectFile(cmd, pcSecFile, &stSecStat))) fatal("%s: %s: permission denied", cmd->pcname, pcSecPath); if (Need.fSecDir) { if ((char *)0 == (pcLook = strrchr(pcSecPath, '/'))) { pcSecDir = acCurDir; } else if (pcSecPath == pcLook) { pcSecDir = acSlash; } else { /* dup and remove last component */ if ((char *)0 == (pcMem = strdup(pcSecPath))) fatal("$d: %s", acBadAlloc); pcCursor = pcMem + strlen(pcMem); while (pcMem < pcCursor && '/' != *pcCursor) *pcCursor-- = '\000'; while (pcMem < pcCursor && '/' == *pcCursor) *pcCursor-- = '\000'; pcSecDir = pcMem; } if (-1 == stat(pcSecDir, & stSecDirStat)) { fatal("stat: %s: %s", pcSecDir, strerror(errno)); } } } if ((const char **)0 != ppcHelmet) { if (fSecFile) { ppcHelmet[iHeTail++] = "-f"; ppcHelmet[iHeTail++] = pcSecPath; } if (fUid) { ppcHelmet[iHeTail++] = "-u"; ppcHelmet[iHeTail++] = pcUid; } if (fGid) { ppcHelmet[iHeTail++] = "-g"; ppcHelmet[iHeTail++] = pcGid; } } if (0 == Need.fSecFd) { iSecFd = -1; } else if (-1 != (iSecFd = open(pcSecPath, O_RDWR|O_NOCTTY, 0600))) { /* OK */; } else if (!(EPERM == errno || EROFS == errno || EISDIR == errno) || -1 == (iSecFd = open(pcSecPath, O_RDONLY, 0600))) { fatal("open: %s: %s", pcSecPath, strerror(errno)); } if (0 == Need.fDirFd) { iDirFd = -1; } else if (-1 == (iDirFd = open(pcSecDir, O_RDONLY, 0700))) { fatal("open: %s: %s", pcSecDir, strerror(errno)); } } if ((char *)0 != (pcLook = FindOpt(cmd, acOptFib))) { register int iFib; #if HAVE_SETFIB extern int setfib(int); /* FreeBSD lacks extern in 7.2 */ #endif iFib = atoi(pcLook); if (iFib < 0) iFib = 0; #if HAVE_SETFIB if (-1 == setfib(iFib)) { fatal("setfib: %d: %s", iFib, strerror(errno)); } #else if (0 != iFib) { fatal("No support for alternate routing tables (FIBs)"); } #endif } if ((char *)0 != (pcLook = FindOpt(cmd, acOptNice))) { register int iPrio; iPrio = atoi(pcLook); if (iPrio < -20) iPrio = -20; if (iPrio > 20) iPrio = 20; (void)setpriority(PRIO_PROCESS, 0, iPrio); } /* Setup our uid and gid creds, but don't change them yet */ { register struct passwd *pw; register struct group *gr; auto char *pcField = (char *)0, *pcMem = (char *)0; auto gid_t gMap, *pwAdd; auto const char *pcEGid, *pcProposed; auto char *pcEmptySetGroups; register int iLen; uEffective = uPriv = geteuid(); uOrig = getuid(); if (uOrig == uPriv && 0 != uPriv) { uPriv = -1; } uReal = -1; gEffective = gPriv = getegid(); gOrig = getgid(); if (gOrig == gPriv) { gPriv = -1; } gReal = -1; if (-1 == (iOrigAccess = getgroups(OP_GIDLIST_MAX, agOrig))) { fatal("getgroups: %s", strerror(errno)); } pcSetGroups = pcEmptySetGroups = (char *)0; iNewGroups = 0; if ((char *)0 != (pcEGid = FindOpt(cmd, acOptEgid))) { if ('\000' == pcEGid[0] || IsMyself(pcEGid)) { gEffective = gOrig; } else if (0 == strcmp(acSpecF, pcEGid)) { gEffective = stSecStat.st_gid; } else if (0 == strcmp(acSpecD, pcEGid)) { gEffective = stSecDirStat.st_gid; } else if (0 == strcmp(acSpecU, pcEGid)) { (void) MapUid(pcUid, cmd, &gEffective); } else if (0 == strcmp(acSpecG, pcEGid)) { gEffective = MapGid(pcGid, cmd); } else if ((struct group *)0 != (gr = getgrnam(pcEGid))) { gEffective = gr->gr_gid; } else if (isdigit(*pcEGid)) { gEffective = atoi(pcEGid); } else { fatal("Invalid effective group: %s", pcEGid); } } pcNewHome = (const char *)0; if ((char *)0 == (pcEmptySetGroups = pcLook = FindOpt(cmd, acOptUid))) { uReal = uEffective; } else if (IsMyself(pcLook) || '\000' == *pcLook) { uReal = uOrig; if ((char *)0 == pcEGid) { gEffective = gOrig; pcEGid = "from real gid"; } } else if (0 == strcmp(acSpecF, pcLook)) { uReal = stSecStat.st_uid; if ((char *)0 == pcEGid) { gEffective = stSecStat.st_gid; pcEGid = "from gid of %f"; } } else if (0 == strcmp(acSpecD, pcLook)) { uReal = stSecDirStat.st_uid; if ((char *)0 == pcEGid) { gEffective = stSecDirStat.st_gid; pcEGid = "from gid of %d"; } } else if (0 == strcmp(acSpecU, pcLook)) { uReal = MapUid(pcUid, cmd, &gMap); if ((char *)0 == pcSetGroups) pcSetGroups = pcUid; if ((char *)0 == pcEGid) { gEffective = gMap; pcEGid = "from login group of %u"; } } else if ((struct passwd *)0 != (pw = getpwnam(pcLook))) { if ((char *)0 == pcSetGroups) { pcSetGroups = pcLook; } if ((const char *)0 == pcNewHome && (char *)0 == (pcNewHome = strdup(pw->pw_dir))) { fatal("$h: %s", acBadAlloc); } if ((char *)0 == pcEGid) { gEffective = pw->pw_gid; pcEGid = "from login group of uid"; } uReal = pw->pw_uid; } else if (!isdigit(*pcLook)) { /* we don't allow -1 as a uid, sorry */ fatal("Invalid real uid: %s", pcLook); } else { uReal = atoi(pcLook); } if ((char *)0 == (pcLook = FindOpt(cmd, acOptEuid))) { uEffective = uReal; } else if (IsMyself(pcLook)) { uEffective = uOrig; if ((char *)0 == pcEGid) { gEffective = gOrig; pcEGid = "from real gid"; } } else if (0 == strcmp(acSpecF, pcLook)) { uEffective = stSecStat.st_uid; if ((char *)0 == pcEGid) { gEffective = stSecStat.st_gid; pcEGid = "from gid of %f"; } } else if (0 == strcmp(acSpecD, pcLook)) { uEffective = stSecDirStat.st_uid; if ((char *)0 == pcEGid) { gEffective = stSecDirStat.st_gid; pcEGid = "from gid of %d"; } } else if (0 == strcmp(acSpecU, pcLook)) { uEffective = MapUid(pcUid, cmd, &gMap); if ((char *)0 == pcSetGroups) pcSetGroups = pcUid; if ((char *)0 == pcEGid) { gEffective = gMap; pcEGid = "from login group of %u"; } } else if ((struct passwd *)0 != (pw = getpwnam(pcLook))) { if ((char *)0 == pcSetGroups) pcSetGroups = pcLook; if ((char *)0 == pcEGid) { gEffective = gMap; pcEGid = "from login group of euid"; } uEffective = pw->pw_uid; } else if (!isdigit(*pcLook)) { fatal("Invalid effective uid: %s", pcLook); } else { uEffective = atoi(pcLook); } if ((char *)0 == pcEmptySetGroups) { pcEmptySetGroups = pcLook; } /* An initgroups spec of %u or %f sets by the command-line, and a * spec of "." or %l leaves them, a name does the obvious, else delete * the groups we had. Let gid= set more as listed. This is not * what the original code did, but it is more sane. */ if ((char *)0 == (pcLook = FindOpt(cmd, acOptInitgrps))) { pcSetGroups = (char *)0; } else if ('\000' == pcLook[0]) { if ((char *)0 == (pcLook = pcEmptySetGroups)) fatal("%s: no uid or euid set", acOptInitgrps); pcSetGroups = pcLook; } else if (IsMyself(pcLook)) { pcSetGroups = acMyself; } else if (0 == strcmp(acSpecF, pcLook) || 0 == strcmp(acSpecD, pcLook)) { register const struct stat *pst; pst = 0 == strcmp(acSpecF, pcLook) ? &stSecStat : &stSecDirStat; if ((struct passwd *)0 == (pw = getpwuid(pst->st_uid))) { fatal("getpwuid: -f owner #%ld unmapped for %s", (long)pst->st_uid, acOptInitgrps); } if ((char *)0 == (pcSetGroups = strdup(pw->pw_name))) fatal("%s: %s", acOptInitgrps, acBadAlloc); if ((char *)0 == pcEGid) { gEffective = pst->st_gid; pcEGid = 0 == strcmp(acSpecF, pcLook) ? "from %f's gid" : "from %d's gid"; } } else if (0 == strcmp(acSpecU, pcLook)) { pcSetGroups = pcUid; (void)MapUid(pcSetGroups, cmd, &gMap); if ((char *)0 == pcEGid) { gEffective = gMap; pcEGid = "from login group of %u"; } } else { pcSetGroups = pcLook; (void)MapUid(pcSetGroups, cmd, &gMap); if ((char *)0 == pcEGid) { gEffective = gMap; iLen = ((24+strlen(pcSetGroups))|15)+1; /* fmt below */ if ((char *)0 == (pcMem = malloc(iLen))) fatal(acBadAlloc); snprintf(pcMem, iLen, "from login group of %s", pcSetGroups); pcEGid = pcMem; } } /* A session is a pam version of initgroups, setting other resource * limits and details that we otherwise won't set. We don't set * all the setrlimit(2) values [or even ulimit(3)], let pam do it. * A session of "" turns this off, like pam="" turns off pam auth. * We end the session under "cleanup=.", at the cost of a fork(2). */ if ((char *)0 == (pcLook = FindOpt(cmd, acOptSession)) || '\000' == *pcLook) { pcSetSession = (const char *)0; } else if (IsMyself(pcLook)) { pcSetSession = pcPerp; } else if (0 == strcmp(acSpecF, pcLook) || 0 == strcmp(acSpecD, pcLook)) { register const struct stat *pst; pst = 0 == strcmp(acSpecF, pcLook) ? &stSecStat : &stSecDirStat; if ((struct passwd *)0 == (pw = getpwuid(pst->st_uid))) { fatal("getpwuid: -f owner #%ld unmapped for session", (long)pst->st_uid); } if ((char *)0 == (pcSetSession = strdup(pw->pw_name))) fatal("%s: %s", acOptSession, acBadAlloc); } else if (0 == strcmp(acSpecU, pcLook)) { pcSetSession = pcUid; } else if (0 == strcmp(acKeyInitgroup, pcLook)) { pcSetSession = pcSetGroups; if ((char *)0 == pcSetSession && (char *)0 == (pcSetSession = pcEmptySetGroups)) fatal("%s: no %s, %s or %s to copy", acOptSession, acOptUid, acOptEuid, acOptInitgrps); } else { pcSetSession = pcLook; } if ((char *)0 == (pcLook = FindOpt(cmd, acOptCleanup)) || '\000' == *pcLook) { pcEndSession = (const char *)0; } else if (IsMyself(pcLook)) { pcEndSession = pcPerp; } else if (0 == strcmp(acSpecF, pcLook) || 0 == strcmp(acSpecD, pcLook)) { register const struct stat *pst; pst = 0 == strcmp(acSpecF, pcLook) ? &stSecStat : &stSecDirStat; if ((struct passwd *)0 == (pw = getpwuid(pst->st_uid))) { fatal("getpwuid: -f owner #%ld unmapped for cleanup", (long)pst->st_uid); } if ((char *)0 == (pcEndSession = strdup(pw->pw_name))) fatal("%s: %s", acOptCleanup, acBadAlloc); } else if (0 == strcmp(acSpecU, pcLook)) { pcEndSession = pcUid; } else if (0 == strcmp(acKeyInitgroup, pcLook)) { pcEndSession = pcSetGroups; if ((char *)0 == pcEndSession && (char *)0 == (pcEndSession = pcEmptySetGroups)) fatal("%s: no %s, %s or %s to copy", acOptCleanup, acOptUid, acOptEuid, acOptInitgrps); } else { pcEndSession = pcLook; } /* When we asked for a gid=list we have to build it, else use initgroups */ if ((char *)0 == (pcProposed = FindOpt(cmd, acOptGid))) { static const char acOrig[] = "from client's real gid"; if ((const char *)0 == pcEGid) { gEffective = gOrig; pcEGid = acOrig; } if ((char *)0 == pcSetGroups) { setgroups(1, & gEffective); gReal = gEffective; } else if (IsMyself(pcSetGroups)) { /* leave them be */ } else if (0 != initgroups(pcSetGroups, gEffective)) { fatal("%s: %s,%ld: %s", acOptInitgrps, pcSetGroups, (long)gEffective, strerror(errno)); } if (-1 == gReal) { gReal = gEffective; } } else { /* Start with our effective, if presented, then add any * groups from initgroups list, only if asked, then from * the gid=list. gid= means gid=. */ if ('\000' == *pcProposed) { pcProposed = acMyself; } if ((char *)0 != pcEGid) { AddGid(gEffective, & iNewGroups, agNewList); } if ((char *)0 == pcSetGroups) { /* nada we have the whole list */ } else if (IsMyself(pcSetGroups)) { for (i = 0; i < iOrigAccess; ++i) { AddGid(agOrig[i], & iNewGroups, agNewList); } } else { j = util_getlogingr(pcSetGroups, &pwAdd); for (i = 0; i < j; ++i, ++pwAdd) { AddGid(*pwAdd, & iNewGroups, agNewList); } } for (pcRead = GetField(pcProposed, &pcField); OP_GIDLIST_MAX > iNewGroups && (char *)0 != pcRead; pcRead = GetField(pcRead, &pcField)) { if (IsMyself(pcField)) { AddGid(gOrig, & iNewGroups, agNewList); } else if (0 == strcmp(acSpecF, pcField)) { AddGid(stSecStat.st_gid, & iNewGroups, agNewList); } else if (0 == strcmp(acSpecD, pcField)) { AddGid(stSecDirStat.st_gid, & iNewGroups, agNewList); } else if (0 == strcmp(acSpecU, pcField)) { (void) MapUid(pcUid, cmd, &gMap); AddGid(gMap, & iNewGroups, agNewList); } else if (0 == strcmp(acSpecG, pcField)) { AddGid(MapGid(pcGid, cmd), & iNewGroups, agNewList); } else if ((struct group *)0 != (gr = getgrnam(pcField))) { AddGid(gr->gr_gid, & iNewGroups, agNewList); } else if (isdigit(*pcField)) { AddGid(atoi(pcField), & iNewGroups, agNewList); } else { fatal("%s: invalid real group: %s", acOptGid, pcField); } /* The first group in the gid list is the real */ if (-1 == gReal) { gReal = agNewList[iNewGroups-1]; } } EndField(); if (-1 == gReal) { fatal("Invalid empty group list"); } /* When not setuid we cannot setgroups. So look for the * first group we don't have for effective. */ if (0 != uPriv) { for (i = 0; gEffective == gReal && i < iNewGroups; ++i) gReal = agNewList[i]; } else if (setgroups(iNewGroups, agNewList) < 0) { fatal("setgroups: %s", strerror(errno)); } if ((char *)0 == pcEGid) { gEffective = gReal; pcEGid = "from first in gid list"; } } /* There is no way to tell is setfsuid(2) worked. So we just * hope for the best. If we have the same fsuid we might get * the same value back, which is also the error we get if it * failed. Who knows which it was? They don't set errno. */ #if HAVE_SETFSUID (void)setfsuid(uEffective); #endif #if HAVE_SETFSGID (void)setfsgid(gEffective); #endif /* If op is setgid and we have a saved gid drop it */ #if HAVE_setresgid if (setresgid(gReal, gReal, gReal) < 0) { fatal("setresgid: %ld,%ld,%ld: %s", (long)gReal, (long)gReal, (long)gReal, strerror(errno)); } #else if (gReal == gEffective) { if (setgid(gReal) < 0) { fatal("setgid: %ld: %s", (long)gReal, strerror(errno)); } } else if (setregid(gReal, gEffective) < 0) { fatal("setregid: to %ld,%ld (from gid %s %ld,%ld): %s", (long)gReal, (long)gEffective, pcEGid, (long)getgid(), (long)getegid(), strerror(errno)); } #endif } /* end uid gid creds */ /* Setup any forced file descriptios for std{in,out,err}, which * depends on the code that assures that 0,1,2 are open already. * We open them before we chroot, of course. Which can make some * chroot's fail (respecting kern.chroot_allow_open_directories). */ { register const char *pcDaemon; aiNewIO[0] = aiNewIO[1] = aiNewIO[2] = aiNewIO[3] = -1; pcDaemon = ((char *)0 != FindOpt(cmd, acOptDaemon)) ? acDevNull : (const char *)0; /* We should be able to read from $n, maybe write too, but not * connect to the socket? */ if ((const char *)0 != (pcRead = FindOpt(cmd, acOptStdin)) || (const char *)0 != (pcRead = pcDaemon)) { aiNewIO[0] = PrivOpen(cmd, "<", pcRead, pcSecPath); } if ((const char *)0 != (pcRead = FindOpt(cmd, acOptStdout)) || (const char *)0 != (pcRead = pcDaemon)) { aiNewIO[1] = PrivOpen(cmd, ">", pcRead, pcSecPath); } if ((const char *)0 != (pcRead = FindOpt(cmd, acOptStderr)) || (const char *)0 != (pcRead = pcDaemon)) { aiNewIO[2] = PrivOpen(cmd, ">", pcRead, pcSecPath); } /* aiNewIO[3] is only set by ExtInput's & spec */ /* Don't chroot until we are done with the password/group files, * then chdir (an empty spec is slash). Under any chroot move to * the new root directory by default. To avoid this use "chdir=.". * Don't actually do it until after any jacket lets us go. */ if ((const char *)0 != (pcNewRoot = FindOpt(cmd, acOptChroot))) { if ('\000' == pcNewRoot[0]) pcNewRoot = (char *)0; else if (0 == strcmp(acSpecF, pcNewRoot)) pcNewRoot = pcSecPath; else if (0 == strcmp(acSpecD, pcNewRoot)) pcNewRoot = pcSecDir; else if (0 == strcmp(acSpecU, pcNewRoot)) pcNewDir = HomeDir(pcUid); else if (0 == strcmp(acSpecMe, pcNewRoot)) pcNewRoot = pcPerpHome; } if ((char *)0 != (pcNewDir = FindOpt(cmd, acOptDir))) { if (0 == strcmp(acSpecF, pcNewDir)) pcNewDir = pcSecPath; else if (0 == strcmp(acSpecD, pcNewDir)) pcNewDir = pcSecDir; else if (0 == strcmp(acSpecU, pcNewDir)) pcNewDir = HomeDir(pcUid); else if (0 == strcmp(acSpecMe, pcNewDir)) pcNewDir = pcPerpHome; else if ('\000' == *pcNewDir) pcNewDir = acSlash; } else if ((char *)0 != pcNewRoot) { pcNewDir = acSlash; } if ((const char **)0 != ppcHelmet) { if ((char *)0 != pcNewRoot) { ppcHelmet[iHeTail++] = "-R"; ppcHelmet[iHeTail++] = pcNewRoot; } ppcHelmet[iHeTail++] = "-C"; ppcHelmet[iHeTail++] = cmd->pcfile; } } /* The manual says umask never fails. I suppose that's true. */ if ((char *)0 == (pcLook = FindOpt(cmd, acOptMask))) { iOrigMask = umask(0022); /* as per original spec */ } else { register long lMask; auto char *pcEnd; lMask = strtoul(pcLook, &pcEnd, 8); iOrigMask = umask((int)lMask); } { register const char *pcEq; register char *pcEnv; register int iKeep; auto char *pcField; /* Copy in the user environment, then overlay ours, if any. * environment => environment=. * environment=RES => match each RE as: * RE contains an '=' => match against whole VAR=value * RE w/o any '=' => match against name only. * match just values use ^[^=]*=RE */ if ((char *)0 != (pcRead = FindOpt(cmd, acOptEnv))) { register regex_t *reg; auto char *pcCarp; if ('\000' == *pcRead) { pcRead = "."; /* RE that matchs a non-empty string */ } while ((char *)0 != (pcRead = GetField(pcRead, &pcField))) { pcEq = strchr(pcField, '='); for (i = 0; (char *)0 != (pcEnv = environ[i]); ++i) { if ((char *)0 == (pcEnv = strchr(pcEnv, '='))) continue; if ((char *)0 == pcEq) *pcEnv = '\000'; if ((regex_t *)0 == (reg = ReComp(pcField, &pcCarp))) fatal("%s: regcomp: %s: %s", acOptEnv, pcField, pcCarp); iKeep = ReMatch(reg, environ[i]); regfree(reg); *pcEnv = '='; if (!iKeep) { continue; } AddEnv(&ppcNewEnv, environ[i], (char **)0); } } EndField(); } } /* Produce the command template if it was "MAGIC_SHELL" * Magic shell maps $* -> ${max}, defaults to $S -c $* * also the cmd record may be fixed a little, it is magic. * then check $* (now that we have the final argv list). */ if (argc < Need.lCmdParam+3) { register const char **ppcClone; if ((const char **)0 == (ppcClone = calloc(((Need.lCmdParam+3)|3)+1, sizeof(const char *)))) { fatal(acBadAlloc); } for (i = 0; i < argc; ++i) { ppcClone[i] = argv[i]; } for (/* above */; i < Need.lCmdParam+3; ++i) ppcClone[i] = (const char *)0; argv = ppcClone; } /* When you config just MAGIC_SHELL and there are extra parameters we * make the command template "$S -c $*" , else "$S", as documented. */ if (0 == strcmp(acMagicShell, cmd->args[0])) { if (1 == argc && 1 == cmd->nargs) { cmd->args[0] = apcMagic[0]; } else { cmd->args = (const char **)apcMagic; cmd->nargs = 3; } } if ((char *)0 != (pcLook = FindOpt(cmd, "$*"))) { for (j = Need.lCmdParam+1; j < argc; ++j) { if (! GenMatch("!*", argv[j], pcLook)) fatal(acBadParam); } } if ((char *)0 != (pcLook = FindOpt(cmd, "!*"))) { for (j = Need.lCmdParam+1; j < argc; ++j) { if (GenMatch("$*", argv[j], pcLook)) fatal(acBadParam); } } /* Build the actual command argument to execute */ { register int c, iCurArg; register char *pcMem; register int iMax, iLen, fAllowEmpty; register const char *pcSplice, *pcScan; register int iArgx, iOptx; auto char acCvt[OP_MAX_LDWIDTH]; /* "xx%ld" */ auto char *pcEnd; auto unsigned long ul; auto int iRep, fEoa; auto char *pcNew; auto gid_t gIgnore, gPrint; auto uid_t uPrint; auto struct group *gr; auto struct passwd *pw; auto struct stat *pstFull; #define MAX_BEFORE 4 auto char *pcD_, *pcShell; auto char acBack[4], **appcBefore[MAX_BEFORE]; auto struct stat stFull; #if OP_MAC_SUPPORT auto mac_t macCvt; #endif auto char acStatPath[MAXPATHLEN+4]; static const char acCmdPerms[] = "$_: %s: Permission denied"; iMax = 10240; if ((char *)0 == (pcMem = (char *)malloc(iMax))) { fatal(acBadAlloc); } fAllowEmpty = iCurArg = 0; /* Double conditional loop, for options and env variables, expand * skipping $*, $1, $2 and the like set all the forced $env=values * to their expansion. Then expand the parameters. We must expand * the envronment first for $S in the args list. But first we must * exapand $_. Which is a pain to setup. */ iLen = ((strlen(cmd->args[0])+3)|15)+(OP_MAX_LDWIDTH+1); if ((char *)0 == (pcD_ = malloc(iLen))) { fatal(acBadAlloc); } snprintf(pcD_, iLen, "%s", cmd->args[0]); cmd->args[0] = FindUnder(cmd, &Need, argc, &pcD_); /* The negative Optx indexes the Before list, which all need to * expanded and replaced with the expansion before the env or * args can be expanded. See /0 > iOptx)/ below for the updates. * Presently we need first $S then $_ before we can build the rest. */ iOptx = 0; appcBefore[iOptx++] = &pcD_; switch (CheckTrust(cmd, acShell)) { case 1: /* they set, or default value */ if ((char *)0 == (pcShell = getenv(acShell+1))) case 0: /* not allowed so we get our default */ pcShell = (char *)acDefShell; break; case 2: /* the value is in our config, expand it first */ pcShell = FindOpt(cmd, acShell); appcBefore[iOptx++] = &pcShell; break; default: fatal("CheckTrust: internal error"); /*NOTREACHED*/ } appcBefore[iOptx] = (char **)0; iOptx = -iOptx; for (iArgx = 0; (iOptx < 0 ? (pcScan = *appcBefore[-(iOptx+1)], 1) : (iOptx < cmd->nopts && (char *)0 != (pcScan = cmd->opts[iOptx++]))) || (iArgx < cmd->nargs && (char *)0 != (pcScan = cmd->args[iArgx++])); /* double cond */) { auto int fMac = 0; /* when looking for an environment variable, or mac spec */ if (0 == iArgx && iOptx > 0) { /* We expand the mac value, like an environment var */ if (0 == strncmp("mac=", pcScan, 4)) { fMac = 1; } else if (*pcScan++ != '$') { continue; } if ('#' == *pcScan || '@' == *pcScan || '*' == *pcScan || isdigit(*pcScan)) { continue; } } iLen = 0, fEoa = 0; iRep = -1; while ('\000' != (c = (pcMem[iLen++] = *pcScan++))) { if (iLen+2 >= iMax) { iMax += 4096; if ((char *)0 == (pcNew = realloc((void *)pcMem, iMax))) { fatal(acBadAlloc); } pcMem = pcNew; } if ('$' != c) { continue; } if ('$' == (c = *pcScan++)) { /* $$ -> $ */ continue; } --iLen; pcSplice = (const char *)0; switch (c) {/*UnIqueStart*/ case '.': /* internal only expander -- break word */ pcSplice = ""; if (0 != iArgx) fEoa = 1; else if (iLen > 0 && !isspace(pcMem[iLen-1])) pcSplice = " "; break; case '@': /* the parameters a words */ case '*': /* the parameters as a single word */ case '-': /* whole command-line as words */ case '+': /* whole command-line as a single word */ /* there is no *++1 */ case ',': /* internal only expander -- $-+1 */ { auto int fFirst; /* When in environment don't break words. */ if (0 == iArgx) { c = '@' == c ? '*' : '-' == c ? '+' : c; } fFirst = 0; if (-1 == iRep) { fAllowEmpty = '@' == c || '-' == c || ',' == c; iRep = Need.lCmdParam; if ('-' == c || '+' == c || ',' == c) iRep = -1; if (',' == c) ++iRep; fFirst = 1; } if (++iRep >= argc) { iRep = -1; pcSplice = ""; break; } pcSplice = argv[iRep]; /* We need a space when we are in $* or $& * and we are not the first element */ if (!fFirst && ('+' == c || '*' == c)) { pcMem[iLen++] = ' '; } /* We need an end-of-arg when in $@, $-, $, */ fEoa = iRep < argc-1 && ('-' == c || '@' == c || ',' == c); pcMem[iLen] = '\000'; } pcScan -= 2; break; case '#': /* the number of command line params */ snprintf(acCvt, sizeof(acCvt), "%ld", (long)argc-1); pcSplice = acCvt; break; case '0': /* Easter Egg $0 is the mnemonic name */ case '1': /* $1, $2... from the command line */ case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': ul = strtoul(pcScan-1, &pcEnd, 10); if (argc < ul) { fatal("$%lu beyond given argument list", ul); } pcScan = pcEnd; pcSplice = argv[ul]; fAllowEmpty = 1; break; case '{': /* allow interpolation of OLD environment */ if ((char *)0 == (pcEnd = strchr(pcScan, '}'))) fatal("Missing close curly in environment variable substitution"); *pcEnd = '\000'; if ((char *)0 == (pcSplice = getenv(pcScan))) pcSplice = ""; /*{*/ *pcEnd++ = '}'; pcScan = pcEnd; break; case '\\': /* allow tr list */ acBack[1] = '\000'; switch (c = *pcScan++) { case '\000': syslog(LOG_ERR, "%s: %s:%d: backslash at end of %s, user notified", cmd->pcname, cmd->pcfile, cmd->iline, 0 == iArgx ? "parameter" : "argument"); fatal("%s: end of string in backslash escape, please ask the administrator to run \"%s -S\"", cmd->pcname, Progname); /*NOTREACHED*/ exit(EX_SOFTWARE); case 'a': acBack[0] = '\007'; break; case 'b': acBack[0] = '\b'; break; case 'd': acBack[0] = '"'; break; case 'f': acBack[0] = '\f'; break; case 'n': acBack[0] = '\n'; break; case 'o': acBack[0] = '`'; break; case 'q': acBack[0] = '\''; break; case 'r': acBack[0] = '\r'; break; case 's': acBack[0] = ' '; break; case 't': acBack[0] = '\t'; break; case 'v': acBack[0] = '\v'; break; default: acBack[0] = c; break; } pcSplice = acBack; break; case '|': /* empty string */ pcSplice = ""; break; case 'C': /* the configuration directory */ { register char *pcTail; if ((char *)0 == (pcSplice = strdup(pcAccess))) { fatal("$%c: %s", c, acBadAlloc); } if ((char *)0 != (pcTail = strrchr(pcSplice, '/'))) { pcTail[1] = '\000'; while ('/' == *pcTail && pcTail > pcSplice) *pcTail-- = '\000'; } else { pcSplice = acCurDir; } } break; case 'c': /* the base configuration file */ pcSplice = pcAccess; break; case 's': /* { script } payload */ pcSplice = cmd->pcscript; break; case 'S': /* the shell specified */ pcSplice = pcShell; if ((char *)0 == pcSplice) { pcSplice = acDefShell; } break; case '_': /* target script, or shell */ if (-1 > iOptx) { syslog(LOG_ERR, "%s: %s:%d uses $_ as part of itself", cmd->pcname, cmd->pcfile, cmd->iline); fatal("$_ may not be used as part of itself"); } pcSplice = pcD_; /* */ break; case 'u': /* -u login */ /* check passes %u attributes */ uPrint = MapUid(pcUid, cmd, &gIgnore); pcSplice = pcUid; break; case 'U': /* -u by uid */ uPrint = MapUid(pcUid, cmd, &gIgnore); snprintf(acCvt, sizeof(acCvt), "%ld", (long)uPrint); pcSplice = acCvt; break; case 'g': /* -g group */ /* check passes %g attributes */ gPrint = MapGid(pcGid, cmd); pcSplice = pcGid; break; case 'G': /* -g by gid */ gPrint = MapGid(pcGid, cmd); snprintf(acCvt, sizeof(acCvt), "%ld", (long)gPrint); pcSplice = acCvt; break; case 'd': /* -f's directory */ if ((const char *)0 == pcSecDir) { fatal("No -f to match $d"); } pcSplice = pcSecDir; break; case 'D': /* -f's directory, by open fd */ if (-1 == iDirFd) { fatal("No -f to match $D"); } snprintf(acCvt, sizeof(acCvt), "%ld", (long)iDirFd); pcSplice = acCvt; break; case 'f': /* -f file */ pcSplice = pcSecPath; break; case 'F': /* -f file by fd */ if (-1 == iSecFd) { fatal("No -f to match $F"); } snprintf(acCvt, sizeof(acCvt), "%ld", (long)iSecFd); pcSplice = acCvt; break; #if OP_MAC_SUPPORT case 'm': /* -m label specified */ if ((char *)0 == pcMac) { fatal("No -m to match $m"); } pcSplice = pcMac; break; case 'M': /* our current process MAC label */ { auto char *pcParam; if (-1 == mac_prepare_process_label(& macCvt)) { fatal("$M: mac_prepare: %s", strerror(errno)); } if (-1 == mac_get_proc(macCvt)) { fatal("$M: mac_get_proc: %s", strerror(errno)); } if (-1 == mac_to_text(macCvt, &pcParam)) { fatal("$M: mac_to_text: %s", strerror(errno)); } /* N.B. we never free the MAC label copy */ pcSplice = pcParam; } break; #else case 'm': case 'M': fatal("No Mandatory Access Control available"); #endif case 't': /* target login */ if ((struct passwd *)0 != (pw = getpwuid(uReal))) { pcSplice = pw->pw_name; } else { snprintf(acCvt, sizeof(acCvt), "#%ld", (long)uReal); pcSplice = acCvt; } break; case 'T': /* target login by uid */ snprintf(acCvt, sizeof(acCvt), "%ld", (long)uReal); pcSplice = acCvt; break; case 'i': /* initgroups target */ case 'I': /* initgroups target but uid */ if (IsMyself(pcSetGroups) || (char *)0 == (pcSplice = pcSetGroups)) { pcSplice = pcPerp; } if ('I' == c && (struct passwd *)0 != (pw = getpwnam(pcSplice))) { snprintf(acCvt, sizeof(acCvt), "%ld", (long)pw->pw_uid); pcSplice = acCvt; } break; case 'a': /* original group access list */ case 'A': /* by gid */ if (++iRep == iOrigAccess) { iRep = -1; pcSplice = ""; break; } if ('a' == c && (struct group *)0 != (gr = getgrgid(agOrig[iRep]))) { snprintf(acCvt, sizeof(acCvt), ",%s", gr->gr_name); } else { snprintf(acCvt, sizeof(acCvt), ",%s%ld", 'a' == c ? "#" : "", (long)agOrig[iRep]); } pcSplice = acCvt; if (0 == iRep) { ++pcSplice; } pcScan -= 2; break; case 'n': /* new group list */ case 'N': /* group list by gid */ if (++iRep == iNewGroups) { iRep = -1; pcSplice = ""; break; } if ('n' == c && (struct group *)0 != (gr = getgrgid(agNewList[iRep]))) { snprintf(acCvt, sizeof(acCvt), ",%s", gr->gr_name); } else { snprintf(acCvt, sizeof(acCvt), ",%s%ld", 'n' == c ? "#" : "", (long)agNewList[iRep]); } pcSplice = acCvt; if (0 == iRep) { ++pcSplice; } pcScan -= 2; break; case 'e': /* exec suid name ("root" usually) */ case '~': /* exec suid home directory ("/") */ if (-1 == uPriv || (struct passwd *)0 == (pw = getpwuid(uPriv))) { break; } if ('~' == c) { pcSplice = pw->pw_dir; } else { pcSplice = pw->pw_name; } break; case 'E': /* our original setuid id */ if (-1 == uPriv) break; snprintf(acCvt, sizeof(acCvt), "%ld", (long)geteuid()); pcSplice = acCvt; break; case 'b': /* effective group, not usually used */ if (-1 == gPriv || (struct group *)0 == (gr = getgrgid(gPriv))) { break; } pcSplice = gr->gr_name; break; case 'B': /* effective group by gid, symmetry */ if (-1 == gPriv) break; snprintf(acCvt, sizeof(acCvt), "%ld", (long)gPriv); pcSplice = acCvt; break; case 'h': /* ~perp */ pcSplice = pcPerpHome; break; case 'H': /* ~new-real-uid */ if ((const char *)0 != pcNewHome) { /* nada */ } else if ((struct passwd *)0 != (pw = getpwuid(uReal))) { pcNewHome = strdup(pw->pw_dir); } pcSplice = pcNewHome; break; case 'K': /* new-real-uid's shell */ pcSplice = (char *)0; if ((struct passwd *)0 != (pw = getpwuid(uReal))) { pcSplice = pw->pw_shell; } /*FALLTHROUGH*//*CONSTANTCOND*/ if (0) { case 'k': /* perp's shell */ pcSplice = pcPerpShell; } if ((char *)0 == pcSplice || '\000' == *pcSplice) { pcSplice = acDefShell; } break; case 'l': /* original login */ pcSplice = pcPerp; break; case 'L': /* original uid */ snprintf(acCvt, sizeof(acCvt), "%ld", (long)uOrig); pcSplice = acCvt; break; case 'p': /* pam session login */ pcSplice = pcSetSession; break; case 'P': /* pam session uid */ if ((char *)0 == pcSetSession) { fprintf(stderr, "%s: $P: no session set\n", Progname); exit(EX_SOFTWARE); } if ((struct passwd *)0 != (pw = getpwnam(pcSetSession))) { snprintf(acCvt, sizeof(acCvt), "%ld", (long)pw->pw_uid); pcSplice = acCvt; } break; case 'r': /* original real group */ if ((struct group *)0 != (gr = getgrgid(gOrig))) { pcSplice = gr->gr_name; } else { snprintf(acCvt, sizeof(acCvt), "#%ld", (long)gOrig); pcSplice = acCvt; } break; case 'R': /* original read gid */ snprintf(acCvt, sizeof(acCvt), "%ld", (long)gOrig); pcSplice = acCvt; break; case 'o': /* target group */ if ((struct group *)0 != (gr = getgrgid(gReal))) { pcSplice = gr->gr_name; } else { snprintf(acCvt, sizeof(acCvt), "#%ld", (long)gReal); pcSplice = acCvt; } break; case 'O': /* target gid */ snprintf(acCvt, sizeof(acCvt), "%ld", (long)gReal); pcSplice = acCvt; break; case 'q': /* the basename of this program */ pcSplice = Progname; break; case 'Q': /* the full path to this program */ pcSplice = FullProgname; break; case '^': /* version 3 compile options, back-ported */ pcSplice = acCompOpts; break; case 'v': /* version of op */ pcSplice = rcsid; break; case 'V': /* version as a number */ pcSplice = rcsid; /* "tok: name number ..." */ i = j = 0; while ('\000' != (c = *pcSplice++)) { if (isspace(c)) { ++j; continue; } if (2 == j) { acCvt[i++] = c; } } acCvt[i] = '\000'; pcSplice = acCvt; break; case 'w': /* which configuration file */ pcSplice = cmd->pcfile; break; case 'W': /* where in the configuration file */ snprintf(acCvt, sizeof(acCvt), "%ld", (long)cmd->iline); pcSplice = acCvt; break; case 'x': /* target directory */ if ((char *)0 == (pcSplice = pcNewDir)) pcSplice = acCurDir; break; case 'X': /* target root, notify if not chroot'd */ if ((char *)0 == (pcSplice = pcNewRoot)) { syslog(LOG_ERR, "%s: %s:%d: $X without chroot set", cmd->pcname, cmd->pcfile, cmd->iline); pcSplice = acSlash; } break; case 'y': /* our tty name from 2, 0, 1 */ if ((char *)0 != (pcSplice = ttyname(2))) /* got it from stderr */; else if ((char *)0 != (pcSplice = ttyname(0))) /* got it from stdin */; else /* try stdout, or fail */ pcSplice = ttyname(1); break; case 'Y': /* our original umask */ snprintf(acCvt, sizeof(acCvt), "%04lo", (long)iOrigMask); pcSplice = acCvt; break; case 'z': /* our pid */ snprintf(acCvt, sizeof(acCvt), "%ld", (long)getpid()); pcSplice = acCvt; break; case 'Z': /* our ppid */ snprintf(acCvt, sizeof(acCvt), "%ld", (long)iOrigPpid); pcSplice = acCvt; break; default: break; }/*UNIqueEnd*/ if ((const char *)0 == pcSplice) { fatal("%s: $%c: no data found", cmd->pcname, c); } if (iLen+strlen(pcSplice) >= iMax) { iMax += (4095|strlen(pcSplice))+1; if ((char *)0 == (pcNew = realloc((void *)pcMem, iMax))) { fatal("$%c: %s", c, acBadAlloc); } pcMem = pcNew; } (void)strcpy(pcMem+iLen, pcSplice); iLen += strlen(pcSplice); if (fEoa && (fAllowEmpty || '\000' != pcMem[0])) { AddArg(&ppcNewArgs, strdup(pcMem)); ++iCurArg; iLen = 0, fEoa = 0; } } /* If we just finished a before expansion, resolve it. */ if (0 > iOptx) { ++iOptx; if ((char *)0 == (*appcBefore[-iOptx] = strdup(pcMem))) fatal("before: %s", acBadAlloc); if (appcBefore[-iOptx] == &pcShell && (char *)0 != strstr(pcShell, "perl")) apcMagic[1] = "-e"; continue; } if (0 == iArgx) { if (!fMac) AddEnv(&ppcNewEnv, strdup(pcMem), environ); else if ((char *)0 == (pcNewMac = strdup(pcMem))) fatal("%s: %s", acOptMac, acBadAlloc); continue; } /* N.B iLen is 1 too big since we counted the \000 */ if (1 == iLen && !fAllowEmpty) { continue; } fAllowEmpty = 0; AddArg(&ppcNewArgs, strdup(pcMem)); ++iCurArg; } /* When basename is set we lie about argv[0] */ if ((const char **)0 == ppcNewArgs || (char *)0 == ppcNewArgs[0] || '\000' == ppcNewArgs[0][0]) { fatal("%s: no command generated from given specification", cmd->pcname); } pcExecPath = ppcNewArgs[0]; if ((char *)0 != (pcLook = FindOpt(cmd, acOptArgv0))) { ppcNewArgs[0] = pcLook; } if ((const char **)0 != ppcNewEnv && CheckEnv(cmd, ppcNewEnv)) { fatal("environment limits forbid this access"); } pcSplice = pcExecPath; /* real path under chroot */ pstFull = &stFull; if (0 == strcmp(pcExecPath, acEcho)) { pstFull = (struct stat *)0; } else { if ((char *)0 != pcNewRoot && 0 != strcmp("/", pcNewRoot)) { snprintf(acStatPath, sizeof(acStatPath), "%s%s%s", pcNewRoot, '/' != *pcExecPath ? "/" : "", pcExecPath); pcSplice = acStatPath; } if (-1 == lstat(pcSplice, & stFull)) { pstFull = (struct stat *)0; } } if ((char *)0 != (pcScan = FindOpt(cmd, "%_"))) { if (!FileAttr(pcSplice, pstFull, pcScan, asFKnown)) fatal(acCmdPerms, pcSplice); } if ((char *)0 != (pcScan = FindOpt(cmd, "!_"))) { if (FileAttr(pcSplice, pstFull, pcScan, asFKnown)) fatal(acCmdPerms, pcSplice); } for (pcRead = asFKnown; '\000' != *pcRead; pcRead += strlen(pcRead)+1) { auto char acLook[((4+sizeof(asFKnown))|7)+1]; snprintf(acLook, sizeof(acLook), "%%_.%s", pcRead); if ((char *)0 != (pcScan = FindOpt(cmd, acLook))) { if (!FileAttr(pcSplice, pstFull, pcScan, pcRead)) fatal(acCmdPerms, pcSplice); } snprintf(acLook, sizeof(acLook), "!_.%s", pcRead); if ((char *)0 != (pcScan = FindOpt(cmd, acLook))) { if (FileAttr(pcSplice, pstFull, pcScan, pcRead)) fatal(acCmdPerms, pcSplice); } } if ((char *)0 != (pcRead = FindOpt(cmd, acPatFowners)) && '\000' != pcRead && !FileAttr(pcSplice, pstFull, pcRead, acPatFowners)) { fatal(acCmdPerms, pcSplice); } if ((char *)0 != (pcRead = FindOpt(cmd, acPatPerms)) && '\000' != pcRead && !FileAttr(pcSplice, pstFull, pcRead, acPatPerms)) { fatal(acCmdPerms, pcSplice); } #undef MAX_BEFORE } /* Make all the processes we were asked for */ { register char **ppcHold; auto char acCvt[OP_MAX_LDWIDTH]; /* "%ld" */ auto pid_t wPid, wReap; auto int iStatus, iFdOldErr, aiPipe[2]; iFdOldErr = 2; #define RESTORE_STDERR() do { register int iE = errno; if (2 != iFdOldErr) { dup2(iFdOldErr, 2); } errno = iE; } /*CONSTANTCOND*/ while (0) /* Finish helmet/jacket args with * [-m mac] -- $mnemonic $path $euid:$egid $cred_type:$cred NULL * then maybe run helmet to check any other stuff the config wants. */ if ((const char **)0 != ppcHelmet) { #if OP_MAC_SUPPORT if ((char *)0 != pcNewMac) { ppcHelmet[iHeTail++] = "-m"; ppcHelmet[iHeTail++] = pcNewMac; } #endif ppcHelmet[iHeTail++] = "--"; ppcHelmet[iHeTail++] = cmd->pcname; ppcHelmet[iHeTail++] = pcExecPath; snprintf(acCvt, sizeof(acCvt), "%ld:%ld", (long)uEffective, (long)gEffective); if ((char *)0 == (ppcHelmet[iHeTail++] = strdup(acCvt))) fatal("%s: %s", acOptHelmet, acBadAlloc); ppcHelmet[iHeTail++] = pcMay; ppcHelmet[iHeTail++] = (char *)0; } if ((const char **)0 != ppcHelmet && (const char *)0 != ppcHelmet[2]) { if (-1 == pipe(aiPipe)) { fatal("pipe: %s", strerror(errno)); } switch (wPid = fork()) { case -1: fatal("fork: %s", strerror(errno)); /*NOTREACHED*/ case 0: /* Check the helmet to make sure we're safe to ride. * Let's not give the checker our special redirections. */ (void)close(0); (void)open(acDevNull, O_RDONLY, 0666); (void)dup2(aiPipe[1], 1); (void)close(aiPipe[0]); if (-1 != iSecFd) { close(iSecFd); } if (-1 != iDirFd) { close(iDirFd); } for (i = 0; i < 3; ++i) { if (-1 != aiNewIO[i]) close(aiNewIO[i]); } /* When we drop uid the client can ptrace us to mess * with the return code: so we can't do that. Bummer. * Never drop uid to the original mortal in a helmet. */ execve(ppcHelmet[2], (char **)(ppcHelmet+2), (char **)ppcNewEnv); fprintf(stderr, "%s: execve: %s: %s\n", Progname, ppcHelmet[2], strerror(errno)); exit(EX_UNAVAILABLE); /*NOTREACHED*/ default: (void)close(aiPipe[1]); break; } if (0 != (iStatus = ExtInput(cmd, aiPipe[0], acOptHelmet, &ppcNewEnv, aiNewIO, pcSecPath))) { exit(iStatus); } close(aiPipe[0]); while (wPid != (wReap = wait(& iStatus))) { if (-1 == wReap) { fatal("wait: %s", strerror(errno)); } } if (0 == iStatus) { /* access allowed */ } else if (WIFEXITED(iStatus)) { exit(WEXITSTATUS(iStatus)); } else { exit(iStatus); } } /* Let's start a detached process from op, under daemon. */ for (i = 0; i < 3; ++i) { if (-1 == aiNewIO[i] || i == aiNewIO[i]) continue; if (iFdOldErr == i) { iFdOldErr = dup(i); (void)fcntl(iFdOldErr, F_SETFD, 1); } if (-1 == dup2(aiNewIO[i], i)) { RESTORE_STDERR(); fatal("dup2: %d: %s", aiNewIO[i], strerror(errno)); } close(aiNewIO[i]); } if ((char *)0 != FindOpt(cmd, acOptDaemon)) { switch ((i = fork())) { case -1: /* We really need the semantics of the double fork */ RESTORE_STDERR(); fprintf(stderr, "%s: fork: %s\n", Progname, strerror(errno)); exit(EX_UNAVAILABLE); /*NOTREACHED*/ case 0: break; default: exit(EX_OK); } #if defined(TIOCNOTTY) if (-1 != (i = open(acDevTty, 2))) { ioctl(i, TIOCNOTTY, 0); close(i); } #else setsid(); #endif } /* Wrap ourself $jacket -P $pid helmet-args, the args slot for the * pid is the helmet program path (or a NULL pointer), overwrite it. */ if ((const char **)0 != ppcHelmet && (const char *)0 != *ppcHelmet) { if (-1 == pipe(aiPipe)) { fatal("pipe: %s", strerror(errno)); } switch ((wPid = fork())) { case -1: RESTORE_STDERR(); fatal("fork: %s", strerror(errno)); /*NOTREACHED*/ case 0: (void)close(aiPipe[1]); if (0 != (iStatus = ExtInput(cmd, aiPipe[0], acOptJacket, &ppcNewEnv, aiNewIO, pcSecPath))) { exit(iStatus); } close(aiPipe[0]); break; default: if (-1 != iSecFd) { close(iSecFd); } if (-1 != iDirFd) { close(iDirFd); } dup2(aiPipe[1], 1); (void)close(aiPipe[1]); (void)close(aiPipe[0]); snprintf(acCvt, sizeof(acCvt), "%ld", (long)wPid); ppcHelmet[2] = acCvt; execve(*ppcHelmet, (char **)ppcHelmet, (char **)ppcNewEnv); RESTORE_STDERR(); fprintf(stderr, "%s: execve: %s: %s\n", Progname, ppcHelmet[0], strerror(errno)); exit(EX_UNAVAILABLE); } } if ((char *)0 != pcNewRoot && '\000' != *pcNewRoot && 0 > chroot(pcNewRoot)) { fatal("%s: %s: %s", acOptChroot, pcNewRoot, strerror(errno)); } if ((char *)0 != pcNewDir && 0 > chdir(pcNewDir)) { fatal("chdir: %s: %s", pcNewDir, strerror(errno)); } if ((const char *)0 != pcSetSession) { #if USE_PAM auto struct pam_conv local_conv; auto int iStatus, iExit; auto pam_handle_t *pPCLocal; local_conv.appdata_ptr = (void *)cmd; local_conv.conv = OpConv; if (PAM_SUCCESS != (iStatus = pam_start(acDefPam, pcSetSession, &local_conv, &pPCLocal))) { RESTORE_STDERR(); fatal("pam_start: %s: %s", pcSetSession, pam_strerror(pPCLocal, errno)); } (void)pam_set_item(pPCLocal, PAM_TTY, acDevTty); (void)pam_set_item(pPCLocal, PAM_RHOST, acMyHost); (void)pam_set_item(pPCLocal, PAM_RUSER, pcPerp); switch (iStatus = pam_open_session(pPCLocal, PAM_SILENT)) { case PAM_SUCCESS: break; default: RESTORE_STDERR(); fatal("%s: %s: %s", acOptSession, pcSetSession, pam_strerror(pPCLocal, iStatus)); /*NOTREACHED*/ } /* If you really need the session closed we have to fork, * which I think is a stretch. Sudo doesn't bother. -- ksb */ if ((char *)0 != pcEndSession) { switch ((wPid = fork())) { case -1: fatal("fork: %s", strerror(errno)); /*NOTREACHED*/ case 0: /* don't hold any input/output pipes */ if (0 == close(0)) (void)open(acDevNull, O_RDWR, 0666); if (0 == close(1)) (void)open(acDevNull, O_RDWR, 0666); iExit = 127; while (wPid != (wReap = wait(& iExit))) { if (-1 == wReap) { fatal("wait: %s", strerror(errno)); } } if (0 != strcmp(pcPerp, pcEndSession)) (void)pam_set_item(pPCLocal, PAM_USER, pcEndSession); iStatus = pam_close_session(pPCLocal, PAM_SILENT); (void)pam_end(pPCLocal, iStatus); /* PAM errors? The show is over, move along. */ exit(iExit); default: /* child */ break; } } else if (PAM_SUCCESS != (iStatus = pam_end(pPCLocal, iStatus))) { RESTORE_STDERR(); fatal("pam_end: %s", pam_strerror(pPCLocal, iStatus)); } #else fatal("%s: no support on this host", acOptSession); #endif } #if OP_MAC_SUPPORT if ((char *)0 != pcNewMac) { auto mac_t macLabel; if (-1 == mac_prepare_process_label(& macLabel)) { fatal("-m: mac_prepare: %s", strerror(errno)); } if (-1 == mac_from_text(&macLabel, pcNewMac)) { fatal("-m: %s: mac_from_text: %s", pcNewMac, strerror(errno)); } if (-1 == mac_set_proc(macLabel)) { fatal("-m: %s: mac_set_proc: %s", pcNewMac, strerror(errno)); } } #endif #if HAVE_setresuid if (setresuid(uReal, uEffective, uReal) < 0) { RESTORE_STDERR(); fatal("setresuid: %ld,%ld,%ld: %s", (long)uReal, (long)uEffective, (long)uReal, strerror(errno)); } #else if (uReal == uEffective) { if (setuid(uReal) < 0) { RESTORE_STDERR(); fatal("setuid: %ld: %s", (long)uReal, strerror(errno)); } } else if (setreuid(uReal, uEffective) < 0) { RESTORE_STDERR(); fatal("setreuid: %ld,%ld: %s", (long)uReal, (long)uEffective, strerror(errno)); } #endif syslog(((char *)0 == FindOpt(cmd, acOptNolog) ? LOG_INFO : LOG_NOTICE), "user %s SUCCEEDED to execute %s", pcPerp, pcCmd); if (0 == strcmp(pcExecPath, acEcho)) { static char acSpace[] = " ", acNL[] = "\n"; register int fNFlag, iV; register struct iovec *pIO; if ((char *)0 != ppcNewArgs[1] && 0 == strcmp("-m", ppcNewArgs[1])) ppcNewArgs += 2, fNFlag = 1; else ppcNewArgs += 1, fNFlag = 0; for (i = 0; (char *)0 != ppcNewArgs[i]; ++i) /* count'm */; pIO = calloc(i*2+fNFlag, sizeof(struct iovec)); iV = 0; for (i = 0; (char *)0 != ppcNewArgs[i]; ++i) { j = strlen(ppcNewArgs[i]); if (j > 1 && '\\' == ppcNewArgs[i][j-2] && 'c' == ppcNewArgs[i][j-1]) j -= 2, fNFlag = 1; if ((struct iovec *)0 == pIO) { if ((i > 0 && -1 == write(1, acSpace, 1)) || -1 == write(1, ppcNewArgs[i], j)) fatal("echo: write: %s", strerror(errno)); continue; } if (i > 0) { pIO[iV].iov_base = acSpace; pIO[iV++].iov_len = 1; } pIO[iV].iov_base = (char *)ppcNewArgs[i]; pIO[iV++].iov_len = j; } if (!fNFlag) { if ((struct iovec *)0 == pIO) { write(1, acNL, 1); } else { pIO[iV].iov_base = acNL; pIO[iV++].iov_len = 1; } } if ((struct iovec *)0 != pIO) { for (j = 0; j < iV; iV -= i, pIO += i) { i = iV > IOV_MAX ? IOV_MAX : iV; if (-1 == writev(1, pIO, i)) fatal("echo: writev: %s", strerror(errno)); } } return EX_OK; } if (aiNewIO[3] > -1) { for (i = getdtablesize()-1; i >= aiNewIO[3]; --i) { if (i == iFdOldErr) continue; close(i); } } execve(pcExecPath, (char **)ppcNewArgs, (char **)ppcNewEnv); ppcHold = environ; environ = (char **)ppcNewEnv; execvp(pcExecPath, (char **)ppcNewArgs); environ = ppcHold; RESTORE_STDERR(); #undef RESTORE_STDERR } /* Gads, after all that work we failed to execve! We can't undo the * syslog notice, but we should close the loop on missing binary files * or bad perms in via log, assuming a human reads it. */ syslog(LOG_ERR, "%s:%d: execvp: %s: %s (user=%s)", cmd->pcfile, cmd->iline, pcExecPath, strerror(errno), pcPerp); fprintf(stderr, "%s: execvp: %s: %s\n", Progname, pcExecPath, strerror(errno)); return EX_UNAVAILABLE; } /* The remains of the original main program from David Koblas, kinda */ static int OldMain(const int argc, const char **argv) { register int iFd, iArgs, iFdNull; register size_t iLen, iCredLen; register cmd_t *cmd; register struct passwd *pw; register char *pcEnv, *pcMem; auto const char *pcCred, *pcMay; auto const char *pcWrong; /* You can try, but we'll just open std{in,out,err} to /dev/null */ iFdNull = -1; for (iFd = 0; iFd < 3; ++iFd) { auto int iJunk; if (-1 == fcntl(iFd, F_GETFD, & iJunk)) { if (-1 == iFdNull) iFdNull = open(acDevNull, O_RDWR, 0666); else dup(iFdNull); } } /* We shouldn't ever get a NULL if mkcmd works */ if ((char *)0 == argv[0]) { fatal(acBadList); } if ((char *)0 == (pcEnv = getenv("USER")) || '\000' == *pcEnv) { pcEnv = getenv("LOGNAME"); } if ((char *)0 != pcEnv && '\000' != *pcEnv && (struct passwd *)0 != (pw = getpwnam(pcEnv)) && pw->pw_uid == getuid()) { /* found them by their preferred name */ } else if ((struct passwd *)0 == (pw = getpwuid(getuid()))) { fprintf(stderr, "%s: getpwuid: %ld: unmapped uid\n", Progname, (long)getuid()); exit(EX_NOUSER); } if ((char *)0 == (pcPerp = strdup(pw->pw_name)) || (char *)0 == (pcPerpHome = strdup(pw->pw_dir)) || (char *)0 == (pcPerpShell = strdup(pw->pw_shell))) fatal("setup: %s", acBadAlloc); /* Set the globals for fatal error conditions */ if ((char *)0 != pcUid && (char *)0 != pcGid) { static char acTempl[] = " as %s:%s"; iLen = ((sizeof(acTempl)+strlen(pcUid)+strlen(pcGid))|15)+1; pcMem = malloc(iLen); if ((char *)0 != pcMem) snprintf(pcMem, iLen, acTempl, pcUid, pcGid); pcCred = pcMem; } else if ((char *)0 != pcUid) { static char acTempl[] = " as login %s"; iLen = ((sizeof(acTempl)+strlen(pcUid))|15)+1; pcMem = malloc(iLen); if ((char *)0 != pcMem) snprintf(pcMem, iLen, acTempl, pcUid); pcCred = pcMem; } else if ((char *)0 != pcGid) { static char acTempl[] = " as group %s"; iLen = ((sizeof(acTempl)+strlen(pcGid))|15)+1; pcMem = malloc(iLen); if ((char *)0 != pcMem) snprintf(pcMem, iLen, acTempl, pcGid); pcCred = pcMem; } else { pcCred = ""; } iCredLen = strlen(pcCred)+2; if ((char *)0 != pcSecFile) { iCredLen += strlen(pcSecFile)+4; /* +strlen(" -f ") */ } for (iArgs = 0; iArgs < argc; ++iArgs) { iCredLen += 1+strlen(argv[iArgs]); } iCredLen = (iCredLen|15)+17; if ((char *)0 == (pcMem = (char *)malloc(iCredLen))) { fatal(acBadAlloc); } pcMem[0] = '\000'; for (iArgs = 0; iArgs < argc; ++iArgs) { (void) strcat(pcMem, " "); (void) strcat(pcMem, argv[iArgs]); } pcMem[0] = '\''; (void) strcat(pcMem, "'"); (void) strcat(pcMem, pcCred); if ((char *)0 != pcSecFile) { (void) strcat(pcMem, " -f "); (void) strcat(pcMem, pcSecFile); } pcCmd = pcMem; pcWrong = acBadMnemonic; cmd = Find(argv[0], (long)argc, argv, & pcWrong); if ((cmd_t *)0 == cmd) { fatal("%s: %s", argv[0], pcWrong); } cmd = Build(cmd); if ((char *)0 == (pcMay = Verify(cmd, argc, argv, &pcWrong))) { fatal("%s: %s", argv[0], pcWrong); } exit(Go(cmd, argc, argv, pcMay)); /*NOTREACHED*/ } #if SENTINEL /* A sentinel directory must be named for a group (and owned by it) (ksb) */ static int AnySentinel(const struct dirent *pDIDir) { return (struct group *)0 != getgrnam(pDIDir->d_name); } #endif static char *pcExtender, /* ".cf" usually */ *pcPrimary; /* "access.cf" usually */ /* Find all the files with the same extender as the primary config. (ksb) */ static int AnyConfig(const struct dirent *pDIThis) { register const char *pcDot; if ((const char *)0 != (pcDot = strrchr(pDIThis->d_name, '.'))) { if ((char *)0 == pcExtender) return 0; return 0 == strcmp(pcDot, pcExtender); } return (char *)0 == pcExtender; } /* Qsort so the first config file read is the primary one: this puts (ksb) * the access.cf DEFAULT for all at the head of the list, if there is * one at the top of that file. */ static int PickPrimary(const void *pvLeft, const void *pvRight) { register const char *pcLeft, *pcRight; pcLeft = ((const struct dirent **)pvLeft)[0]->d_name; pcRight = ((const struct dirent **)pvRight)[0]->d_name; if ((const char *)0 != pcPrimary) { if (0 == strcmp(pcLeft, pcPrimary)) return -1; if (0 == strcmp(pcRight, pcPrimary)) return 1; } return strcmp(pcLeft, pcRight); } /* Read the whole config file via the parser. (ksb) * Side-effect set pcAccess for $c/$C and Version. */ static int ReadFile(const char *pcTemplate) { register char *pcSlash, *pcFile; register int iCount, iErr, fd; auto int uPriv; auto struct dirent **ppDI; auto char acPath[MAXPATHLEN+4]; auto struct stat stFile; #if SENTINEL auto struct stat stSentDir; #endif if (strlen(pcTemplate) > sizeof(acPath)) { syslog(LOG_ERR, "configuration path too long (%s)", pcTemplate); fatal("open: %s: %s", pcTemplate, strerror(ENAMETOOLONG)); /*NOTREACHED*/ } uPriv = geteuid(); /* look for other *.cf in that directory -- ksb */ if ((char *)0 == (pcSlash = strrchr(strcpy(acPath, pcTemplate), '/')) || acPath == pcSlash) { syslog(LOG_ERR, "configuration filename missing slash: %s", acPath); fatal("Configuration file path should be absolute (%s)", acPath); } *pcSlash = '\000'; pcPrimary = pcSlash+1; pcExtender = strrchr(pcPrimary, '.'); #if SENTINEL { /* Allow the superuser to make alias for op that manage groups. * The group is represented by a directory next to access.cf that * is named for a group entry in /etc/group, and must be grouped * to that gid. The configuration is read from that directory and * the effective gid of op becomes that group (for $b/$B). * Yes, the sentinel can be a symbolic link to a dir, but it cannot * be named "OLD". The new effective uid is the owner of the dir, * or set to -1 to indicate no sentinel found. */ register struct group *grp; auto char acSentDir[MAXPATHLEN+4]; stSentDir.st_uid = -1; if (0 == getuid() || 0 == geteuid() || getegid() != getgid()) { setgrent(); iCount = scandir(acPath, & ppDI, (void *)AnySentinel, (void *)0); for (/* above */; iCount-- > 0; ++ppDI) { if (0 == strcmp(ppDI[0]->d_name, "OLD")) continue; snprintf(acSentDir, sizeof(acSentDir), "%s/%s", acPath, ppDI[0]->d_name); if (-1 == stat(acSentDir, &stSentDir) || (struct group *)0 == (grp = getgrnam(ppDI[0]->d_name)) || grp->gr_gid != stSentDir.st_gid || 0 != (stSentDir.st_mode&SENTINEL_MODE_MASK)) { if (fSanity) { fprintf(stderr, "%s: %s: sentinel directory permissions fail sanity check\n", Progname, acSentDir); } stSentDir.st_uid = -1; continue; } /* /usr/local/lib/op/op could even be a sentinel, if * there is a group named "op" -- don't do that. */ if (0 != strcmp(Progname, ppDI[0]->d_name)) { stSentDir.st_uid = -1; continue; } #if HAVE_setresgid if (-1 == setresgid(-1, stSentDir.st_gid, stSentDir.st_gid)) fatal("sentinel: setresgid: -1,%ld,%ld: %s", (long)stSentDir.st_gid, (long)stSentDir.st_gid, strerror(errno)); #else if (-1 == setegid(stSentDir.st_gid)) fatal("sentinel: setegid: %ld: %s", (long)stSentDir.st_gid, strerror(errno)); #endif uPriv = stSentDir.st_uid; pcFile = acSentDir+strlen(acSentDir); *pcFile++ = '/'; *pcFile = '\000'; strncat(acSentDir, pcPrimary, sizeof(acSentDir)); if (0 != strcmp(pcPrimary, pcFile)) fatal("sentinel: path too long"); pcSlash = strrchr(strcpy(acPath, acSentDir), '/'); *pcSlash = '\000'; pcPrimary = pcSlash+1; pcExtender = strrchr(pcPrimary, '.'); break; } endgrent(); } } #endif iCount = scandir(acPath, & ppDI, (void *)AnyConfig, PickPrimary); if (iCount < 1 || 0 != strcmp(pcPrimary, ppDI[0]->d_name)) { *pcSlash = '/'; syslog(LOG_ERR, "missing main configuration %s", acPath); fatal("stat: %s: %s", acPath, strerror(ENOENT)); } if (uPriv == getuid()) uPriv = -1; while (iCount-- > 0) { snprintf(pcSlash, &acPath[sizeof(acPath)]-pcSlash, "/%s", ppDI[0]->d_name); if ((char *)0 == (pcFile = strdup(acPath))) fatal("configuration: %s", acBadAlloc); if ((const char *)0 == pcAccess) pcAccess = pcFile; ++ppDI; if (-1 == (fd = open(pcFile, O_RDONLY))) { iErr = errno; syslog(LOG_ERR, "open: %s: %s", pcFile, strerror(errno)); fatal("open: %s: %s", pcFile, strerror(iErr)); } if (-1 == fstat(fd, &stFile) || (-1 != uPriv && stFile.st_uid != uPriv) || ((stFile.st_mode & 0037) != 0)) { /* no perm */ syslog(LOG_ERR, "Permission problems on %s", pcFile); fatal("Permission problems on %s", pcFile); } OneFile(fd, pcFile); (void)close(fd); } #if SENTINEL if (-1 != stSentDir.st_uid && -1 == seteuid(stSentDir.st_uid)) { fatal("sentinel: seteuid: %ld: %s", (long)stSentDir.st_uid, strerror(errno)); } #endif /* We don't free the scandir result, but we execve soon enough */ return 0; }