From 289d648004c78b19cd953b36db69df6958dfb0aa Mon Sep 17 00:00:00 2001 From: smitsohu Date: Mon, 3 May 2021 00:52:08 +0200 Subject: enhance clean_pathname function --- src/firejail/util.c | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/firejail/util.c b/src/firejail/util.c index 2ad85acd6..8966521cb 100644 --- a/src/firejail/util.c +++ b/src/firejail/util.c @@ -544,11 +544,13 @@ char *split_comma(char *str) { } -// remove consecutive and trailing slashes -// and return allocated memory -// e.g. /home//user/ -> /home/user +// simplify absolute path by removing +// 1) consecutive and trailing slashes, and +// 2) segments with a single dot +// for example /foo//./bar/ -> /foo/bar char *clean_pathname(const char *path) { - assert(path); + assert(path && path[0] == '/'); + size_t len = strlen(path); char *rv = malloc(len + 1); if (!rv) @@ -557,15 +559,23 @@ char *clean_pathname(const char *path) { size_t i = 0; size_t j = 0; while (path[i]) { - while (path[i] == '/' && path[i+1] == '/') - i++; + if (path[i] == '/') { + while (path[i+1] == '/' || + (path[i+1] == '.' && path[i+2] == '/')) + i++; + } + rv[j++] = path[i++]; } rv[j] = '\0'; + // remove a trailing dot + if (j > 1 && rv[j - 1] == '.' && rv[j - 2] == '/') + rv[--j] = '\0'; + // remove a trailing slash if (j > 1 && rv[j - 1] == '/') - rv[j - 1] = '\0'; + rv[--j] = '\0'; return rv; } -- cgit v1.2.3-70-g09d2 From 923d7ada73f9600cda12a4ceb59b90928e4ce0d6 Mon Sep 17 00:00:00 2001 From: smitsohu Date: Mon, 3 May 2021 01:09:05 +0200 Subject: introduce safer_openat function --- src/firejail/chroot.c | 8 ++++---- src/firejail/dbus.c | 2 +- src/firejail/firejail.h | 2 +- src/firejail/fs.c | 10 +++++----- src/firejail/fs_home.c | 6 +++--- src/firejail/fs_whitelist.c | 12 ++++++------ src/firejail/pulseaudio.c | 2 +- src/firejail/restrict_users.c | 2 +- src/firejail/util.c | 45 +++++++++++++++++++++++-------------------- src/firejail/x11.c | 10 +++++----- 10 files changed, 51 insertions(+), 48 deletions(-) (limited to 'src') diff --git a/src/firejail/chroot.c b/src/firejail/chroot.c index d7e96cf4c..757ffb1f7 100644 --- a/src/firejail/chroot.c +++ b/src/firejail/chroot.c @@ -131,9 +131,9 @@ void fs_chroot(const char *rootdir) { assert(rootdir); // fails if there is any symlink or if rootdir is not a directory - int parentfd = safe_fd(rootdir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); + int parentfd = safer_openat(-1, rootdir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (parentfd == -1) - errExit("safe_fd"); + errExit("safer_openat"); // rootdir has to be owned by root and is not allowed to be generally writable, // this also excludes /tmp and friends struct stat s; @@ -215,12 +215,12 @@ void fs_chroot(const char *rootdir) { if (arg_debug) printf("Mounting %s on chroot %s\n", orig_pulse, orig_pulse); - int src = safe_fd(orig_pulse, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); + int src = safer_openat(-1, orig_pulse, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (src == -1) { fprintf(stderr, "Error: cannot open %s\n", orig_pulse); exit(1); } - int dst = safe_fd(pulse, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); + int dst = safer_openat(-1, pulse, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (dst == -1) { fprintf(stderr, "Error: cannot open %s\n", pulse); exit(1); diff --git a/src/firejail/dbus.c b/src/firejail/dbus.c index 658b84537..b8aa2c974 100644 --- a/src/firejail/dbus.c +++ b/src/firejail/dbus.c @@ -416,7 +416,7 @@ void dbus_proxy_stop(void) { } static void socket_overlay(char *socket_path, char *proxy_path) { - int fd = safe_fd(proxy_path, O_PATH | O_NOFOLLOW | O_CLOEXEC); + int fd = safer_openat(-1, proxy_path, O_PATH | O_NOFOLLOW | O_CLOEXEC); if (fd == -1) errExit("opening DBus proxy socket"); struct stat s; diff --git a/src/firejail/firejail.h b/src/firejail/firejail.h index ca4c988fa..6691b9570 100644 --- a/src/firejail/firejail.h +++ b/src/firejail/firejail.h @@ -528,7 +528,7 @@ void mkdir_attr(const char *fname, mode_t mode, uid_t uid, gid_t gid); unsigned extract_timeout(const char *str); void disable_file_or_dir(const char *fname); void disable_file_path(const char *path, const char *file); -int safe_fd(const char *path, int flags); +int safer_openat(int dirfd, const char *path, int flags); int has_handler(pid_t pid, int signal); void enter_network_namespace(pid_t pid); int read_pid(const char *name, pid_t *pid); diff --git a/src/firejail/fs.c b/src/firejail/fs.c index fc67a15f3..09de11de9 100644 --- a/src/firejail/fs.c +++ b/src/firejail/fs.c @@ -453,7 +453,7 @@ void fs_tmpfs(const char *dir, unsigned check_owner) { if (arg_debug) printf("Mounting tmpfs on %s, check owner: %s\n", dir, (check_owner)? "yes": "no"); // get a file descriptor for dir, fails if there is any symlink - int fd = safe_fd(dir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); + int fd = safer_openat(-1, dir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) errExit("while opening directory"); struct stat s; @@ -493,7 +493,7 @@ static void fs_remount_simple(const char *path, OPERATION op) { assert(path); // open path without following symbolic links - int fd1 = safe_fd(path, O_PATH|O_NOFOLLOW|O_CLOEXEC); + int fd1 = safer_openat(-1, path, O_PATH|O_NOFOLLOW|O_CLOEXEC); if (fd1 == -1) goto out; struct stat s1; @@ -559,7 +559,7 @@ static void fs_remount_simple(const char *path, OPERATION op) { // mount --bind -o remount,ro path // need to open path again without following symbolic links - int fd2 = safe_fd(path, O_PATH|O_NOFOLLOW|O_CLOEXEC); + int fd2 = safer_openat(-1, path, O_PATH|O_NOFOLLOW|O_CLOEXEC); if (fd2 == -1) errExit("open"); struct stat s2; @@ -992,9 +992,9 @@ void fs_overlayfs(void) { char *firejail; if (asprintf(&firejail, "%s/.firejail", cfg.homedir) == -1) errExit("asprintf"); - int fd = safe_fd(firejail, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); + int fd = safer_openat(-1, firejail, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) - errExit("safe_fd"); + errExit("safer_openat"); free(firejail); // create basedir if it doesn't exist // the new directory will be owned by root diff --git a/src/firejail/fs_home.c b/src/firejail/fs_home.c index 46f32d7ad..e3b044a3b 100644 --- a/src/firejail/fs_home.c +++ b/src/firejail/fs_home.c @@ -262,10 +262,10 @@ void fs_private_homedir(void) { if (arg_debug) printf("Mount-bind %s on top of %s\n", private_homedir, homedir); // get file descriptors for homedir and private_homedir, fails if there is any symlink - int src = safe_fd(private_homedir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); + int src = safer_openat(-1, private_homedir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (src == -1) errExit("opening private directory"); - int dst = safe_fd(homedir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); + int dst = safer_openat(-1, homedir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (dst == -1) errExit("opening home directory"); // both mount source and target should be owned by the user @@ -576,7 +576,7 @@ void fs_private_home_list(void) { if (arg_debug) printf("Mount-bind %s on top of %s\n", RUN_HOME_DIR, homedir); - int fd = safe_fd(homedir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); + int fd = safer_openat(-1, homedir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) errExit("opening home directory"); // home directory should be owned by the user diff --git a/src/firejail/fs_whitelist.c b/src/firejail/fs_whitelist.c index 698d47b69..23310d92d 100644 --- a/src/firejail/fs_whitelist.c +++ b/src/firejail/fs_whitelist.c @@ -28,7 +28,7 @@ #include #ifndef O_PATH -# define O_PATH 010000000 +#define O_PATH 010000000 #endif // mountinfo functionality test; @@ -220,7 +220,7 @@ static void whitelist_path(ProfileEntry *entry) { // confirm again the mount source exists and there is no symlink struct stat wfilestat; EUID_USER(); - int fd = safe_fd(wfile, O_PATH|O_NOFOLLOW|O_CLOEXEC); + int fd = safer_openat(-1, wfile, O_PATH|O_NOFOLLOW|O_CLOEXEC); EUID_ROOT(); if (fd == -1) { if (arg_debug || arg_debug_whitelists) @@ -317,9 +317,9 @@ static void whitelist_path(ProfileEntry *entry) { if (mptr->dir == strrchr(mptr->dir, '/')) errLogExit("invalid whitelist mount"); // confirm the right file was mounted by comparing device and inode numbers - int fd4 = safe_fd(path, O_PATH|O_NOFOLLOW|O_CLOEXEC); + int fd4 = safer_openat(-1, path, O_PATH|O_NOFOLLOW|O_CLOEXEC); if (fd4 == -1) - errExit("safe_fd"); + errExit("safer_openat"); struct stat s; if (fstat(fd4, &s) == -1) errExit("fstat"); @@ -1059,9 +1059,9 @@ void fs_whitelist(void) { if (stat(cfg.homedir, &s) == 0) { // keep a copy of real home dir in RUN_WHITELIST_HOME_USER_DIR mkdir_attr(RUN_WHITELIST_HOME_USER_DIR, 0755, getuid(), getgid()); - int fd = safe_fd(cfg.homedir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); + int fd = safer_openat(-1, cfg.homedir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) - errExit("safe_fd"); + errExit("safer_openat"); char *proc; if (asprintf(&proc, "/proc/self/fd/%d", fd) == -1) errExit("asprintf"); diff --git a/src/firejail/pulseaudio.c b/src/firejail/pulseaudio.c index 4b9203c36..a50134893 100644 --- a/src/firejail/pulseaudio.c +++ b/src/firejail/pulseaudio.c @@ -131,7 +131,7 @@ void pulseaudio_init(void) { // if ~/.config/pulse exists and there are no symbolic links, mount the new directory // else set environment variable - int fd = safe_fd(homeusercfg, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); + int fd = safer_openat(-1, homeusercfg, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) { pulseaudio_fallback(pulsecfg); goto out; diff --git a/src/firejail/restrict_users.c b/src/firejail/restrict_users.c index a0ca4c02c..8368d84a1 100644 --- a/src/firejail/restrict_users.c +++ b/src/firejail/restrict_users.c @@ -73,7 +73,7 @@ static void sanitize_home(void) { if (arg_debug) printf("Cleaning /home directory\n"); // open user home directory in order to keep it around - int fd = safe_fd(cfg.homedir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); + int fd = safer_openat(-1, cfg.homedir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) goto errout; if (fstat(fd, &s) == -1) { // FUSE diff --git a/src/firejail/util.c b/src/firejail/util.c index 8966521cb..d16354d9d 100644 --- a/src/firejail/util.c +++ b/src/firejail/util.c @@ -915,9 +915,9 @@ int remove_overlay_directory(void) { errExit("fork"); if (child == 0) { // open ~/.firejail, fails if there is any symlink - int fd = safe_fd(path, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); + int fd = safer_openat(-1, path, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) - errExit("safe_fd"); + errExit("safer_openat"); // chdir to ~/.firejail if (fchdir(fd) == -1) errExit("fchdir"); @@ -1136,13 +1136,13 @@ void disable_file_path(const char *path, const char *file) { } // open an existing file without following any symbolic link -int safe_fd(const char *path, int flags) { - flags |= O_NOFOLLOW; +// relative paths are interpreted relative to dirfd +// ignore dirfd if path is absolute +// https://web.archive.org/web/20180419120236/https://blogs.gnome.org/jamesh/2018/04/19/secure-mounts +int safer_openat(int dirfd, const char *path, int flags) { assert(path); - if (*path != '/' || strstr(path, "..")) { - fprintf(stderr, "Error: invalid path %s\n", path); - exit(1); - } + flags |= O_NOFOLLOW; + int fd = -1; #ifdef __NR_openat2 // kernel 5.6 or better @@ -1150,7 +1150,7 @@ int safe_fd(const char *path, int flags) { memset(&oh, 0, sizeof(oh)); oh.flags = flags; oh.resolve = RESOLVE_NO_SYMLINKS; - fd = syscall(__NR_openat2, -1, path, &oh, sizeof(struct open_how)); + fd = syscall(__NR_openat2, dirfd, path, &oh, sizeof(struct open_how)); if (fd != -1 || errno != ENOSYS) return fd; #endif @@ -1161,18 +1161,23 @@ int safe_fd(const char *path, int flags) { if (!dup) errExit("strdup"); char *tok = strtok(dup, "/"); - if (!tok) { // root directory + if (!tok) { // nothing to do, path is either empty string or the root directory free(dup); - return open("/", flags); + return openat(dirfd, path, flags); } char *last_tok = EMPTY_STRING; - int parentfd = open("/", O_PATH|O_CLOEXEC); + + int parentfd; + if (path[0] == '/') + parentfd = open("/", O_PATH|O_CLOEXEC); + else + parentfd = fcntl(dirfd, F_DUPFD_CLOEXEC, 0); if (parentfd == -1) - errExit("open"); + errExit("open/fcntl"); - while(1) { + while (1) { // open path component, assuming it is a directory; this fails with ENOTDIR if it is a symbolic link - // if token is a single dot, the previous directory is reopened + // if token is a single dot, the directory referred to by parentfd is reopened fd = openat(parentfd, tok, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) { // if the following token is NULL, the current token is the final path component @@ -1303,13 +1308,11 @@ pid_t require_pid(const char *name) { // return 1 if there is a link somewhere in path of directory static int has_link(const char *dir) { assert(dir); - int fd = safe_fd(dir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); - if (fd == -1) { - if ((errno == ELOOP || errno == ENOTDIR) && is_dir(dir)) - return 1; - } - else + int fd = safer_openat(-1, dir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); + if (fd != -1) close(fd); + else if (errno == ELOOP || (errno == ENOTDIR && is_dir(dir))) + return 1; return 0; } diff --git a/src/firejail/x11.c b/src/firejail/x11.c index 1dabf272e..da0acc69c 100644 --- a/src/firejail/x11.c +++ b/src/firejail/x11.c @@ -1239,9 +1239,9 @@ void x11_xorg(void) { } } // get a file descriptor for ~/.Xauthority - int dst = safe_fd(dest, O_PATH|O_NOFOLLOW|O_CLOEXEC); + int dst = safer_openat(-1, dest, O_PATH|O_NOFOLLOW|O_CLOEXEC); if (dst == -1) - errExit("safe_fd"); + errExit("safer_openat"); // check if the actual mount destination is a user owned regular file if (fstat(dst, &s) == -1) errExit("fstat"); @@ -1263,9 +1263,9 @@ void x11_xorg(void) { fs_remount(RUN_XAUTHORITY_SEC_DIR, MOUNT_NOEXEC, 0); // get a file descriptor for the new Xauthority file - int src = safe_fd(tmpfname, O_PATH|O_NOFOLLOW|O_CLOEXEC); + int src = safer_openat(-1, tmpfname, O_PATH|O_NOFOLLOW|O_CLOEXEC); if (src == -1) - errExit("safe_fd"); + errExit("safer_openat"); if (fstat(src, &s) == -1) errExit("fstat"); if (!S_ISREG(s.st_mode)) { @@ -1373,7 +1373,7 @@ void fs_x11(void) { char *wx11file; if (asprintf(&wx11file, "%s/X%d", RUN_WHITELIST_X11_DIR, display) == -1) errExit("asprintf"); - fd = safe_fd(wx11file, O_PATH|O_NOFOLLOW|O_CLOEXEC); + fd = safer_openat(-1, wx11file, O_PATH|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) errExit("opening X11 socket"); // confirm once more we are mounting a socket -- cgit v1.2.3-70-g09d2 From 5445d87af6e9fc5fb4508e1c7558f349c012e2b3 Mon Sep 17 00:00:00 2001 From: smitsohu Date: Mon, 3 May 2021 01:23:25 +0200 Subject: add support for arbitrary whitelist directories --- etc/firejail.config | 4 + src/firejail/checkcfg.c | 26 + src/firejail/firejail.h | 27 +- src/firejail/fs_whitelist.c | 1291 +++++++++++++------------------------------ src/include/rundefs.h | 12 - 5 files changed, 434 insertions(+), 926 deletions(-) (limited to 'src') diff --git a/etc/firejail.config b/etc/firejail.config index 731e744dd..9dd33b5ed 100644 --- a/etc/firejail.config +++ b/etc/firejail.config @@ -116,6 +116,10 @@ # Enable or disable whitelisting support, default enabled. # whitelist yes +# Disable whitelist top level directories, in addition to those +# that are disabled out of the box. None by default; this is an example. +# whitelist-disable-topdir /etc,/usr/etc + # Enable or disable X11 sandboxing support, default enabled. # x11 yes diff --git a/src/firejail/checkcfg.c b/src/firejail/checkcfg.c index e1613b325..8a6e47e17 100644 --- a/src/firejail/checkcfg.c +++ b/src/firejail/checkcfg.c @@ -35,6 +35,7 @@ char *xvfb_extra_params = ""; char *netfilter_default = NULL; unsigned long join_timeout = 5000000; // microseconds char *config_seccomp_error_action_str = "EPERM"; +char **whitelist_reject_topdirs = NULL; int checkcfg(int val) { assert(val < CFG_MAX); @@ -238,6 +239,31 @@ int checkcfg(int val) { errExit("strdup"); } + else if (strncmp(ptr, "whitelist-disable-topdir ", 25) == 0) { + char *str = strdup(ptr + 25); + if (!str) + errExit("strdup"); + + size_t cnt = 0; + size_t sz = 4; + whitelist_reject_topdirs = malloc(sz * sizeof(char *)); + if (!whitelist_reject_topdirs) + errExit("malloc"); + + char *tok = strtok(str, ","); + while (tok) { + whitelist_reject_topdirs[cnt++] = tok; + if (cnt >= sz) { + sz *= 2; + whitelist_reject_topdirs = realloc(whitelist_reject_topdirs, sz * sizeof(char *)); + if (!whitelist_reject_topdirs) + errExit("realloc"); + } + tok = strtok(NULL, ","); + } + whitelist_reject_topdirs[cnt] = NULL; + } + else goto errout; diff --git a/src/firejail/firejail.h b/src/firejail/firejail.h index 6691b9570..20de229df 100644 --- a/src/firejail/firejail.h +++ b/src/firejail/firejail.h @@ -122,26 +122,22 @@ typedef struct interface_t { uint8_t configured; } Interface; +typedef struct topdir_t { + char *path; + int fd; +} TopDir; + typedef struct profile_entry_t { struct profile_entry_t *next; char *data; // command // whitelist command parameters - char *link; // link name - set if the file is a link - enum { - WLDIR_HOME = 1, // whitelist in home directory - WLDIR_TMP, // whitelist in /tmp directory - WLDIR_MEDIA, // whitelist in /media directory - WLDIR_MNT, // whitelist in /mnt directory - WLDIR_VAR, // whitelist in /var directory - WLDIR_DEV, // whitelist in /dev directory - WLDIR_OPT, // whitelist in /opt directory - WLDIR_SRV, // whitelist in /srv directory - WLDIR_ETC, // whitelist in /etc directory - WLDIR_SHARE, // whitelist in /usr/share directory - WLDIR_MODULE, // whitelist in /sys/module directory - WLDIR_RUN // whitelist in /run/user/$uid directory - } wldir; + struct wparam_t { + char *file; // resolved file path + char *link; // link path + TopDir *top; // top level directory + } *wparam; + } ProfileEntry; typedef struct config_t { @@ -792,6 +788,7 @@ extern char *xvfb_extra_params; extern char *netfilter_default; extern unsigned long join_timeout; extern char *config_seccomp_error_action_str; +extern char **whitelist_reject_topdirs; int checkcfg(int val); void print_compiletime_support(void); diff --git a/src/firejail/fs_whitelist.c b/src/firejail/fs_whitelist.c index 23310d92d..c58b8e786 100644 --- a/src/firejail/fs_whitelist.c +++ b/src/firejail/fs_whitelist.c @@ -16,14 +16,12 @@ * 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 #include #include @@ -31,35 +29,33 @@ #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 -#define EMPTY_STRING ("") -static size_t homedir_len; // cache length of homedir string -static size_t runuser_len; // cache length of runuser string -static char *runuser; +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 int mkpath(const char* path, mode_t mode) { - assert(path && *path); - mode |= 0111; - // create directories with uid/gid as root, or as current user if inside home or run/user/$uid directory - int userprivs = 0; - if ((strncmp(path, cfg.homedir, homedir_len) == 0 && path[homedir_len] == '/') || - (strncmp(path, runuser, runuser_len) == 0 && path[runuser_len] == '/')) { - EUID_USER(); - userprivs = 1; - } +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* path, mode_t mode) { // work on a copy of the path char *dup = strdup(path); if (!dup) errExit("strdup"); - // don't create the last path element + // only create leading directories, don't create the file char *p = strrchr(dup, '/'); assert(p); *p = '\0'; @@ -69,10 +65,10 @@ static int mkpath(const char* path, mode_t mode) { errExit("open"); // traverse the path, return -1 if a symlink is encountered - int done = 0; int fd = -1; + int done = 0; char *tok = strtok(dup, "/"); - assert(tok); // path is no top level directory + assert(tok); while (tok) { // create the directory if necessary if (mkdirat(parentfd, tok, mode) == -1) { @@ -81,9 +77,6 @@ static int mkpath(const char* path, mode_t mode) { perror("mkdir"); close(parentfd); free(dup); - if (userprivs) { - EUID_ROOT(); - } return -1; } } @@ -96,9 +89,6 @@ static int mkpath(const char* path, mode_t mode) { perror("open"); close(parentfd); free(dup); - if (userprivs) { - EUID_ROOT(); - } return -1; } // move on to next path segment @@ -111,195 +101,111 @@ static int mkpath(const char* path, mode_t mode) { fs_logger2("mkpath", path); free(dup); - if (userprivs) { - EUID_ROOT(); - } return fd; } -static void whitelist_path(ProfileEntry *entry) { - assert(entry); - const char *path = entry->data + 10; - const char *fname; - char *wfile = NULL; - - if (entry->wldir == WLDIR_HOME) { - if (strncmp(path, cfg.homedir, homedir_len) != 0 || path[homedir_len] != '/') - // either symlink pointing outside home directory - // or entire home directory, skip the mount - return; - - fname = path + homedir_len + 1; // strlen("/home/user/") - - if (asprintf(&wfile, "%s/%s", RUN_WHITELIST_HOME_USER_DIR, fname) == -1) - errExit("asprintf"); - } - else if (entry->wldir == WLDIR_TMP) { - fname = path + 5; // strlen("/tmp/") - - if (asprintf(&wfile, "%s/%s", RUN_WHITELIST_TMP_DIR, fname) == -1) - errExit("asprintf"); - } - else if (entry->wldir == WLDIR_MEDIA) { - fname = path + 7; // strlen("/media/") - - if (asprintf(&wfile, "%s/%s", RUN_WHITELIST_MEDIA_DIR, fname) == -1) - errExit("asprintf"); - } - else if (entry->wldir == WLDIR_MNT) { - fname = path + 5; // strlen("/mnt/") - - if (asprintf(&wfile, "%s/%s", RUN_WHITELIST_MNT_DIR, fname) == -1) - errExit("asprintf"); - } - else if (entry->wldir == WLDIR_VAR) { - if (strncmp(path, "/var/", 5) != 0) - // symlink pointing outside /var, skip the mount - return; - - fname = path + 5; // strlen("/var/") - - if (asprintf(&wfile, "%s/%s", RUN_WHITELIST_VAR_DIR, fname) == -1) - errExit("asprintf"); - } - else if (entry->wldir == WLDIR_DEV) { - if (strncmp(path, "/dev/", 5) != 0) - // symlink pointing outside /dev, skip the mount - return; - - fname = path + 5; // strlen("/dev/") - - if (asprintf(&wfile, "%s/%s", RUN_WHITELIST_DEV_DIR, fname) == -1) - errExit("asprintf"); - } - else if (entry->wldir == WLDIR_OPT) { - fname = path + 5; // strlen("/opt/") - - if (asprintf(&wfile, "%s/%s", RUN_WHITELIST_OPT_DIR, fname) == -1) - errExit("asprintf"); - } - else if (entry->wldir == WLDIR_SRV) { - fname = path + 5; // strlen("/srv/") - - if (asprintf(&wfile, "%s/%s", RUN_WHITELIST_SRV_DIR, fname) == -1) - errExit("asprintf"); - } - else if (entry->wldir == WLDIR_ETC) { - if (strncmp(path, "/etc/", 5) != 0) - // symlink pointing outside /etc, skip the mount - return; - - fname = path + 5; // strlen("/etc/") - - if (asprintf(&wfile, "%s/%s", RUN_WHITELIST_ETC_DIR, fname) == -1) - errExit("asprintf"); - } - else if (entry->wldir == WLDIR_SHARE) { - fname = path + 11; // strlen("/usr/share/") - - if (asprintf(&wfile, "%s/%s", RUN_WHITELIST_SHARE_DIR, fname) == -1) - errExit("asprintf"); - } - else if (entry->wldir == WLDIR_MODULE) { - fname = path + 12; // strlen("/sys/module/") - - if (asprintf(&wfile, "%s/%s", RUN_WHITELIST_MODULE_DIR, fname) == -1) - errExit("asprintf"); - } - else if (entry->wldir == WLDIR_RUN) { - fname = path + runuser_len + 1; // strlen("/run/user/$uid/") - - if (asprintf(&wfile, "%s/%s", RUN_WHITELIST_RUN_USER_DIR, fname) == -1) - errExit("asprintf"); - } - assert(wfile); +static void whitelist_file(int dirfd, const char *topdir, const char *relpath, const char *path) { + assert(topdir && relpath && path); if (arg_debug || arg_debug_whitelists) - printf("Whitelisting %s\n", path); - - // confirm again the mount source exists and there is no symlink - struct stat wfilestat; - EUID_USER(); - int fd = safer_openat(-1, wfile, O_PATH|O_NOFOLLOW|O_CLOEXEC); - EUID_ROOT(); + printf("Debug %d: dirfd: %d; topdir: %s; relpath: %s; path: %s\n", __LINE__, dirfd, topdir, relpath, path); + + // 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 no follow symbolic links + int fd = safer_openat(dirfd, relpath, O_PATH|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) { if (arg_debug || arg_debug_whitelists) printf("Debug %d: skip whitelisting of %s\n", __LINE__, path); - free(wfile); return; } - if (fstat(fd, &wfilestat) == -1) + struct stat s; + if (fstat(fd, &s) == -1) errExit("fstat"); - close(fd); - if (S_ISLNK(wfilestat.st_mode)) { + if (S_ISLNK(s.st_mode)) { if (arg_debug || arg_debug_whitelists) printf("Debug %d: skip whitelisting of %s\n", __LINE__, path); - free(wfile); + close(fd); return; } - // create path of the mount target if necessary - int fd2 = mkpath(path, 0755); + // create mount target as root, except if inside home or run/user/$UID directory + int userprivs = 0; + if (strcmp(topdir, cfg.homedir) == 0 || strcmp(topdir, runuser) == 0) { + EUID_USER(); + userprivs = 1; + } + + // create path of the mount target + int fd2 = whitelist_mkpath(path, 0755); if (fd2 == -1) { // something went wrong during path creation or a symlink was found; // if there is a symlink somewhere in the path of the mount target, // assume the file is whitelisted already if (arg_debug || arg_debug_whitelists) printf("Debug %d: skip whitelisting of %s\n", __LINE__, path); - free(wfile); + close(fd); + if (userprivs) + EUID_ROOT(); return; } // get file name of the mount target const char *file = gnu_basename(path); - // create the mount target if necessary and open it, a symlink is rejected + // create mount target itself and open it, a symlink is rejected int fd3 = -1; - if (S_ISDIR(wfilestat.st_mode)) { + if (S_ISDIR(s.st_mode)) { // directory foo can exist already: - // firejail --whitelist=/foo/bar --whitelist=/foo + // firejail --whitelist=~/foo/bar --whitelist=~/foo if (mkdirat(fd2, file, 0755) == -1 && errno != EEXIST) { if (arg_debug || arg_debug_whitelists) { perror("mkdir"); printf("Debug %d: skip whitelisting of %s\n", __LINE__, path); } + close(fd); close(fd2); - free(wfile); + if (userprivs) + EUID_ROOT(); return; } fd3 = openat(fd2, file, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); } - else { + else // create an empty file, fails with EEXIST if it is whitelisted already: // firejail --whitelist=/foo --whitelist=/foo/bar fd3 = openat(fd2, file, O_RDONLY|O_CREAT|O_EXCL|O_CLOEXEC, S_IRUSR|S_IWUSR); - } if (fd3 == -1) { - if (arg_debug || arg_debug_whitelists) { - if (errno != EEXIST) { - perror("open"); - printf("Debug %d: skip whitelisting of %s\n", __LINE__, path); - } + if (errno != EEXIST && (arg_debug || arg_debug_whitelists)) { + perror("open"); + printf("Debug %d: skip whitelisting of %s\n", __LINE__, path); } + close(fd); close(fd2); - free(wfile); + if (userprivs) + EUID_ROOT(); return; } + close(fd2); + if (userprivs) + EUID_ROOT(); - fs_logger2("whitelist", path); + if (arg_debug || arg_debug_whitelists) + printf("Whitelisting %s\n", path); // in order to make this mount resilient against symlink attacks, use - // a magic link in /proc/self/fd instead of mounting on path directly - char *proc; - if (asprintf(&proc, "/proc/self/fd/%d", fd3) == -1) + // magic links in /proc/self/fd instead of mounting the paths directly + char *proc_src, *proc_dst; + if (asprintf(&proc_src, "/proc/self/fd/%d", fd) == -1) + errExit("asprintf"); + if (asprintf(&proc_dst, "/proc/self/fd/%d", fd3) == -1) errExit("asprintf"); - if (mount(wfile, proc, NULL, MS_BIND|MS_REC, NULL) < 0) + if (mount(proc_src, proc_dst, NULL, MS_BIND | MS_REC, NULL) < 0) errExit("mount bind"); - free(proc); - close(fd3); - // check the last mount operation MountData *mptr = get_last_mount(); // will do exit(1) if the mount cannot be found #ifdef TEST_MOUNTINFO @@ -316,35 +222,52 @@ static void whitelist_path(ProfileEntry *entry) { // - there should be more than one '/' char in dest string if (mptr->dir == strrchr(mptr->dir, '/')) errLogExit("invalid whitelist mount"); - // confirm the right file was mounted by comparing device and inode numbers - int fd4 = safer_openat(-1, path, O_PATH|O_NOFOLLOW|O_CLOEXEC); - if (fd4 == -1) - errExit("safer_openat"); - struct stat s; - if (fstat(fd4, &s) == -1) - errExit("fstat"); - if (s.st_dev != wfilestat.st_dev || s.st_ino != wfilestat.st_ino) - errLogExit("invalid whitelist mount"); - close(fd4); - - free(wfile); - return; + free(proc_src); + free(proc_dst); + close(fd); + close(fd3); + fs_logger2("whitelist", path); } -static void whitelist_home(int topdir) { - ProfileEntry entry; - memset(&entry, 0, sizeof(entry)); - char *cmd; - if (asprintf(&cmd, "whitelist %s", cfg.homedir) == -1) - errExit("asprintf"); - entry.data = cmd; - entry.wldir = topdir; - // creates path owned by root, except homedir is inside /run/user/$uid - // does nothing if homedir does not exist - whitelist_path(&entry); - free(cmd); -} +static void whitelist_symlink(const char *topdir, const char *link, const char *target) { + assert(topdir && link && target); + if (arg_debug || arg_debug_whitelists) + printf("Debug %d: topdir: %s; link: %s; target: %s\n", __LINE__, topdir, link, target); + + // create files as root, except if inside home or run/user/$UID directory + int userprivs = 0; + if (strcmp(topdir, cfg.homedir) == 0 || strcmp(topdir, runuser) == 0) { + EUID_USER(); + userprivs = 1; + } + + int fd = whitelist_mkpath(link, 0755); + if (fd == -1) { + if (arg_debug || arg_debug_whitelists) + printf("Debug %d: cannot create symbolic link %s\n", __LINE__, link); + if (userprivs) + EUID_ROOT(); + return; + } + + // get file name of symlink + const char *file = gnu_basename(link); + + // 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); + if (userprivs) + EUID_ROOT(); +} static void globbing(const char *pattern) { assert(pattern); @@ -363,6 +286,11 @@ static void globbing(const char *pattern) { // 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; @@ -378,6 +306,176 @@ static void globbing(const char *pattern) { globfree(&globbuf); } +// mount tmpfs on all top level directories +static void tmpfs_topdirs(TopDir *topdirs) { + // process user home directory first + int i; + for (i = 0; i < TOP_MAX && topdirs[i].path; i++) { + if (strcmp(topdirs[i].path, cfg.homedir) == 0) { + fs_private(); + break; + } + } + + for (i = 0; i < TOP_MAX && topdirs[i].path; i++) { + if (strcmp(topdirs[i].path, cfg.homedir) != 0) { + // mount the tmpfs + fs_tmpfs(topdirs[i].path, 0); + selinux_relabel_path(topdirs[i].path, topdirs[i].path); + + // init tmpfs + // fix pam-tmpdir (#2685) + if (strcmp(topdirs[i].path, "/tmp") == 0) { + const char *env = env_get("TMP"); + if (env) { + char *pamtmpdir; + if (asprintf(&pamtmpdir, "/tmp/user/%u", getuid()) == -1) + errExit("asprintf"); + if (strcmp(env, pamtmpdir) == 0) { + // create empty user-owned /tmp/user/$UID directory + mkdir_attr("/tmp/user", 0711, 0, 0); + selinux_relabel_path("/tmp/user", "/tmp/user"); + fs_logger("mkdir /tmp/user"); + mkdir_attr(pamtmpdir, 0700, getuid(), 0); + selinux_relabel_path(pamtmpdir, pamtmpdir); + fs_logger2("mkdir", pamtmpdir); + } + free(pamtmpdir); + } + } + + // bring back user home directory if it is masked by the tmpfs + size_t topdir_len = strlen(topdirs[i].path); + if (strncmp(topdirs[i].path, cfg.homedir, topdir_len) == 0 && cfg.homedir[topdir_len] == '/') { + // get path relative to top level directory + const char *rel = cfg.homedir + topdir_len + 1; + whitelist_file(topdirs[i].fd, topdirs[i].path, rel, cfg.homedir); + } + } + } +} + +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) { + assert(dir && path); + + // /proc, /run and /sys are not allowed + if (strcmp(dir, "/") == 0 || + strcmp(dir, "/proc") == 0 || + strcmp(dir, "/run") == 0 || + strcmp(dir, "/sys") == 0) + whitelist_error(path); + + // 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)) { + fwarning("skipping whitelist %s because\n" + "whitelist top level directory is disabled in Firejail configuration file\n", path); + 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++; + + char *dup = strdup(dir); + if (!dup) + errExit("strdup"); + rv->path = dup; + + // open the directory, don't follow symbolic links + rv->fd = safer_openat(-1, dup, O_PATH|O_NOFOLLOW|O_DIRECTORY|O_CLOEXEC); + if (rv->fd == -1) { + fprintf(stderr, "Error: cannot open %s\n", dup); + 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 is treated as top level directory + if (strncmp(dup, cfg.homedir, homedir_len) == 0 && dup[homedir_len] == '/') + dup[homedir_len] = '\0'; + // whitelisting in /run and /sys is not allowed, + // but /run/user/$UID and /sys/module are exceptions + // and are treated as top level directories here + else if (strncmp(dup, runuser, runuser_len) == 0 && dup[runuser_len] == '/') + dup[runuser_len] = '\0'; + 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) { ProfileEntry *entry = cfg.profile; @@ -389,29 +487,18 @@ void fs_whitelist(void) { runuser_len = strlen(runuser); homedir_len = strlen(cfg.homedir); - char *new_name = NULL; - int home_dir = 0; // /home/user directory flag - int tmp_dir = 0; // /tmp directory flag - int media_dir = 0; // /media directory flag - int mnt_dir = 0; // /mnt directory flag - int var_dir = 0; // /var directory flag - int dev_dir = 0; // /dev directory flag - int opt_dir = 0; // /opt directory flag - int srv_dir = 0; // /srv directory flag - int etc_dir = 0; // /etc directory flag - int share_dir = 0; // /usr/share directory flag - int module_dir = 0; // /sys/module directory flag - int run_dir = 0; // /run/user/$uid directory flag - size_t nowhitelist_c = 0; size_t nowhitelist_m = 32; char **nowhitelist = calloc(nowhitelist_m, sizeof(*nowhitelist)); if (nowhitelist == NULL) - errExit("failed allocating memory for nowhitelist entries"); + errExit("calloc"); + + TopDir *topdirs = calloc(TOP_MAX, sizeof(*topdirs)); + if (topdirs == NULL) + errExit("calloc"); // verify whitelist files, extract symbolic links, etc. EUID_USER(); - struct stat s; while (entry) { int nowhitelist_flag = 0; @@ -424,48 +511,69 @@ void fs_whitelist(void) { entry = entry->next; continue; } - char *dataptr = (nowhitelist_flag)? entry->data + 12: entry->data + 10; - - // replace ~/ or ${HOME} into /home/username or resolve macro - new_name = expand_macros(dataptr); - assert(new_name); - - // mount empty home directory if resolving the macro was not successful - if (is_macro(new_name) && macro_id(new_name) > -1) { - // no warning if home does not exist (e.g. in a chroot) - if (stat(cfg.homedir, &s) == 0 && !nowhitelist_flag && !arg_private) { - home_dir = 1; - if (!arg_quiet) { - fprintf(stderr, "***\n"); - fprintf(stderr, "*** Warning: cannot whitelist %s directory\n", new_name); - fprintf(stderr, "*** Any file saved in this directory will be lost when the sandbox is closed.\n"); - fprintf(stderr, "***\n"); - } + 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->data = EMPTY_STRING; entry = entry->next; - free(new_name); + free(expanded); continue; } - // remove trailing slashes and single dots - if (!nowhitelist_flag) - trim_trailing_slash_or_dot(new_name); + 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) - fprintf(stderr, "Debug %d: new_name #%s#, %s\n", __LINE__, new_name, (nowhitelist_flag)? "nowhitelist": "whitelist"); + printf("Debug %d: new_name: %s\n", __LINE__, new_name); - // valid path referenced to filesystem root - if (*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) - fprintf(stderr, "Debug %d: \n", __LINE__); - goto errexit; + 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); } - // extract the absolute path of the file + // 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; + char *fname = NULL; if (strcmp(new_name, "/dev/fd") == 0) fname = strdup("/proc/self/fd"); else if (strcmp(new_name, "/dev/stdin") == 0) @@ -477,60 +585,26 @@ void fs_whitelist(void) { else fname = realpath(new_name, NULL); - // if this is not a real path, let's try globbing - // mark this entry as EMPTY_STRING and 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 - if (!fname && !nowhitelist_flag) - globbing(new_name); - if (!fname) { - // file not found, blank the entry in the list and continue if (arg_debug || arg_debug_whitelists) { - printf("Removed whitelist/nowhitelist path: %s\n", entry->data); + printf("Removed path: %s\n", entry->data); printf("\texpanded: %s\n", new_name); - printf("\treal path: (null)\n"); - printf("\t");fflush(0); - perror("realpath"); + printf("\trealpath: (null)\n"); + printf("\t%s\n", strerror(errno)); } - // if 1 the file was not found; mount an empty directory if (!nowhitelist_flag) { - if (strncmp(new_name, cfg.homedir, homedir_len) == 0 && new_name[homedir_len] == '/') { - if(!arg_private) - home_dir = 1; - } - else if (strncmp(new_name, "/tmp/", 5) == 0) - tmp_dir = 1; - else if (strncmp(new_name, "/media/", 7) == 0) - media_dir = 1; - else if (strncmp(new_name, "/mnt/", 5) == 0) - mnt_dir = 1; - else if (strncmp(new_name, "/var/", 5) == 0) - var_dir = 1; - else if (strncmp(new_name, "/dev/", 5) == 0) - dev_dir = 1; - else if (strncmp(new_name, "/opt/", 5) == 0) - opt_dir = 1; - else if (strncmp(new_name, "/srv/", 5) == 0) - srv_dir = 1; - else if (strncmp(new_name, "/etc/", 5) == 0) - etc_dir = 1; - else if (strncmp(new_name, "/usr/share/", 11) == 0) - share_dir = 1; - else if (strncmp(new_name, "/sys/module/", 12) == 0) - module_dir = 1; - else if (strncmp(new_name, runuser, runuser_len) == 0 && new_name[runuser_len] == '/') - run_dir = 1; + // 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->data = EMPTY_STRING; entry = entry->next; free(new_name); continue; } - else if (arg_debug_whitelists) - printf("real path %s\n", fname); if (nowhitelist_flag) { // store the path in nowhitelist array @@ -544,175 +618,12 @@ void fs_whitelist(void) { errExit("failed increasing memory for nowhitelist entries"); } nowhitelist[nowhitelist_c++] = fname; - entry->data = EMPTY_STRING; entry = entry->next; free(new_name); continue; } - - // check for supported directories - if (strncmp(new_name, cfg.homedir, homedir_len) == 0 && new_name[homedir_len] == '/') { - // whitelisting home directory is disabled if --private option is present - if (arg_private) { - if (arg_debug || arg_debug_whitelists) - printf("\"%s\" disabled by --private\n", entry->data); - - entry->data = EMPTY_STRING; - entry = entry->next; - free(fname); - free(new_name); - continue; - } - - entry->wldir = WLDIR_HOME; - home_dir = 1; - if (arg_debug || arg_debug_whitelists) - fprintf(stderr, "Debug %d: fname #%s#, cfg.homedir #%s#\n", - __LINE__, fname, cfg.homedir); - - // both path and absolute path are in user home, - // if not check if the symlink destination is owned by the user - if (strncmp(fname, cfg.homedir, homedir_len) != 0 || fname[homedir_len] != '/') { - if (checkcfg(CFG_FOLLOW_SYMLINK_AS_USER)) { - if (stat(fname, &s) == 0 && s.st_uid != getuid()) { - free(fname); - goto errexit; - } - } - } - } - else if (strncmp(new_name, "/tmp/", 5) == 0) { - entry->wldir = WLDIR_TMP; - tmp_dir = 1; - - // both path and absolute path are under /tmp - if (strncmp(fname, "/tmp/", 5) != 0) { - free(fname); - goto errexit; - } - } - else if (strncmp(new_name, "/media/", 7) == 0) { - entry->wldir = WLDIR_MEDIA; - media_dir = 1; - // both path and absolute path are under /media - if (strncmp(fname, "/media/", 7) != 0) { - free(fname); - goto errexit; - } - } - else if (strncmp(new_name, "/mnt/", 5) == 0) { - entry->wldir = WLDIR_MNT; - mnt_dir = 1; - // both path and absolute path are under /mnt - if (strncmp(fname, "/mnt/", 5) != 0) { - free(fname); - goto errexit; - } - } - else if (strncmp(new_name, "/var/", 5) == 0) { - entry->wldir = WLDIR_VAR; - var_dir = 1; - // both path and absolute path are under /var - // exceptions: /var/tmp, /var/run and /var/lock - if (strcmp(new_name, "/var/run")== 0 && strcmp(fname, "/run") == 0); - else if (strcmp(new_name, "/var/lock")== 0 && strcmp(fname, "/run/lock") == 0); - else if (strcmp(new_name, "/var/tmp")== 0 && strcmp(fname, "/tmp") == 0); - else { - // both path and absolute path are under /var - if (strncmp(fname, "/var/", 5) != 0) { - free(fname); - goto errexit; - } - } - } - else if (strncmp(new_name, "/dev/", 5) == 0) { - entry->wldir = WLDIR_DEV; - dev_dir = 1; - // special handling for /dev/shm - // on some platforms (Debian wheezy, Ubuntu 14.04), it is a symlink to /run/shm - if (strcmp(new_name, "/dev/shm") == 0 && strcmp(fname, "/run/shm") == 0); - // special handling for /dev/log, which can be a symlink to /run/systemd/journal/dev-log - else if (strcmp(new_name, "/dev/log") == 0 && strcmp(fname, "/run/systemd/journal/dev-log") == 0); - // special processing for /proc/self/fd files - else if (strcmp(new_name, "/dev/fd") == 0 && strcmp(fname, "/proc/self/fd") == 0); - else if (strcmp(new_name, "/dev/stdin") == 0 && strcmp(fname, "/proc/self/fd/0") == 0); - else if (strcmp(new_name, "/dev/stdout") == 0 && strcmp(fname, "/proc/self/fd/1") == 0); - else if (strcmp(new_name, "/dev/stderr") == 0 && strcmp(fname, "/proc/self/fd/2") == 0); - else { - // both path and absolute path are under /dev - if (strncmp(fname, "/dev/", 5) != 0) { - free(fname); - goto errexit; - } - } - } - else if (strncmp(new_name, "/opt/", 5) == 0) { - entry->wldir = WLDIR_OPT; - opt_dir = 1; - // both path and absolute path are under /dev - if (strncmp(fname, "/opt/", 5) != 0) { - free(fname); - goto errexit; - } - } - else if (strncmp(new_name, "/srv/", 5) == 0) { - entry->wldir = WLDIR_SRV; - srv_dir = 1; - // both path and absolute path are under /srv - if (strncmp(fname, "/srv/", 5) != 0) { - free(fname); - goto errexit; - } - } - else if (strncmp(new_name, "/etc/", 5) == 0) { - entry->wldir = WLDIR_ETC; - etc_dir = 1; - // special handling for some of the symlinks - if (strcmp(new_name, "/etc/localtime") == 0); - else if (strcmp(new_name, "/etc/mtab") == 0); - else if (strcmp(new_name, "/etc/os-release") == 0); - // both path and absolute path are under /etc - else { - if (strncmp(fname, "/etc/", 5) != 0) { - free(fname); - goto errexit; - } - } - } - else if (strncmp(new_name, "/usr/share/", 11) == 0) { - entry->wldir = WLDIR_SHARE; - share_dir = 1; - // both path and absolute path are under /etc - if (strncmp(fname, "/usr/share/", 11) != 0) { - free(fname); - goto errexit; - } - } - else if (strncmp(new_name, "/sys/module/", 12) == 0) { - entry->wldir = WLDIR_MODULE; - module_dir = 1; - // both path and absolute path are under /sys/module - if (strncmp(fname, "/sys/module/", 12) != 0) { - free(fname); - goto errexit; - } - } - else if (strncmp(new_name, runuser, runuser_len) == 0 && new_name[runuser_len] == '/') { - entry->wldir = WLDIR_RUN; - run_dir = 1; - // both path and absolute path are under /run/user/$uid - if (strncmp(fname, runuser, runuser_len) != 0 || fname[runuser_len] != '/') { - free(fname); - goto errexit; - } - } else { - free(fname); - goto errexit; - } - - // check if the path is in nowhitelist array - if (nowhitelist_flag == 0) { + // check if the path is in nowhitelist array size_t i; int found = 0; for (i = 0; i < nowhitelist_c; i++) { @@ -726,494 +637,76 @@ void fs_whitelist(void) { if (found) { if (arg_debug || arg_debug_whitelists) printf("Skip nowhitelisted path %s\n", fname); - entry->data = EMPTY_STRING; entry = entry->next; - free(fname); free(new_name); + free(fname); continue; } } - // mark symbolic links + // 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->link = new_name; - else { + entry->wparam->link = new_name; + else free(new_name); - entry->link = NULL; - } - // change file name in entry->data - if (strcmp(fname, entry->data + 10) != 0) { - char *newdata; - if (asprintf(&newdata, "whitelist %s", fname) == -1) - errExit("asprintf"); - entry->data = newdata; - if (arg_debug || arg_debug_whitelists) - printf("Replaced whitelist path: %s\n", entry->data); - } - free(fname); entry = entry->next; } // release nowhitelist memory - assert(nowhitelist); free(nowhitelist); + // mount tmpfs on all top level directories EUID_ROOT(); - // /tmp mountpoint - if (tmp_dir) { - // check if /tmp directory exists - if (stat("/tmp", &s) == 0) { - // keep a copy of real /tmp directory in RUN_WHITELIST_TMP_DIR - mkdir_attr(RUN_WHITELIST_TMP_DIR, 1777, 0, 0); - if (mount("/tmp", RUN_WHITELIST_TMP_DIR, NULL, MS_BIND|MS_REC, NULL) < 0) - errExit("mount bind"); - - // mount tmpfs on /tmp - if (arg_debug || arg_debug_whitelists) - printf("Mounting tmpfs on /tmp directory\n"); - if (mount("tmpfs", "/tmp", "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=1777,gid=0") < 0) - errExit("mounting tmpfs on /tmp"); - selinux_relabel_path("/tmp", "/tmp"); - fs_logger("tmpfs /tmp"); - - // pam-tmpdir - issue #2685 - const char *env = env_get("TMP"); - if (env) { - char *pamtmpdir; - if (asprintf(&pamtmpdir, "/tmp/user/%u", getuid()) == -1) - errExit("asprintf"); - if (strcmp(env, pamtmpdir) == 0) { - // create empty user-owned /tmp/user/$uid directory - mkdir_attr("/tmp/user", 0711, 0, 0); - selinux_relabel_path("/tmp/user", "/tmp/user"); - fs_logger("mkdir /tmp/user"); - mkdir_attr(pamtmpdir, 0700, getuid(), 0); - selinux_relabel_path(pamtmpdir, pamtmpdir); - fs_logger2("mkdir", pamtmpdir); - } - free(pamtmpdir); - } - - // autowhitelist home directory if it is masked by the tmpfs - if (strncmp(cfg.homedir, "/tmp/", 5) == 0) - whitelist_home(WLDIR_TMP); - } - else - tmp_dir = 0; - } - - // /media mountpoint - if (media_dir) { - // some distros don't have a /media directory - if (stat("/media", &s) == 0) { - // keep a copy of real /media directory in RUN_WHITELIST_MEDIA_DIR - mkdir_attr(RUN_WHITELIST_MEDIA_DIR, 0755, 0, 0); - if (mount("/media", RUN_WHITELIST_MEDIA_DIR, NULL, MS_BIND|MS_REC, NULL) < 0) - errExit("mount bind"); - - // mount tmpfs on /media - if (arg_debug || arg_debug_whitelists) - printf("Mounting tmpfs on /media directory\n"); - if (mount("tmpfs", "/media", "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mounting tmpfs on /media"); - selinux_relabel_path("/media", "/media"); - fs_logger("tmpfs /media"); - - // autowhitelist home directory if it is masked by the tmpfs - if (strncmp(cfg.homedir, "/media/", 7) == 0) - whitelist_home(WLDIR_MEDIA); - } - else - media_dir = 0; - } - - // /mnt mountpoint - if (mnt_dir) { - // check if /mnt directory exists - if (stat("/mnt", &s) == 0) { - // keep a copy of real /mnt directory in RUN_WHITELIST_MNT_DIR - mkdir_attr(RUN_WHITELIST_MNT_DIR, 0755, 0, 0); - if (mount("/mnt", RUN_WHITELIST_MNT_DIR, NULL, MS_BIND|MS_REC, NULL) < 0) - errExit("mount bind"); - - // mount tmpfs on /mnt - if (arg_debug || arg_debug_whitelists) - printf("Mounting tmpfs on /mnt directory\n"); - if (mount("tmpfs", "/mnt", "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mounting tmpfs on /mnt"); - selinux_relabel_path("/mnt", "/mnt"); - fs_logger("tmpfs /mnt"); - - // autowhitelist home directory if it is masked by the tmpfs - if (strncmp(cfg.homedir, "/mnt/", 5) == 0) - whitelist_home(WLDIR_MNT); - } - else - mnt_dir = 0; - } - - // /var mountpoint - if (var_dir) { - // check if /var directory exists - if (stat("/var", &s) == 0) { - // keep a copy of real /var directory in RUN_WHITELIST_VAR_DIR - mkdir_attr(RUN_WHITELIST_VAR_DIR, 0755, 0, 0); - if (mount("/var", RUN_WHITELIST_VAR_DIR, NULL, MS_BIND|MS_REC, NULL) < 0) - errExit("mount bind"); - - // mount tmpfs on /var - if (arg_debug || arg_debug_whitelists) - printf("Mounting tmpfs on /var directory\n"); - if (mount("tmpfs", "/var", "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mounting tmpfs on /var"); - selinux_relabel_path("/var", "/var"); - fs_logger("tmpfs /var"); - - // autowhitelist home directory if it is masked by the tmpfs - if (strncmp(cfg.homedir, "/var/", 5) == 0) - whitelist_home(WLDIR_VAR); - } - else - var_dir = 0; - } - - // /dev mountpoint - if (dev_dir) { - // check if /dev directory exists - if (stat("/dev", &s) == 0) { - // keep a copy of real /dev directory in RUN_WHITELIST_DEV_DIR - mkdir_attr(RUN_WHITELIST_DEV_DIR, 0755, 0, 0); - if (mount("/dev", RUN_WHITELIST_DEV_DIR, NULL, MS_BIND|MS_REC, "mode=755,gid=0") < 0) - errExit("mount bind"); - - // mount tmpfs on /dev - if (arg_debug || arg_debug_whitelists) - printf("Mounting tmpfs on /dev directory\n"); - if (mount("tmpfs", "/dev", "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mounting tmpfs on /dev"); - selinux_relabel_path("/dev", "/dev"); - fs_logger("tmpfs /dev"); - - // autowhitelist home directory if it is masked by the tmpfs - if (strncmp(cfg.homedir, "/dev/", 5) == 0) - whitelist_home(WLDIR_DEV); - } - else - dev_dir = 0; - } - - // /opt mountpoint - if (opt_dir) { - // check if /opt directory exists - if (stat("/opt", &s) == 0) { - // keep a copy of real /opt directory in RUN_WHITELIST_OPT_DIR - mkdir_attr(RUN_WHITELIST_OPT_DIR, 0755, 0, 0); - if (mount("/opt", RUN_WHITELIST_OPT_DIR, NULL, MS_BIND|MS_REC, NULL) < 0) - errExit("mount bind"); - - // mount tmpfs on /opt - if (arg_debug || arg_debug_whitelists) - printf("Mounting tmpfs on /opt directory\n"); - if (mount("tmpfs", "/opt", "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mounting tmpfs on /opt"); - selinux_relabel_path("/opt", "/opt"); - fs_logger("tmpfs /opt"); - - // autowhitelist home directory if it is masked by the tmpfs - if (strncmp(cfg.homedir, "/opt/", 5) == 0) - whitelist_home(WLDIR_OPT); - } - else - opt_dir = 0; - } - - // /srv mountpoint - if (srv_dir) { - // check if /srv directory exists - if (stat("/srv", &s) == 0) { - // keep a copy of real /srv directory in RUN_WHITELIST_SRV_DIR - mkdir_attr(RUN_WHITELIST_SRV_DIR, 0755, 0, 0); - if (mount("/srv", RUN_WHITELIST_SRV_DIR, NULL, MS_BIND|MS_REC, NULL) < 0) - errExit("mount bind"); - - // mount tmpfs on /srv - if (arg_debug || arg_debug_whitelists) - printf("Mounting tmpfs on /srv directory\n"); - if (mount("tmpfs", "/srv", "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mounting tmpfs on /srv"); - selinux_relabel_path("/srv", "/srv"); - fs_logger("tmpfs /srv"); - - // autowhitelist home directory if it is masked by the tmpfs - if (strncmp(cfg.homedir, "/srv/", 5) == 0) - whitelist_home(WLDIR_SRV); - } - else - srv_dir = 0; - } - - // /etc mountpoint - if (etc_dir) { - // check if /etc directory exists - if (stat("/etc", &s) == 0) { - // keep a copy of real /etc directory in RUN_WHITELIST_ETC_DIR - mkdir_attr(RUN_WHITELIST_ETC_DIR, 0755, 0, 0); - if (mount("/etc", RUN_WHITELIST_ETC_DIR, NULL, MS_BIND|MS_REC, NULL) < 0) - errExit("mount bind"); - - // mount tmpfs on /etc - if (arg_debug || arg_debug_whitelists) - printf("Mounting tmpfs on /etc directory\n"); - if (mount("tmpfs", "/etc", "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mounting tmpfs on /etc"); - selinux_relabel_path("/etc", "/etc"); - fs_logger("tmpfs /etc"); - - // autowhitelist home directory if it is masked by the tmpfs - if (strncmp(cfg.homedir, "/etc/", 5) == 0) - whitelist_home(WLDIR_ETC); - } - else - etc_dir = 0; - } - - // /usr/share mountpoint - if (share_dir) { - // check if /usr/share directory exists - if (stat("/usr/share", &s) == 0) { - // keep a copy of real /usr/share directory in RUN_WHITELIST_ETC_DIR - mkdir_attr(RUN_WHITELIST_SHARE_DIR, 0755, 0, 0); - if (mount("/usr/share", RUN_WHITELIST_SHARE_DIR, NULL, MS_BIND|MS_REC, NULL) < 0) - errExit("mount bind"); - - // mount tmpfs on /srv - if (arg_debug || arg_debug_whitelists) - printf("Mounting tmpfs on /usr/share directory\n"); - if (mount("tmpfs", "/usr/share", "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mounting tmpfs on /usr/share"); - selinux_relabel_path("/usr/share", "/usr/share"); - fs_logger("tmpfs /usr/share"); - - // autowhitelist home directory if it is masked by the tmpfs - if (strncmp(cfg.homedir, "/usr/share/", 11) == 0) - whitelist_home(WLDIR_SHARE); - } - else - share_dir = 0; - } - - // /sys/module mountpoint - if (module_dir) { - // check if /sys/module directory exists - if (stat("/sys/module", &s) == 0) { - // keep a copy of real /sys/module directory in RUN_WHITELIST_MODULE_DIR - mkdir_attr(RUN_WHITELIST_MODULE_DIR, 0755, 0, 0); - if (mount("/sys/module", RUN_WHITELIST_MODULE_DIR, NULL, MS_BIND|MS_REC, NULL) < 0) - errExit("mount bind"); - - // mount tmpfs on /sys/module - if (arg_debug || arg_debug_whitelists) - printf("Mounting tmpfs on /sys/module directory\n"); - if (mount("tmpfs", "/sys/module", "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mounting tmpfs on /sys/module"); - selinux_relabel_path("/sys/module", "/sys/module"); - fs_logger("tmpfs /sys/module"); - } - else - module_dir = 0; - } - - // /run/user/$uid mountpoint - if (run_dir) { - // check if /run/user/$uid directory exists - if (stat(runuser, &s) == 0) { - // keep a copy of real /run/user/$uid directory in RUN_WHITELIST_RUN_USER_DIR - mkdir_attr(RUN_WHITELIST_RUN_USER_DIR, 0700, getuid(), getgid()); - if (mount(runuser, RUN_WHITELIST_RUN_USER_DIR, NULL, MS_BIND|MS_REC, NULL) < 0) - errExit("mount bind"); - - // mount tmpfs on /run/user/$uid - if (arg_debug || arg_debug_whitelists) - printf("Mounting tmpfs on %s directory\n", runuser); - char *options; - if (asprintf(&options, "mode=700,uid=%u,gid=%u", getuid(), getgid()) == -1) - errExit("asprintf"); - if (mount("tmpfs", runuser, "tmpfs", MS_NOSUID | MS_NODEV | MS_STRICTATIME, options) < 0) - errExit("mounting tmpfs on /run/user/"); - selinux_relabel_path(runuser, runuser); - free(options); - fs_logger2("tmpfs", runuser); - - // autowhitelist home directory if it is masked by the tmpfs - if (strncmp(cfg.homedir, runuser, runuser_len) == 0 && cfg.homedir[runuser_len] == '/') - whitelist_home(WLDIR_RUN); - } - else - run_dir = 0; - } - - // home mountpoint - if (home_dir) { - // check if home directory exists - if (stat(cfg.homedir, &s) == 0) { - // keep a copy of real home dir in RUN_WHITELIST_HOME_USER_DIR - mkdir_attr(RUN_WHITELIST_HOME_USER_DIR, 0755, getuid(), getgid()); - int fd = safer_openat(-1, cfg.homedir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); - if (fd == -1) - errExit("safer_openat"); - char *proc; - if (asprintf(&proc, "/proc/self/fd/%d", fd) == -1) - errExit("asprintf"); - if (mount(proc, RUN_WHITELIST_HOME_USER_DIR, NULL, MS_BIND|MS_REC, NULL) < 0) - errExit("mount bind"); - free(proc); - close(fd); - - // mount a tmpfs and initialize home directory - fs_private(); - } - else - home_dir = 0; - } + tmpfs_topdirs(topdirs); // go through profile rules again, and interpret whitelist commands entry = cfg.profile; while (entry) { - // handle only whitelist commands - if (strncmp(entry->data, "whitelist ", 10)) { - entry = entry->next; - continue; - } - -//printf("here %d#%s#\n", __LINE__, entry->data); - // whitelist the real file - whitelist_path(entry); - - // create the link if any - if (entry->link) { - // if the link is already there, do not bother - if (lstat(entry->link, &s) != 0) { - // create the path if necessary - // entry->link has no trailing slashes or single dots - int fd = mkpath(entry->link, 0755); - if (fd == -1) { - if (arg_debug || arg_debug_whitelists) - printf("Debug %d: cannot create symbolic link %s\n", __LINE__, entry->link); - free(entry->link); - entry->link = NULL; - entry = entry->next; - continue; - } - // get file name of symlink - const char *file = gnu_basename(entry->link); - // create the link - int rv = symlinkat(entry->data + 10, fd, file); - if (rv) { - if (arg_debug || arg_debug_whitelists) { - perror("symlink"); - printf("Debug %d: cannot create symbolic link %s\n", __LINE__, entry->link); - } - } - else if (arg_debug || arg_debug_whitelists) - printf("Created symbolic link %s -> %s\n", entry->link, entry->data + 10); - close(fd); + if (entry->wparam) { + char *file = entry->wparam->file; + char *link = entry->wparam->link; + const char *topdir = entry->wparam->top->path; + size_t topdir_len = strlen(topdir); + int dirfd = entry->wparam->top->fd; + + // top level directories of link and file can differ + // whitelist the file only if it is in same top level directory + if (strncmp(file, topdir, topdir_len) == 0 && file[topdir_len] == '/') { + // get path relative to top level directory + const char *rel = file + topdir_len + 1; + whitelist_file(dirfd, topdir, rel, file); } - free(entry->link); - entry->link = NULL; - } - - entry = entry->next; - } - - // mask the real home directory, currently mounted on RUN_WHITELIST_HOME_DIR - if (home_dir) { - if (mount("tmpfs", RUN_WHITELIST_HOME_USER_DIR, "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mount tmpfs"); - fs_logger2("tmpfs", RUN_WHITELIST_HOME_USER_DIR); - } - - // mask the real /tmp directory, currently mounted on RUN_WHITELIST_TMP_DIR - if (tmp_dir) { - if (mount("tmpfs", RUN_WHITELIST_TMP_DIR, "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mount tmpfs"); - fs_logger2("tmpfs", RUN_WHITELIST_TMP_DIR); - } - - // mask the real /var directory, currently mounted on RUN_WHITELIST_VAR_DIR - if (var_dir) { - if (mount("tmpfs", RUN_WHITELIST_VAR_DIR, "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mount tmpfs"); - fs_logger2("tmpfs", RUN_WHITELIST_VAR_DIR); - } - - // mask the real /opt directory, currently mounted on RUN_WHITELIST_OPT_DIR - if (opt_dir) { - if (mount("tmpfs", RUN_WHITELIST_OPT_DIR, "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mount tmpfs"); - fs_logger2("tmpfs", RUN_WHITELIST_OPT_DIR); - } - - // mask the real /dev directory, currently mounted on RUN_WHITELIST_DEV_DIR - if (dev_dir) { - if (mount("tmpfs", RUN_WHITELIST_DEV_DIR, "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mount tmpfs"); - fs_logger2("tmpfs", RUN_WHITELIST_DEV_DIR); - } - - // mask the real /media directory, currently mounted on RUN_WHITELIST_MEDIA_DIR - if (media_dir) { - if (mount("tmpfs", RUN_WHITELIST_MEDIA_DIR, "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mount tmpfs"); - fs_logger2("tmpfs", RUN_WHITELIST_MEDIA_DIR); - } - - // mask the real /mnt directory, currently mounted on RUN_WHITELIST_MNT_DIR - if (mnt_dir) { - if (mount("tmpfs", RUN_WHITELIST_MNT_DIR, "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mount tmpfs"); - fs_logger2("tmpfs", RUN_WHITELIST_MNT_DIR); - } - // mask the real /srv directory, currently mounted on RUN_WHITELIST_SRV_DIR - if (srv_dir) { - if (mount("tmpfs", RUN_WHITELIST_SRV_DIR, "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mount tmpfs"); - fs_logger2("tmpfs", RUN_WHITELIST_SRV_DIR); - } - - // mask the real /etc directory, currently mounted on RUN_WHITELIST_ETC_DIR - if (etc_dir) { - if (mount("tmpfs", RUN_WHITELIST_ETC_DIR, "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mount tmpfs"); - fs_logger2("tmpfs", RUN_WHITELIST_ETC_DIR); - } + // create the link if any + if (link) + whitelist_symlink(topdir, link, file); - // mask the real /usr/share directory, currently mounted on RUN_WHITELIST_SHARE_DIR - if (share_dir) { - if (mount("tmpfs", RUN_WHITELIST_SHARE_DIR, "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mount tmpfs"); - fs_logger2("tmpfs", RUN_WHITELIST_SHARE_DIR); - } - - // mask the real /sys/module directory, currently mounted on RUN_WHITELIST_MODULE_DIR - if (module_dir) { - if (mount("tmpfs", RUN_WHITELIST_MODULE_DIR, "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mount tmpfs"); - fs_logger2("tmpfs", RUN_WHITELIST_MODULE_DIR); - } + free(link); + free(file); + free(entry->wparam); + entry->wparam = NULL; + } - // mask the real /run/user/$uid directory, currently mounted on RUN_WHITELIST_RUN_USER_DIR - if (run_dir) { - if (mount("tmpfs", RUN_WHITELIST_RUN_USER_DIR, "tmpfs", MS_NOSUID | MS_STRICTATIME, "mode=755,gid=0") < 0) - errExit("mount tmpfs"); - fs_logger2("tmpfs", RUN_WHITELIST_RUN_USER_DIR); + entry = entry->next; } + // release resources free(runuser); - return; -errexit: - fprintf(stderr, "Error: invalid whitelist path %s\n", new_name); - exit(1); + size_t i; + for (i = 0; i < TOP_MAX && topdirs[i].path; i++) { + free(topdirs[i].path); + close(topdirs[i].fd); + } + free(topdirs); } diff --git a/src/include/rundefs.h b/src/include/rundefs.h index d14f6782f..a172dd511 100644 --- a/src/include/rundefs.h +++ b/src/include/rundefs.h @@ -84,18 +84,6 @@ #define RUN_DEVLOG_FILE RUN_MNT_DIR "/devlog" #define RUN_WHITELIST_X11_DIR RUN_MNT_DIR "/orig-x11" -#define RUN_WHITELIST_HOME_USER_DIR RUN_MNT_DIR "/orig-home-user" // home directory whitelisting -#define RUN_WHITELIST_RUN_USER_DIR RUN_MNT_DIR "/orig-run-user" // run directory whitelisting -#define RUN_WHITELIST_TMP_DIR RUN_MNT_DIR "/orig-tmp" -#define RUN_WHITELIST_MEDIA_DIR RUN_MNT_DIR "/orig-media" -#define RUN_WHITELIST_MNT_DIR RUN_MNT_DIR "/orig-mnt" -#define RUN_WHITELIST_VAR_DIR RUN_MNT_DIR "/orig-var" -#define RUN_WHITELIST_DEV_DIR RUN_MNT_DIR "/orig-dev" -#define RUN_WHITELIST_OPT_DIR RUN_MNT_DIR "/orig-opt" -#define RUN_WHITELIST_SRV_DIR RUN_MNT_DIR "/orig-srv" -#define RUN_WHITELIST_ETC_DIR RUN_MNT_DIR "/orig-etc" -#define RUN_WHITELIST_SHARE_DIR RUN_MNT_DIR "/orig-share" -#define RUN_WHITELIST_MODULE_DIR RUN_MNT_DIR "/orig-module" #define RUN_XAUTHORITY_FILE RUN_MNT_DIR "/.Xauthority" // private options #define RUN_XAUTH_FILE RUN_MNT_DIR "/xauth" // x11=xorg -- cgit v1.2.3-70-g09d2 From 14b104aa026de9dc1e206bc8b821e516300feee5 Mon Sep 17 00:00:00 2001 From: smitsohu Date: Sat, 8 May 2021 00:05:48 +0200 Subject: tweak --- src/firejail/util.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/firejail/util.c b/src/firejail/util.c index d16354d9d..def635ea7 100644 --- a/src/firejail/util.c +++ b/src/firejail/util.c @@ -1140,7 +1140,7 @@ void disable_file_path(const char *path, const char *file) { // ignore dirfd if path is absolute // https://web.archive.org/web/20180419120236/https://blogs.gnome.org/jamesh/2018/04/19/secure-mounts int safer_openat(int dirfd, const char *path, int flags) { - assert(path); + assert(path && path[0]); flags |= O_NOFOLLOW; int fd = -1; @@ -1161,7 +1161,7 @@ int safer_openat(int dirfd, const char *path, int flags) { if (!dup) errExit("strdup"); char *tok = strtok(dup, "/"); - if (!tok) { // nothing to do, path is either empty string or the root directory + if (!tok) { // nothing to do, path is the root directory free(dup); return openat(dirfd, path, flags); } -- cgit v1.2.3-70-g09d2 From 9e7cad06c86b64a8608e690b8c637c33ce18c6c3 Mon Sep 17 00:00:00 2001 From: smitsohu Date: Sat, 8 May 2021 00:06:14 +0200 Subject: add /run whitelist support --- src/firejail/fs_whitelist.c | 125 +++++++++++++++++++++++++++++--------------- 1 file changed, 84 insertions(+), 41 deletions(-) (limited to 'src') diff --git a/src/firejail/fs_whitelist.c b/src/firejail/fs_whitelist.c index c58b8e786..c7dbe6496 100644 --- a/src/firejail/fs_whitelist.c +++ b/src/firejail/fs_whitelist.c @@ -114,7 +114,7 @@ static void whitelist_file(int dirfd, const char *topdir, const char *relpath, c // 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 no follow symbolic links + // take care to not follow symbolic links int fd = safer_openat(dirfd, relpath, O_PATH|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) { if (arg_debug || arg_debug_whitelists) @@ -307,51 +307,93 @@ static void globbing(const char *pattern) { } // mount tmpfs on all top level directories -static void tmpfs_topdirs(TopDir *topdirs) { - // process user home directory first +static void tmpfs_topdirs(const TopDir *topdirs) { + int tmpfs_home = 0; + int tmpfs_runuser = 0; + int i; for (i = 0; i < TOP_MAX && topdirs[i].path; i++) { + // do user home and /run/user/$UID last if (strcmp(topdirs[i].path, cfg.homedir) == 0) { - fs_private(); - break; + tmpfs_home = 1; + continue; + } + if (strcmp(topdirs[i].path, runuser) == 0) { + tmpfs_runuser = 1; + continue; } - } - for (i = 0; i < TOP_MAX && topdirs[i].path; i++) { - if (strcmp(topdirs[i].path, cfg.homedir) != 0) { - // mount the tmpfs - fs_tmpfs(topdirs[i].path, 0); - selinux_relabel_path(topdirs[i].path, topdirs[i].path); + // 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); + + // init tmpfs + if (strcmp(topdirs[i].path, "/run") == 0) { + // restore /run/firejail directory + if (mkdir(RUN_FIREJAIL_DIR, 0755) == -1) + errExit("mkdir"); + char *proc; + if (asprintf(&proc, "/proc/self/fd/%d", fd) == -1) + errExit("asprintf"); + if (mount(proc, RUN_FIREJAIL_DIR, NULL, MS_BIND | MS_REC, NULL) < 0) + errExit("mount bind"); + free(proc); + close(fd); + fs_logger2("whitelist", RUN_FIREJAIL_DIR); - // init tmpfs + // restore /run/user/$UID directory + // get path relative to /run + const char *rel = runuser + 5; + whitelist_file(topdirs[i].fd, topdirs[i].path, rel, runuser); + } + else if (strcmp(topdirs[i].path, "/tmp") == 0) { // fix pam-tmpdir (#2685) - if (strcmp(topdirs[i].path, "/tmp") == 0) { - const char *env = env_get("TMP"); - if (env) { - char *pamtmpdir; - if (asprintf(&pamtmpdir, "/tmp/user/%u", getuid()) == -1) - errExit("asprintf"); - if (strcmp(env, pamtmpdir) == 0) { - // create empty user-owned /tmp/user/$UID directory - mkdir_attr("/tmp/user", 0711, 0, 0); - selinux_relabel_path("/tmp/user", "/tmp/user"); - fs_logger("mkdir /tmp/user"); - mkdir_attr(pamtmpdir, 0700, getuid(), 0); - selinux_relabel_path(pamtmpdir, pamtmpdir); - fs_logger2("mkdir", pamtmpdir); - } - free(pamtmpdir); + const char *env = env_get("TMP"); + if (env) { + char *pamtmpdir; + if (asprintf(&pamtmpdir, "/tmp/user/%u", getuid()) == -1) + errExit("asprintf"); + if (strcmp(env, pamtmpdir) == 0) { + // create empty user-owned /tmp/user/$UID directory + mkdir_attr("/tmp/user", 0711, 0, 0); + selinux_relabel_path("/tmp/user", "/tmp/user"); + fs_logger("mkdir /tmp/user"); + mkdir_attr(pamtmpdir, 0700, getuid(), 0); + selinux_relabel_path(pamtmpdir, pamtmpdir); + fs_logger2("mkdir", pamtmpdir); } + free(pamtmpdir); } + } - // bring back user home directory if it is masked by the tmpfs - size_t topdir_len = strlen(topdirs[i].path); - if (strncmp(topdirs[i].path, cfg.homedir, topdir_len) == 0 && cfg.homedir[topdir_len] == '/') { - // get path relative to top level directory - const char *rel = cfg.homedir + topdir_len + 1; - whitelist_file(topdirs[i].fd, topdirs[i].path, rel, cfg.homedir); - } + // restore user home directory if it is masked by the tmpfs + // creates path owned by root + size_t topdir_len = strlen(topdirs[i].path); + if (strncmp(topdirs[i].path, cfg.homedir, topdir_len) == 0 && cfg.homedir[topdir_len] == '/') { + // get path relative to top level directory + const char *rel = cfg.homedir + topdir_len + 1; + whitelist_file(topdirs[i].fd, topdirs[i].path, rel, cfg.homedir); } + + selinux_relabel_path(topdirs[i].path, topdirs[i].path); + } + + // 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); } } @@ -372,10 +414,9 @@ static int reject_topdir(const char *dir) { static TopDir *add_topdir(const char *dir, TopDir *topdirs, const char *path) { assert(dir && path); - // /proc, /run and /sys are not allowed + // /proc and /sys are not allowed if (strcmp(dir, "/") == 0 || strcmp(dir, "/proc") == 0 || - strcmp(dir, "/run") == 0 || strcmp(dir, "/sys") == 0) whitelist_error(path); @@ -448,14 +489,16 @@ static char *extract_topdir(const char *path) { if (!dup) errExit("strdup"); - // user home is treated as top level directory + // 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'; - // whitelisting in /run and /sys is not allowed, - // but /run/user/$UID and /sys/module are exceptions - // and are treated as top level directories here + // /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 -- cgit v1.2.3-70-g09d2