/* * Copyright (C) 2014-2023 Firejail Authors * * This file is part of firejail project * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "firejail.h" #include #include #include #include #include #include #ifndef O_PATH #define O_PATH 010000000 #endif #define TOP_MAX 64 // maximum number of top level directories // mountinfo functionality test; // 1. enable TEST_MOUNTINFO definition // 2. run firejail --whitelist=/any/directory //#define TEST_MOUNTINFO static size_t homedir_len = 0; // cache length of homedir string static size_t runuser_len = 0; // cache length of runuser string static char *runuser = NULL; static void whitelist_error(const char *path) { assert(path); fprintf(stderr, "Error: invalid whitelist path %s\n", path); exit(1); } static int whitelist_mkpath(const char *parentdir, const char *relpath, mode_t mode) { // starting from top level directory int parentfd = safer_openat(-1, parentdir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (parentfd < 0) errExit("open"); // top level directory mount id int mountid = get_mount_id(parentfd); if (mountid < 0) { close(parentfd); return -1; } // work on a copy of the path char *dup = strdup(relpath); if (!dup) errExit("strdup"); // only create leading directories, don't create the file char *p = strrchr(dup, '/'); if (!p) { // nothing to do free(dup); return parentfd; } *p = '\0'; // traverse the path, return -1 if a symlink is encountered int fd = -1; int done = 0; char *tok = strtok(dup, "/"); assert(tok); while (tok) { // create the directory if necessary if (mkdirat(parentfd, tok, mode) == -1) { if (errno != EEXIST) { if (arg_debug || arg_debug_whitelists) perror("mkdir"); close(parentfd); free(dup); return -1; } } else done = 1; // open the directory fd = openat(parentfd, tok, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) { if (arg_debug || arg_debug_whitelists) perror("open"); close(parentfd); free(dup); return -1; } // different mount id indicates earlier whitelist mount if (get_mount_id(fd) != mountid) { if (arg_debug || arg_debug_whitelists) printf("Debug %d: whitelisted already\n", __LINE__); close(parentfd); close(fd); free(dup); return -1; } // move on to next path segment close(parentfd); parentfd = fd; tok = strtok(NULL, "/"); } if (done) { char *abspath; if (asprintf(&abspath, "%s/%s", parentdir, relpath) < 0) errExit("asprintf"); fs_logger2("mkpath", abspath); free(abspath); } free(dup); return fd; } static void whitelist_file(const TopDir * const top, const char *path) { EUID_ASSERT(); assert(top && path); // check if path is inside top level directory size_t top_pathlen = strlen(top->path); if (strncmp(top->path, path, top_pathlen) != 0 || path[top_pathlen] != '/') return; const char *relpath = path + top_pathlen + 1; // open mount source, using a file descriptor that refers to the // top level directory // as the top level directory was opened before mounting the tmpfs // we still have full access to all directory contents // take care to not follow symbolic links (top->fd was obtained without // following a link, too) int fd = safer_openat(top->fd, relpath, O_PATH|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) { if (arg_debug || arg_debug_whitelists) printf("Debug %d: skip whitelist %s\n", __LINE__, path); return; } struct stat s; if (fstat(fd, &s) == -1) errExit("fstat"); if (S_ISLNK(s.st_mode)) { if (arg_debug || arg_debug_whitelists) printf("Debug %d: skip whitelist %s\n", __LINE__, path); close(fd); return; } // now modify the tmpfs: // create mount target as root, except if inside home or run/user/$UID directory if (strcmp(top->path, cfg.homedir) != 0 && strcmp(top->path, runuser) != 0) EUID_ROOT(); // create path of the mount target int fd2 = whitelist_mkpath(top->path, relpath, 0755); if (fd2 == -1) { if (arg_debug || arg_debug_whitelists) printf("Debug %d: skip whitelist %s\n", __LINE__, path); close(fd); EUID_USER(); return; } // get file name of the mount target const char *file = gnu_basename(relpath); // create mount target itself if necessary // and open it, a symlink is not allowed int fd3 = -1; if (S_ISDIR(s.st_mode)) { // directory bar can exist already: // firejail --whitelist=/foo/bar/baz --whitelist=/foo/bar if (mkdirat(fd2, file, 0755) == -1 && errno != EEXIST) { if (arg_debug || arg_debug_whitelists) { perror("mkdir"); printf("Debug %d: skip whitelist %s\n", __LINE__, path); } close(fd); close(fd2); EUID_USER(); return; } fd3 = openat(fd2, file, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); } else // create an empty file // fails with EEXIST if it is whitelisted already fd3 = openat(fd2, file, O_RDONLY|O_CREAT|O_EXCL|O_CLOEXEC, S_IRUSR|S_IWUSR); if (fd3 == -1) { if (errno != EEXIST && (arg_debug || arg_debug_whitelists)) { perror("open"); printf("Debug %d: skip whitelist %s\n", __LINE__, path); } close(fd); close(fd2); EUID_USER(); return; } close(fd2); if (arg_debug || arg_debug_whitelists) printf("Whitelisting %s\n", path); EUID_ROOT(); if (bind_mount_by_fd(fd, fd3)) errExit("mount bind"); EUID_USER(); // check the last mount operation MountData *mptr = get_last_mount(); // will do exit(1) if the mount cannot be found #ifdef TEST_MOUNTINFO printf("TEST_MOUNTINFO\n"); mptr->dir = "foo"; #endif // confirm the file was mounted on the right target // strcmp does not work here, because mptr->dir can be a child mount size_t path_len = strlen(path); if (strncmp(mptr->dir, path, path_len) != 0 || (*(mptr->dir + path_len) != '\0' && *(mptr->dir + path_len) != '/')) errLogExit("invalid whitelist mount"); // No mounts are allowed on top level directories. A destination such as "/etc" is very bad! // - there should be more than one '/' char in dest string if (mptr->dir == strrchr(mptr->dir, '/')) errLogExit("invalid whitelist mount"); close(fd); close(fd3); fs_logger2("whitelist", path); } static void whitelist_symlink(const TopDir * const top, const char *link, const char *target) { EUID_ASSERT(); assert(top && link && target); // confirm link is inside top level directory // this should never fail size_t top_pathlen = strlen(top->path); assert(strncmp(top->path, link, top_pathlen) == 0 && link[top_pathlen] == '/'); const char *relpath = link + top_pathlen + 1; // create link as root, except if inside home or run/user/$UID directory if (strcmp(top->path, cfg.homedir) != 0 && strcmp(top->path, runuser) != 0) EUID_ROOT(); int fd = whitelist_mkpath(top->path, relpath, 0755); if (fd == -1) { if (arg_debug || arg_debug_whitelists) printf("Debug %d: cannot create symbolic link %s\n", __LINE__, link); EUID_USER(); return; } // get file name of symlink const char *file = gnu_basename(relpath); // create the link if (symlinkat(target, fd, file) == -1) { if (arg_debug || arg_debug_whitelists) { perror("symlink"); printf("Debug %d: cannot create symbolic link %s\n", __LINE__, link); } } else if (arg_debug || arg_debug_whitelists) printf("Created symbolic link %s -> %s\n", link, target); close(fd); EUID_USER(); } static void globbing(const char *pattern) { EUID_ASSERT(); assert(pattern); // globbing glob_t globbuf; int globerr = glob(pattern, GLOB_NOCHECK | GLOB_NOSORT | GLOB_PERIOD, NULL, &globbuf); if (globerr) { fprintf(stderr, "Error: failed to glob private-bin pattern %s\n", pattern); exit(1); } size_t i; for (i = 0; i < globbuf.gl_pathc; i++) { assert(globbuf.gl_pathv[i]); // testing for GLOB_NOCHECK - no pattern matched returns the original pattern if (strcmp(globbuf.gl_pathv[i], pattern) == 0) continue; // foo/* expands to foo/. and foo/.. const char *base = gnu_basename(globbuf.gl_pathv[i]); if (strcmp(base, ".") == 0 || strcmp(base, "..") == 0) continue; // build the new profile command char *newcmd; if (asprintf(&newcmd, "whitelist %s", globbuf.gl_pathv[i]) == -1) errExit("asprintf"); // add the new profile command at the end of the list if (arg_debug || arg_debug_whitelists) printf("Adding new profile command: %s\n", newcmd); profile_add(newcmd); } globfree(&globbuf); } // mount tmpfs on all top level directories static void tmpfs_topdirs(const TopDir * const topdirs) { int tmpfs_home = 0; int tmpfs_runuser = 0; int i; for (i = 0; i < TOP_MAX && topdirs[i].path; i++) { // do nested top level directories last // this way '--whitelist=nested_top_level_dir' // yields the full, unmodified directory // instead of the tmpfs if (strcmp(topdirs[i].path, cfg.homedir) == 0) { tmpfs_home = 1; continue; } if (strcmp(topdirs[i].path, runuser) == 0) { tmpfs_runuser = 1; continue; } // special case /run // open /run/firejail, so it can be restored right after mounting the tmpfs int fd = -1; if (strcmp(topdirs[i].path, "/run") == 0) { fd = open(RUN_FIREJAIL_DIR, O_PATH|O_CLOEXEC); if (fd == -1) errExit("open"); } // mount tmpfs fs_tmpfs(topdirs[i].path, 0); selinux_relabel_path(topdirs[i].path, topdirs[i].path); // init tmpfs if (strcmp(topdirs[i].path, "/run") == 0) { // restore /run/firejail directory EUID_ROOT(); mkdir_attr(RUN_FIREJAIL_DIR, 0755, 0, 0); if (bind_mount_fd_to_path(fd, RUN_FIREJAIL_DIR)) errExit("mount bind"); EUID_USER(); close(fd); fs_logger2("whitelist", RUN_FIREJAIL_DIR); // restore /run/user/$UID directory whitelist_file(&topdirs[i], runuser); } else if (strcmp(topdirs[i].path, "/tmp") == 0) { // fix pam-tmpdir (#2685) const char *env = env_get("TMP"); if (env) { // we allow TMP env set as /tmp/user/$UID and /tmp/$UID - see #4151 char *pamtmpdir1; if (asprintf(&pamtmpdir1, "/tmp/user/%u", getuid()) == -1) errExit("asprintf"); char *pamtmpdir2; if (asprintf(&pamtmpdir2, "/tmp/%u", getuid()) == -1) errExit("asprintf"); if (strcmp(env, pamtmpdir1) == 0) { // create empty user-owned /tmp/user/$UID directory EUID_ROOT(); mkdir_attr("/tmp/user", 0755, 0, 0); selinux_relabel_path("/tmp/user", "/tmp/user"); fs_logger("mkdir /tmp/user"); mkdir_attr(pamtmpdir1, 0700, getuid(), 0); selinux_relabel_path(pamtmpdir1, pamtmpdir1); fs_logger2("mkdir", pamtmpdir1); EUID_USER(); } else if (strcmp(env, pamtmpdir2) == 0) { // create empty user-owned /tmp/$UID directory EUID_ROOT(); mkdir_attr(pamtmpdir2, 0700, getuid(), 0); selinux_relabel_path(pamtmpdir2, pamtmpdir2); fs_logger2("mkdir", pamtmpdir2); EUID_USER(); } free(pamtmpdir1); free(pamtmpdir2); } } // restore user home directory if it is masked by the tmpfs // creates path owned by root // does nothing if user home directory doesn't exist whitelist_file(&topdirs[i], cfg.homedir); } // user home directory if (tmpfs_home) fs_private(); // checks owner if outside /home // /run/user/$UID directory if (tmpfs_runuser) { fs_tmpfs(runuser, 0); selinux_relabel_path(runuser, runuser); } } static int reject_topdir(const char *dir) { if (!whitelist_reject_topdirs) return 0; size_t i; for (i = 0; whitelist_reject_topdirs[i]; i++) { if (strcmp(dir, whitelist_reject_topdirs[i]) == 0) return 1; } return 0; } // keep track of whitelist top level directories by adding them to an array // open each directory static TopDir *add_topdir(const char *dir, TopDir *topdirs, const char *path) { EUID_ASSERT(); assert(dir && path); // /proc and /sys are not allowed if (strcmp(dir, "/") == 0 || strcmp(dir, "/proc") == 0 || strcmp(dir, "/sys") == 0) whitelist_error(path); // whitelisting home directory is disabled if --private option is present if (arg_private && strcmp(dir, cfg.homedir) == 0) { if (arg_debug || arg_debug_whitelists) printf("Debug %d: skip %s - a private home dir is configured!\n", __LINE__, path); return NULL; } // do nothing if directory doesn't exist struct stat s; if (lstat(dir, &s) != 0) { if (arg_debug || arg_debug_whitelists) printf("Cannot access whitelist top level directory %s: %s\n", dir, strerror(errno)); return NULL; } // do nothing if directory is a link if (!S_ISDIR(s.st_mode)) { if (S_ISLNK(s.st_mode)) { fwarning("skipping whitelist %s because %s is a symbolic link\n", path, dir); return NULL; } whitelist_error(path); } // do nothing if directory is disabled by administrator if (reject_topdir(dir)) { fmessage("Whitelist top level directory %s is disabled in Firejail configuration file\n", dir); return NULL; } // add directory to array if (arg_debug || arg_debug_whitelists) printf("Adding whitelist top level directory %s\n", dir); static int cnt = 0; if (cnt >= TOP_MAX) { fprintf(stderr, "Error: too many whitelist top level directories\n"); exit(1); } TopDir *rv = topdirs + cnt; cnt++; rv->path = strdup(dir); if (!rv->path) errExit("strdup"); // open the directory, don't follow symbolic links rv->fd = safer_openat(-1, dir, O_PATH|O_NOFOLLOW|O_DIRECTORY|O_CLOEXEC); if (rv->fd == -1) { fprintf(stderr, "Error: cannot open %s\n", dir); exit(1); } return rv; } static TopDir *have_topdir(const char *dir, TopDir *topdirs) { assert(dir); int i; for (i = 0; i < TOP_MAX; i++) { TopDir *rv = topdirs + i; if (!rv->path) break; if (strcmp(dir, rv->path) == 0) return rv; } return NULL; } static char *extract_topdir(const char *path) { assert(path); char *dup = strdup(path); if (!dup) errExit("strdup"); // user home directory can be anywhere; disconnect user home // whitelisting from top level directory whitelisting // by treating user home as separate whitelist top level directory if (strncmp(dup, cfg.homedir, homedir_len) == 0 && dup[homedir_len] == '/') dup[homedir_len] = '\0'; // /run/user/$UID is treated as top level directory else if (strncmp(dup, runuser, runuser_len) == 0 && dup[runuser_len] == '/') dup[runuser_len] = '\0'; // whitelisting in /sys is not allowed, but /sys/module is an exception // and is treated as top level directory here else if (strncmp(dup, "/sys/module", 11) == 0 && dup[11] == '/') dup[11] = '\0'; // treat /usr subdirectories as top level directories else if (strncmp(dup, "/usr/", 5) == 0) { char *p = strchr(dup+5, '/'); if (!p) whitelist_error(path); *p = '\0'; } // all other top level directories else { assert(dup[0] == '/'); char *p = strchr(dup+1, '/'); if (!p) whitelist_error(path); *p = '\0'; } return dup; } void fs_whitelist(void) { EUID_ASSERT(); ProfileEntry *entry = cfg.profile; if (!entry) return; if (asprintf(&runuser, "/run/user/%u", getuid()) == -1) errExit("asprintf"); runuser_len = strlen(runuser); homedir_len = strlen(cfg.homedir); size_t nowhitelist_c = 0; size_t nowhitelist_m = 32; char **nowhitelist = calloc(nowhitelist_m, sizeof(*nowhitelist)); if (nowhitelist == NULL) errExit("calloc"); TopDir *topdirs = calloc(TOP_MAX, sizeof(*topdirs)); if (topdirs == NULL) errExit("calloc"); // verify whitelist files, extract symbolic links, etc. while (entry) { int nowhitelist_flag = 0; // handle only whitelist and nowhitelist commands if (strncmp(entry->data, "whitelist ", 10) == 0) nowhitelist_flag = 0; else if (strncmp(entry->data, "nowhitelist ", 12) == 0) nowhitelist_flag = 1; else { entry = entry->next; continue; } if (arg_debug || arg_debug_whitelists) printf("Debug %d: %s\n", __LINE__, entry->data); const char *dataptr = (nowhitelist_flag)? entry->data + 12: entry->data + 10; // replace ~ into /home/username or resolve macro char *expanded = expand_macros(dataptr); // check if respolving the macro was successful if (is_macro(expanded) && macro_id(expanded) > -1) { if (!nowhitelist_flag && (have_topdir(cfg.homedir, topdirs) || add_topdir(cfg.homedir, topdirs, expanded)) && !arg_quiet) { fprintf(stderr, "***\n"); fprintf(stderr, "*** Warning: cannot whitelist %s directory\n", expanded); fprintf(stderr, "*** Any file saved in this directory will be lost when the sandbox is closed.\n"); fprintf(stderr, "***\n"); } entry = entry->next; free(expanded); continue; } if (arg_debug || arg_debug_whitelists) printf("Debug %d: expanded: %s\n", __LINE__, expanded); // path should be absolute at this point if (expanded[0] != '/') whitelist_error(expanded); // sane pathname char *new_name = clean_pathname(expanded); free(expanded); if (arg_debug || arg_debug_whitelists) printf("Debug %d: new_name: %s\n", __LINE__, new_name); if (strstr(new_name, "..")) whitelist_error(new_name); TopDir *current_top = NULL; if (!nowhitelist_flag) { // extract whitelist top level directory char *dir = extract_topdir(new_name); if (arg_debug || arg_debug_whitelists) printf("Debug %d: dir: %s\n", __LINE__, dir); // check if this top level directory has been processed already current_top = have_topdir(dir, topdirs); if (!current_top) { // got new top level directory current_top = add_topdir(dir, topdirs, new_name); if (!current_top) { // skip this command, top level directory not valid entry = entry->next; free(new_name); free(dir); continue; } } free(dir); } // /run/firejail directory is internal and not allowed if (strncmp(new_name, RUN_FIREJAIL_DIR, strlen(RUN_FIREJAIL_DIR)) == 0) { entry = entry->next; free(new_name); continue; } // extract resolved path of the file // realpath function will fail with ENOENT if the file is not found or with EACCES if user has no permission // special processing for /dev/fd, /dev/stdin, /dev/stdout and /dev/stderr char *fname = NULL; if (strcmp(new_name, "/dev/fd") == 0) fname = strdup("/proc/self/fd"); else if (strcmp(new_name, "/dev/stdin") == 0) fname = strdup("/proc/self/fd/0"); else if (strcmp(new_name, "/dev/stdout") == 0) fname = strdup("/proc/self/fd/1"); else if (strcmp(new_name, "/dev/stderr") == 0) fname = strdup("/proc/self/fd/2"); else fname = realpath(new_name, NULL); if (!fname) { if (arg_debug || arg_debug_whitelists) { printf("Removed path: %s\n", entry->data); printf("\tnew_name: %s\n", new_name); printf("\trealpath: (null)\n"); printf("\t%s\n", strerror(errno)); } if (!nowhitelist_flag) { // if this is not a real path, let's try globbing // push the new paths at the end of profile entry list // the new profile entries will be processed in this loop // currently there is no globbing support for nowhitelist globbing(new_name); } entry = entry->next; free(new_name); continue; } // /run/firejail directory is internal and not allowed if (strncmp(fname, RUN_FIREJAIL_DIR, strlen(RUN_FIREJAIL_DIR)) == 0) { entry = entry->next; free(new_name); free(fname); continue; } if (nowhitelist_flag) { // store the path in nowhitelist array if (arg_debug || arg_debug_whitelists) printf("Storing nowhitelist %s\n", fname); if (nowhitelist_c >= nowhitelist_m) { nowhitelist_m *= 2; nowhitelist = realloc(nowhitelist, sizeof(*nowhitelist) * nowhitelist_m); if (nowhitelist == NULL) errExit("failed increasing memory for nowhitelist entries"); } nowhitelist[nowhitelist_c++] = fname; entry = entry->next; free(new_name); continue; } else { // check if the path is in nowhitelist array size_t i; int found = 0; for (i = 0; i < nowhitelist_c; i++) { if (nowhitelist[i] == NULL) break; if (strcmp(nowhitelist[i], fname) == 0) { found = 1; break; } } if (found) { if (arg_debug || arg_debug_whitelists) printf("Skip nowhitelisted path %s\n", fname); entry = entry->next; free(new_name); free(fname); continue; } } // attach whitelist parameters to profile entry entry->wparam = calloc(1, sizeof(struct wparam_t)); if (!entry->wparam) errExit("calloc"); assert(current_top); entry->wparam->top = current_top; entry->wparam->file = fname; // mark link if (is_link(new_name)) entry->wparam->link = new_name; else free(new_name); entry = entry->next; } // mount tmpfs on all top level directories tmpfs_topdirs(topdirs); // go through profile rules again, and interpret whitelist commands entry = cfg.profile; while (entry) { if (entry->wparam) { char *file = entry->wparam->file; char *link = entry->wparam->link; const TopDir * const current_top = entry->wparam->top; // top level directories of link and file can differ // will whitelist the file only if it is in same top level directory whitelist_file(current_top, file); // create the link if any if (link) { whitelist_symlink(current_top, link, file); free(link); } free(file); free(entry->wparam); entry->wparam = NULL; } entry = entry->next; } // release resources size_t i; for (i = 0; i < nowhitelist_c; i++) free(nowhitelist[i]); free(nowhitelist); for (i = 0; i < TOP_MAX && topdirs[i].path; i++) { free(topdirs[i].path); close(topdirs[i].fd); } free(topdirs); free(runuser); }