From ec7bbbf0a56ceebd4ec763360520acd2c16e24c2 Mon Sep 17 00:00:00 2001 From: Mattias Andrée Date: Mon, 2 Nov 2026 19:16:28 +0100 Subject: First commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mattias Andrée --- fsshield.c | 323 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 fsshield.c (limited to 'fsshield.c') diff --git a/fsshield.c b/fsshield.c new file mode 100644 index 0000000..4518e4d --- /dev/null +++ b/fsshield.c @@ -0,0 +1,323 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include + +NUSAGE(125, "[-a file] ... [-r file] ... [-t file] ... [-nR] utility [argument] ..."); + + +enum type { + ACCESSIBLE, + READONLY, + TMPFS +}; + +struct file { + const char *path; + enum type type; +}; + + +static struct file *files = NULL; +static size_t nfiles = 0u; + +static const struct file preset[] = { + {".", ACCESSIBLE}, + {"/tmp", TMPFS}, + {"/var/tmp", TMPFS}, + {"/var/empty", READONLY}, + {"/etc/passwd", READONLY}, + {"/usr", READONLY}, + {"/bin", READONLY}, + {"/sbin", READONLY}, + {"/lib", READONLY}, + {"/lib64", READONLY}, + {"/lib32", READONLY}, + {"/libexec", READONLY} +}; + +static size_t npreset = ELEMSOF(preset); + +static char *cwd; + + +static void +add(const char *path, enum type type) +{ + if (!*path) + usage(); + files[nfiles].path = path; + files[nfiles].type = type; + nfiles += 1u; +} + + +static void +make_dirs(char *path, mode_t mode, int make_top) +{ + char *p; + for (p = &path[1u]; *p; p++) { + if (*p != '/') + continue; + *p = '\0'; + if (mkdir(path, mode) && errno != EEXIST) + eprintf("mkdir %s %03o:", path, mode); + *p = '/'; + } + if (make_top) + if (mkdir(path, mode) && errno != EEXIST) + eprintf("mkdir %s %03o:", path, mode); +} + + +static void +touch(const char *path, mode_t mode) +{ + int fd; + if (!faccessat(AT_FDCWD, path, F_OK, AT_SYMLINK_NOFOLLOW)) + return; + fd = open(path, O_CREAT | O_WRONLY, mode); + if (fd < 0) + eprintf("open %s O_CREAT|O_WRONLY %03o:", path, mode); + close(fd); +} + + +static char * +clean_path(const char *path) +{ + char *ret, *r, *w; + + if (*path != '/') { + ret = emalloc(strlen(cwd) + strlen(path) + sizeof("/")); + stpcpy(stpcpy(stpcpy(ret, cwd), "/"), path); + } else { + ret = estrdup(path); + } + + r = w = &ret[1u]; + + while (*r) { + while (*r == '/') r++; + if (r[0u] == '.' && r[1u] == '/') { + r = &r[2u]; + } else if (r[0u] == '.' && !r[1u]) { + break; + } else if (r[0u] == '.' && r[1u] == '.' && (r[2u] == '/' || !r[2u])) { + if (--w != ret) + while (*w != '/') + w--; + w++; + r = &r[2u]; + if (!*r++) + break; + } else { + do { + *w++ = *r; + } while (*r && *r++ != '/'); + } + } + + if (w[-1] == '/' && w != &ret[1u]) + w--; + + *w = '\0'; + + if (!strcmp(ret, "/.oldroot")) + eprintf("/.oldroot must not be included as an accessible path"); + + return ret; +} + + +static int +cmpfiles(const void *av, const void *bv) +{ + const struct file *a = av; + const struct file *b = bv; + int cmp = strcmp(a->path, b->path); + return cmp ? cmp : (int)a->type - (int)b->type; +} + + +static int +isdir(const char *path) +{ + struct stat st; + return stat(path, &st) || S_ISDIR(st.st_mode); +} + + +static void +setup_filesystem(void) +{ + struct mount_attr attr, ro = {.attr_set = MOUNT_ATTR_RDONLY}; + char root[] = "/tmp/fsshield.XXXXXX"; + char *pathbuf; + size_t i, len, maxlen = sizeof("/.oldroot") - 1u; + int fds[2]; + pid_t pid; + + if (socketpair(PF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, fds)) + eprintf("socketpair PF_LOCAL SOCK_STREAM|SOCK_CLOEXEC 0:"); + + if (!mkdtemp(root)) + eprintf("mkdtemp:"); + + pid = fork(); + switch (pid) { + case -1: + eprintf("fork:"); + case 0: + close(fds[1]); + pid = fork(); + switch (pid) { + case -1: + eprintf("fork:"); + case 0: + if (write(fds[0], &(char){0}, 1u) != 1) + eprintf("write :"); + if (read(fds[0], &(char){0}, 1u) != 0) + eprintf("read :"); + close(fds[0]); + if (rmdir(root)) + weprintf("rmdir %s:", root); + break; + default: + break; + } + exit(0); + default: + close(fds[0]); + switch (read(fds[1], &(char){0}, 1u)) { + case 0: + exit(125); + case 1: + break; + default: + eprintf("read :"); + } + waitpid(pid, NULL, 0); + break; + } + + for (i = 0u; i < nfiles; i++) { + len = strlen(files[i].path); + maxlen = MAX(maxlen, len); + } + pathbuf = emalloc(maxlen + sizeof(root)); + + if (unshare(CLONE_NEWNS)) + eprintf("unshare CLONE_NEWNS:"); + + if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) + eprintf("mount NULL / NULL MS_REC|MS_PRIVATE NULL:"); + + if (mount("tmpfs", root, "tmpfs", MS_NOSUID | MS_NODEV, "mode=0755")) + eprintf("mount tmpfs %s tmpfs MS_NOSUID|MS_NODEV mode=0755:", root); + + for (i = 0u; i < nfiles; i++) { + stpcpy(stpcpy(pathbuf, root), files[i].path); + switch (files[i].type) { + case ACCESSIBLE: + case READONLY: + if (isdir(files[i].path)) { + make_dirs(pathbuf, 0755, 1); + if (mount(files[i].path, pathbuf, NULL, MS_BIND | MS_REC, NULL)) + eprintf("mount %s %s NULL MS_BIND|MS_REC NULL:", files[i].path, pathbuf); + } else { + make_dirs(pathbuf, 0755, 0); + touch(pathbuf, 0644); + if (mount(files[i].path, pathbuf, NULL, MS_BIND, NULL)) + eprintf("mount %s %s NULL MS_BIND NULL:", files[i].path, pathbuf); + } + if (files[i].type == READONLY) { + attr = ro; + if (mount_setattr(AT_FDCWD, pathbuf, AT_RECURSIVE, &attr, sizeof(attr))) + eprintf("mount_setattr AT_FDCWD %s AT_RECURSIVE {.attr_set = MOUNT_ATTR_RDONLY}:", pathbuf); + } + break; + case TMPFS: + make_dirs(pathbuf, 01777, 1); + if (mount("tmpfs", pathbuf, "tmpfs", MS_NOSUID | MS_NODEV, "mode=1777")) + eprintf("mount tmpfs %s tmpfs MS_NOSUID|MS_NODEV mode=1777:", pathbuf); + break; + default: + abort(); + } + } + + stpcpy(stpcpy(pathbuf, root), "/.oldroot"); + make_dirs(pathbuf, 0700, 1); + if (chdir(root)) + eprintf("chdir %s:", root); + if (syscall(SYS_pivot_root, ".", ".oldroot")) + eprintf("pivot_root . .oldroot (from %s):", root); + if (chdir(cwd)) + eprintf("chdir %s:", cwd); + if (umount2("/.oldroot", MNT_DETACH)) + eprintf("umount2 /.oldroot MNT_DETACH"); + if (rmdir("/.oldroot")) + eprintf("rmdir /.oldroot"); + free(pathbuf); +} + + +int +main(int argc, char *argv[]) +{ + size_t i, n; + enum type cwd_access = ACCESSIBLE; + + libsimple_default_failure_exit = 125; + + argv0 = argv[0]; + files = ecalloc((size_t)argc + npreset, sizeof(*files)); + + ARGBEGIN { + case 'a': add(ARG(), ACCESSIBLE); break; + case 'r': add(ARG(), READONLY); break; + case 't': add(ARG(), TMPFS); break; + case 'n': npreset = 1u; break; + case 'R': cwd_access = READONLY; break; + default: + usage(); + } ARGEND; + + if (!argc) + usage(); + + files[nfiles] = preset[0u]; + files[nfiles++].type = cwd_access; + for (i = 1u; i < npreset; i++) + if (!faccessat(AT_FDCWD, preset[i].path, F_OK, AT_SYMLINK_NOFOLLOW)) + files[nfiles++] = preset[i]; + + cwd = get_current_dir_name(); + if (!cwd) + eprintf("get_current_dir_name:"); + if (*cwd != '/') + abort(); + + for (i = 0u; i < nfiles; i++) + files[i].path = clean_path(files[i].path); + + qsort(files, nfiles, sizeof(*files), &cmpfiles); + for (i = n = 1u; i < nfiles; i++) + if (strcmp(files[i].path, files[n - 1u].path)) + files[n++] = files[i]; + nfiles = n; + + setup_filesystem(); + unsetenv("PWD"); + + if (setegid(getgid())) + eprintf("setegid :"); + if (seteuid(getuid())) + eprintf("seteuid :"); + + execvp(argv[0], argv); + enprintf(errno == ENOENT ? 127 : 126, "execvp %s:", argv[0]); +} -- cgit v1.3.1