#!/usr/bin/env mkcmd # We layer other jackets one-on the next to make a new jacket. (ksb) # Accept a list of jackets in $COAT as # COAT=/path/to/fist:second:/new/dir/third:... # Each jacket wraps those after it, the first one must pass before the # second one starts. If all the jackets/helmet pass we show them # each the exit code of the access (or jacket to the right) to get # them to map the exit code as they see fit. (For example the exit # code 69 might map to 77, if there were a jacket to do that mapping.) # # The common usage might be to lock 2 resources with a locking jacket. # Another might allow both a ttyowner check with a timestamp check. # # $Compile(*): ${mkcmd:-mkcmd} -n %F %f && ${cc:-gcc} -Wall %s -g %F.c -o %[F.-$] # $Compile: ${mkcmd:-mkcmd} -n %F %f && ${cc:-gcc} -Wall -g %F.c -o %[F.-$] # $InstallDir: ${install:-install} -dr ${DESTDIR}/usr/local/libexec/jacket # $Install: ( [ -f %F ] || %b -mCompile %f ) && ${install:-install} -c -m 0711 %F ${DESTDIR}/usr/local/libexec/jacket/%F # from '' from '' from '' from '' from '' from '' from '' from '' from '' from '' from '' require "std_help.m" "std_version.m" require "api_jacket.m" require "util_ppm.m" require "ext.m" %i #if !defined(COAT_LIST) #define COAT_LIST "COAT" #endif #if !defined(COAT_REVEAL) #define COAT_REVEAL "COAT_REVEAL" #endif #if !defined(HAVE_VOID_UNSETENV) #define HAVE_VOID_UNSETENV 1 #endif #if !defined(HAVE_SETPROCTITLE) #define HAVE_SETPROCTITLE (defined __NetBSD__ || defined __FreeBSD__ || defined __OpenBSD__) #endif static const char rcsid[] = "$Id: coat.m,v 1.22 2012/10/19 20:15:04 ksb Exp $", acCoatList[] = COAT_LIST, acReveal[] = COAT_REVEAL, acDevNull[] = "/dev/null"; %% augment action 'V' { user "CoatVersion();" } action 'H' { named "Explain" help "explain the usage of the environment configuration" update "%n(stdout);" aborts "exit(EX_OK);" } list { hidden named "Coat" help "silent running" param "" update "%n(argc, argv);" } exit { update "exit(EX_UNAVAILABLE);" } %c /* mkcmd hook for detailed version information (ksb) */ static void CoatVersion() { printf("%s: list: %s\n", progname, acCoatList); printf("%s: reveal: %s\n", progname, acReveal); } /* We could do better here (ksb) */ static void Explain(FILE *fp) { fprintf(fp, "%s: nest multiple jackets in layers\n", progname); fprintf(fp, "%-13s a colon separate list of helmets or jackets\n", acCoatList); fprintf(fp, "%-13s the standard reveal prefix hook\n", acReveal); } /* Try to reproduce the same exit code we reaped from wait (ksb) */ static void DupExit(int iCode) { /* Try to exit with a signal, if we saw one */ if (iCode < 127 && 0 != iCode) kill(getpid(), iCode); else iCode >>= 8; exit(iCode); abort(); } /* The shim waits for an exit code on stdin, then returns that (ksb) */ static int Shim() { auto int iCode; #if HAVE_SETPROCTITLE (void)setproctitle("shim for pid %ld", (long)getppid()); #endif if (1 != scanf(" %d", & iCode)) return EX_PROTOCOL; DupExit(iCode); return EX_OSERR; } extern char **environ; /* Fake op's reveal feature in our current environment (ksb) * Return 1 if we updated our list variable, we we don't also remove it. */ static int InternalReveal(char *pcPrefix, size_t wLen) { register int j, iRet; register unsigned int iKeep; register char *pcEq; register char **ppcList; iRet = 0; #if DEBUG printf("# %s internal reveal `%.*s'\n", progname, (int)wLen, pcPrefix); fflush(stdout); #endif for (iKeep = 0; (char *)0 != environ[iKeep]; ++iKeep) { /* count them all */ } if ((char **)0 == (ppcList = calloc((iKeep|7)+1, sizeof(char *)))) { if (fJacket) printf("%d\n", EX_OSERR); exit(EX_OSERR); } iKeep = 0; for (j = 0; (char *)0 != environ[j]; ++j) { if (0 != strncmp(environ[j], pcPrefix, wLen)) continue; ppcList[iKeep++] = strdup(environ[j]); } for (j = 0; j < iKeep; ++j) { if ((char *)0 == (pcEq = strchr(ppcList[j], '='))) { unsetenv(ppcList[j]); printf("-%s\n", ppcList[j]); fflush(stdout); continue; } *pcEq = '\000'; if (0 == strcmp(acCoatList, ppcList[j]+wLen)) iRet = 1; printf("-%s\n", ppcList[j]); unsetenv(ppcList[j]); if (0 != setenv(ppcList[j]+wLen, pcEq+1, 1)) printf("# setenv failed!\n"); printf("\"$%s", ppcList[j]+wLen); *pcEq = '='; do switch (*pcEq) { case '\"': printf("\\d"); continue; case '\'': printf("\\q"); continue; case '`': printf("\\o"); continue; case '\n': printf("\\n"); continue; case '\t': printf("\\t"); continue; default: printf("%c", *pcEq); continue; } while ('\000' != *++pcEq); printf("\"\n"); fflush(stdout); } return iRet; } /* We apply the normal ksb path compression here (ksb) * that is to say "/usr/local/libexec/jacket/a:b:c" uses the directory * from the first component to find the others (/usr/local/libexec/jacket/b, * and .../jacket/c are implied). */ static void /*ARGSUSED1*/ Coat(int argc, char **argv) { auto char acLayer[PATH_MAX+4]; auto char *pcLayers, *pcNext; register char *pcEnd, *pcScan; typedef struct LAnode { char *pcpath; /* path to jacket/helmet */ int fdshim; /* write to shim */ int fdenv; /* read from jacket */ int icode; /* exit code returned (debug) */ pid_t wpid; /* pid of the jacket process */ } LAYER; register LAYER *pLA; register int i; auto char acCvt[64]; /* %ld l(2^128)/l(10) = 38 */ auto int iRet, aiShim[2], aiEnv[2]; auto unsigned iPly; aiShim[0] = aiShim[1] = -1; /* Because we're not going to check this everyplace, do it here. * op does this too, but we may not be called from op. */ if (-1 == fcntl(0, F_GETFD, 1) && 0 != open(acDevNull, O_RDONLY, 0666)) { if (fJacket) printf("%d\n", EX_OSERR); exit(EX_OSERR); } if (-1 == fcntl(1, F_GETFD, 1) && 1 != open(acDevNull, O_WRONLY, 0666)) { if (fJacket) printf("%d\n", EX_OSERR); exit(EX_OSERR); } if (-1 == fcntl(2, F_GETFD, 1) && 2 != open(acDevNull, O_RDWR, 0666)) { if (fJacket) printf("%d\n", EX_OSERR); exit(EX_OSERR); } /* Recover and remove our data from op's environment */ if ((char *)0 == (pcLayers = getenv(acCoatList))) { if (fJacket) printf("%ld\n", (long int)EX_DATAERR); exit(EX_DATAERR); } /* We'll reveal the next level of magic before we go on. * That way we can call ourself. Most jackets reveal later. * If you want to reveal later put sheval as the first jacket * and use that to reveal after we fork. */ { register char *pcPrefix; if ((char *)0 != (pcPrefix = getenv(acReveal))) { (void)unsetenv(acReveal); printf("-%s\n", acReveal); if (!InternalReveal(pcPrefix, strlen(pcPrefix))) { (void)unsetenv(acCoatList); printf("-%s\n", acCoatList); } } else { printf("-%s\n", acCoatList); } #if DEBUG if ((char *)0 == (pcPrefix = getenv(acCoatList))) { printf("# no %s in child environment\n", acCoatList); } #endif fflush(stdout); } /* Build the books to keep track of the layers, if you have * only 1 layer we _could_ just run that jacket/helmet. */ for (iPly = 1, pcScan = pcLayers; '\000' != *pcScan; /* below */) { if (':' == *pcScan++) ++iPly; } if ((void *)0 == (pLA = calloc((iPly|3)+1, sizeof(LAYER)))) { fprintf(stderr, "%s: calloc: %d plys of size %lu: %s\n", progname, (iPly|3)+1, (unsigned long)sizeof(LAYER), strerror(errno)); if (fJacket) printf("%d\n", EX_OSERR); exit(EX_OSERR); } pLA[0].fdshim = open(acDevNull, O_RDWR, 0666); iRet = EX_OK; strncpy(acLayer, argv[0], sizeof(acLayer)); for (i = 0, pcScan = pcLayers; (char *)0 != pcScan; pcScan = pcNext, ++i) { if ((char *)0 != (pcNext = strchr(pcScan, ':'))) { /* We could take empty layers to be a copy of * ourself (coat), but that is error-prone. */ /* skiping m/:+/ here */ while (':' == *pcNext) *pcNext++ = '\000'; if ('\000' == *pcNext) pcNext = (char *)0; } if ('/' == *pcScan) { strncpy(acLayer, pcScan, PATH_MAX); } else if ((char *)0 != (pcEnd = strrchr(acLayer, '/'))) { *++pcEnd = '\000'; strncat(acLayer, pcScan, PATH_MAX); } else { strncpy(acLayer, pcScan, PATH_MAX); } pLA[i].pcpath = strdup(acLayer); #if DEBUG printf("# ply %d runs %s\n", i, acLayer); fflush(stdout); #endif } /* Fire up each layer and read+react to the env mod stream. */ for (i = 0; i < iPly; ++i) { auto size_t wLen; register pid_t wShim; register char *pc; if (fJacket && -1 == pipe(aiShim)) { fprintf(stderr, "%s: shim pipe: %s\n", progname, strerror(errno)); iRet = EX_OSERR; break; } if (-1 == pipe(aiEnv)) { fprintf(stderr, "%s: env pipe: %s\n", progname, strerror(errno)); iRet = EX_OSERR; break; } switch (pLA[i].wpid = fork()) { case -1: fprintf(stderr, "%s: fork: %s\n", progname, strerror(errno)); iRet = EX_OSERR; break; case 0: /* jacket/helmet process */ /* clean up the already open shim fds */ { register int j; for (j = 0; j < i; ++j) close(pLA[j].fdshim); } close(1); dup(aiEnv[1]); close(aiEnv[1]); close(aiEnv[0]); pcScan = pLA[i].pcpath; if ((char *)0 != (pcScan = strrchr(pcScan, '/'))) argv[0] = pcScan+1; else argv[0] = pLA[i].pcpath; if (fJacket) { register char **ppc; (void)fcntl(aiShim[1], F_SETFD, 1); switch (wShim = fork()) { case -1: /* Oh look out */ exit(EX_OSERR); /*NOTREACHED*/ case 0: close(0); dup(aiShim[0]); close(aiShim[0]); /* unblock the esclation */ close(aiShim[1]); close(1); dup(2); exit(Shim()); /*NOTREACHED*/ default: close(aiShim[0]); break; } /* Find -P pid and replace it, op always (ksb) * puts it first, but may not in future. * This doesn't parse options perfectly, * the -P must be the first in a bundle, and * a parameter -P might fool it. */ snprintf(acCvt, sizeof(acCvt), "-P%ld", (long int)wShim); for (ppc = argv; (char *)0 != *ppc; ++ppc) { if ('-' != ppc[0][0]) { continue; } /* EoO failed to find -Ppid (XXX) */ if ('-' == ppc[0][1] && '\000' == ppc[0][2]) { break; } if ('P' != ppc[0][1]) { continue; } /* bundled together */ if ('\000' != ppc[0][2]) { ppc[0] = acCvt; break; } /* missing on end of argv (XXX) */ if ((char *)0 == ppc[1]) { break; } /* separated from option */ ppc[1] = acCvt+2; break; } } execve(pLA[i].pcpath, argv, environ); #if defined(COAT_SEARCH) execvp(pLA[i].pcpath, argv); fprintf(stderr, "%s: execvp: %s\n", progname, strerror(errno)); #else fprintf(stderr, "%s: execve: %s\n", progname, strerror(errno)); #endif if (fJacket) { /* we set close-on-exec so we still have it */ write(aiShim[1], "69\n", 3); } exit(EX_UNAVAILABLE); /*NOTREACHED*/ default: /* parent */ close(aiEnv[1]); if (fJacket) { close(aiShim[0]); } pLA[i+1].fdshim = aiShim[1]; pLA[i].fdenv = aiEnv[0]; pLA[i].icode = -1; break; } if (EX_OK != iRet) { break; } /* Fake ExtInput function from op.m about line 6975, * but we also echo the pseudo-script to op (or ourself). * We trust that the input lines are mostly \n terminated, * which op.m doesn't really trust. */ while ((char *)0 != (pc = ExtLine(pLA[i].fdenv, &wLen))) { auto char *pcEol; if ((int)wLen != write(1, pc, wLen)) { break; } /* The ext Line took off the \n or \r\n, put it back */ write(1, "\n", 1); if (wLen > 0 && '\n' == pc[wLen]) { pc[wLen--] = '\000'; } /* op does NOT put the \r, but some network may have */ if (wLen > 0 && '\r' == pc[wLen]) { pc[wLen--] = '\000'; } /* If you like add, && '\"' == pc[wLen], but I don't. */ if ('\"' == pc[0]) { wLen = ExtDecode(pc); } switch (pc[0]) { case '#': #if DEBUG fprintf(stderr, "%s\n", pc); break; #endif case '\r': case '\n': /* being liberal with the op spec */ case '\000': break; case '$': /* set by name or name=value */ /* XXX We don't have the "old environment" * to consult for the $env case, set to empty. * It should be set properly in the op process * so it will be fine in the escalated cmd. * But any nested EnvAuth check will fail :-(. */ if ((char *)0 != (pcEol = strchr(pc+1, '='))) *pcEol++ = '\000'; else pcEol = ""; if (0 != setenv(pc+1, pcEol, 1)) fprintf(stderr, "setenv failed!\n"); break; case '-': /* delete by name */ unsetenv(pc+1); break; case '~': /* reveal names */ --wLen, ++pc; (void)InternalReveal(pc, wLen); break; case '&': /* We can't really do I/O redirections */ break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': iRet = strtol(pc, &pcEol, 0); if ('\000' == *pcEol) break; /*FALLTHROUGH*/ default: /* not sure this is the correct spelling LLL */ fprintf(stderr, "%s: %s: %s%s", progname, pLA->pcpath, pc, strchr(pc, '\n') ? "" : "\n"); iRet = EX_PROTOCOL; break; } } close(pLA[i].fdenv); /* aka close(aiEnv[0]); */ pLA[i].fdenv = -1; if (fJacket) { continue; } /* Helmet checks for exit code */ if (-1 == waitpid(pLA[i].wpid, &pLA[i].icode, 0)) { pLA[i].icode = EX_OSERR; break; } pLA[i].wpid = -1; if (EX_OK != iRet || EX_OK != (iRet = pLA[i].icode)) { break; } } if (EX_OK != iRet) { if (fJacket) printf("%d\n", iRet); fflush(stdout); /* XXX signal ourself? */ exit(iRet < 256 ? iRet : iRet >> 8); } /* Add the escalated process layer, note that shim is already set */ i = iPly++; pLA[i].pcpath = "escalated"; /* pLA[i].fdshim set in loop above */ pLA[i].fdenv = -1; pLA[i].icode = -1; pLA[i].wpid = iPid; /* Tell op (or us) to release the actual escalation process. */ close(1); (void)open(acDevNull, O_RDWR, 0666); /* If we are a Helmet we are done. Reaped others and good to go. */ if (!fJacket) { exit(iRet); } /* We are a jacket so we must proxy the exit codes from the * escalated process back though the jackets, and any of the * jackets could exit in any order. Usually you'd expet the * escalated process to exit first, then the deepest jacket, * outwards to us, but that doesn't always happen. So Look Out. * Oh, and we might get stray child pids, just like make(1) does. */ { register pid_t wReap; register int iScan, iRemain; auto int iStatus; /* Wait for the deepest ply in the stack, send that status * to the shim above and repeat until no more plys. Note that * a jacket might interpret an exit code (viz. invert it). */ iRemain = iPly; while (0 < (wReap = wait(& iStatus))) { snprintf(acCvt, sizeof(acCvt), "%ld", (long int)iStatus); for (iScan = 0; iScan < iPly; ++iScan) { if (wReap == pLA[iScan].wpid) break; } /* found some stray process we do not love */ if (iScan == iPly) { continue; } /* Ask the next plys to map the exit code for us. */ #if DEBUG printf("# coat%d: sending status %s from process %ld to next ply via fd %d\n", iScan, acCvt, (long int)pLA[iScan].wpid, pLA[iScan].fdshim); fflush(stdout); #endif write(pLA[iScan].fdshim, acCvt, strlen(acCvt)); close(pLA[iScan].fdshim); pLA[iScan].fdshim = -1; pLA[iScan].icode = iStatus; pLA[iScan].wpid = -1; while (iScan > 0 && -1 == pLA[iScan].fdshim) --iScan; iPid = pLA[iScan].wpid; --iRemain; } iStatus = pLA[0].icode; exit(iStatus < 256 ? iStatus : iStatus >> 8); } }