/* * Copyright (C) 2014-2021 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. */ #ifdef HAVE_CHROOT #include "firejail.h" #include #include #include #include #ifndef O_PATH #define O_PATH 010000000 #endif #ifdef HAVE_GCOV #include #endif // exit if error void fs_check_chroot_dir(void) { EUID_ASSERT(); assert(cfg.chrootdir); if (strstr(cfg.chrootdir, "..") || is_link(cfg.chrootdir)) goto errout; // check chroot dirname exists, chrooting into the root directory is not allowed char *rpath = realpath(cfg.chrootdir, NULL); if (rpath == NULL || !is_dir(rpath) || strcmp(rpath, "/") == 0) goto errout; char *overlay; if (asprintf(&overlay, "%s/.firejail", cfg.homedir) == -1) errExit("asprintf"); if (strncmp(rpath, overlay, strlen(overlay)) == 0) { fprintf(stderr, "Error: invalid chroot directory: no directories in %s are allowed\n", overlay); exit(1); } free(overlay); cfg.chrootdir = rpath; return; errout: fprintf(stderr, "Error: invalid chroot directory %s\n", cfg.chrootdir); exit(1); } // copy /etc/resolv.conf or /etc/machine-id in chroot directory static void update_file(int parentfd, const char *relpath) { assert(relpath && relpath[0] && relpath[0] != '/'); char *abspath; if (asprintf(&abspath, "/%s", relpath) == -1) errExit("asprintf"); int in = open(abspath, O_RDONLY|O_CLOEXEC); free(abspath); if (in == -1) goto errout; struct stat src; if (fstat(in, &src) == -1) errExit("fstat"); // try to detect if file has been bind mounted into the chroot struct stat dst; if (fstatat(parentfd, relpath, &dst, 0) == 0) { if (src.st_dev == dst.st_dev && src.st_ino == dst.st_ino) { close(in); return; } } if (arg_debug) printf("Updating chroot /%s\n", relpath); unlinkat(parentfd, relpath, 0); int out = openat(parentfd, relpath, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC, S_IRUSR | S_IWRITE | S_IRGRP | S_IROTH); if (out == -1) { close(in); goto errout; } if (sendfile(out, in, NULL, src.st_size) == -1) errExit("sendfile"); close(in); close(out); return; errout: fwarning("chroot /%s not initialized\n", relpath); } // exit if error static void check_subdir(int parentfd, const char *subdir, int check_writable) { assert(subdir && subdir[0] && subdir[0] != '/'); struct stat s; if (fstatat(parentfd, subdir, &s, AT_SYMLINK_NOFOLLOW) != 0) { fprintf(stderr, "Error: cannot find /%s in chroot directory\n", subdir); exit(1); } if (!S_ISDIR(s.st_mode)) { if (S_ISLNK(s.st_mode)) fprintf(stderr, "Error: chroot /%s is a symbolic link\n", subdir); else fprintf(stderr, "Error: chroot /%s is not a directory\n", subdir); exit(1); } if (s.st_uid != 0) { fprintf(stderr, "Error: chroot /%s should be owned by root\n", subdir); exit(1); } if (check_writable && ((S_IWGRP|S_IWOTH) & s.st_mode) != 0) { fprintf(stderr, "Error: only root user should be given write permission on chroot /%s\n", subdir); exit(1); } } // chroot into an existing directory; mount existing /dev and update /etc/resolv.conf void fs_chroot(const char *rootdir) { assert(rootdir); // fails if there is any symlink or if rootdir is not a directory int parentfd = safer_openat(-1, rootdir, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (parentfd == -1) 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; if (fstat(parentfd, &s) == -1) errExit("fstat"); if (s.st_uid != 0) { fprintf(stderr, "Error: chroot directory should be owned by root\n"); exit(1); } if (((S_IWGRP|S_IWOTH) & s.st_mode) != 0) { fprintf(stderr, "Error: only root user should be given write permission on chroot directory\n"); exit(1); } // check chroot subdirectories; /tmp/.X11-unix and /run are treated separately check_subdir(parentfd, "dev", 0); check_subdir(parentfd, "etc", 1); check_subdir(parentfd, "proc", 0); check_subdir(parentfd, "tmp", 0); check_subdir(parentfd, "var", 1); check_subdir(parentfd, "var/tmp", 0); // mount-bind a /dev in rootdir if (arg_debug) printf("Mounting /dev on chroot /dev\n"); // open chroot /dev to get a file descriptor, // then use this descriptor as a mount target int fd = openat(parentfd, "dev", O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) errExit("open"); if (bind_mount_path_to_fd("/dev", fd)) errExit("mounting /dev"); close(fd); #ifdef HAVE_X11 // if users want this mount, they should set FIREJAIL_CHROOT_X11 if (env_get("FIREJAIL_X11") || env_get("FIREJAIL_CHROOT_X11")) { if (arg_debug) printf("Mounting /tmp/.X11-unix on chroot /tmp/.X11-unix\n"); struct stat s1, s2; if (stat("/tmp", &s1) || lstat("/tmp/.X11-unix", &s2)) errExit("mounting /tmp/.X11-unix"); if ((s1.st_mode & S_ISVTX) != S_ISVTX) { fprintf(stderr, "Error: sticky bit not set on /tmp directory\n"); exit(1); } if (s2.st_uid != 0) { fprintf(stderr, "Error: /tmp/.X11-unix not owned by root user\n"); exit(1); } check_subdir(parentfd, "tmp/.X11-unix", 0); fd = openat(parentfd, "tmp/.X11-unix", O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) errExit("open"); if (bind_mount_path_to_fd("/tmp/.X11-unix", fd)) errExit("mounting /tmp/.X11-unix"); close(fd); } #endif // HAVE_X11 // some older distros don't have a /run directory, create one by default if (mkdirat(parentfd, "run", 0755) == -1 && errno != EEXIST) errExit("mkdir"); check_subdir(parentfd, "run", 1); // pulseaudio; only support for default directory /run/user/$UID/pulse if (env_get("FIREJAIL_CHROOT_PULSE")) { char *pulse; if (asprintf(&pulse, "%s/run/user/%d/pulse", cfg.chrootdir, getuid()) == -1) errExit("asprintf"); char *orig_pulse = pulse + strlen(cfg.chrootdir); if (arg_debug) printf("Mounting %s on chroot %s\n", orig_pulse, orig_pulse); 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 = 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); } if (bind_mount_by_fd(src, dst)) errExit("mounting pulseaudio"); close(src); close(dst); free(pulse); // update /etc/machine-id in chroot update_file(parentfd, "etc/machine-id"); } // create /run/firejail directory in chroot if (mkdirat(parentfd, &RUN_FIREJAIL_DIR[1], 0755) == -1 && errno != EEXIST) errExit("mkdir"); check_subdir(parentfd, &RUN_FIREJAIL_DIR[1], 1); // create /run/firejail/lib directory in chroot if (mkdirat(parentfd, &RUN_FIREJAIL_LIB_DIR[1], 0755) == -1 && errno != EEXIST) errExit("mkdir"); check_subdir(parentfd, &RUN_FIREJAIL_LIB_DIR[1], 1); // mount lib directory into the chroot fd = openat(parentfd, &RUN_FIREJAIL_LIB_DIR[1], O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) errExit("open"); if (bind_mount_path_to_fd(RUN_FIREJAIL_LIB_DIR, fd)) errExit("mount bind"); close(fd); // create /run/firejail/mnt directory in chroot if (mkdirat(parentfd, &RUN_MNT_DIR[1], 0755) == -1 && errno != EEXIST) errExit("mkdir"); check_subdir(parentfd, &RUN_MNT_DIR[1], 1); // mount the current mnt directory into the chroot fd = openat(parentfd, &RUN_MNT_DIR[1], O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) errExit("open"); if (bind_mount_path_to_fd(RUN_MNT_DIR, fd)) errExit("mount bind"); close(fd); // update chroot resolv.conf update_file(parentfd, "etc/resolv.conf"); #ifdef HAVE_GCOV __gcov_flush(); #endif // create /run/firejail/mnt/oroot char *oroot = RUN_OVERLAY_ROOT; if (mkdir(oroot, 0755) == -1) errExit("mkdir"); // mount the chroot dir on top of /run/firejail/mnt/oroot in order to reuse the apparmor rules for overlay if (bind_mount_fd_to_path(parentfd, oroot)) errExit("mounting rootdir oroot"); close(parentfd); // chroot into the new directory if (arg_debug) printf("Chrooting into %s\n", rootdir); if (chroot(oroot) < 0) errExit("chroot"); // mount a new proc filesystem if (arg_debug) printf("Mounting /proc filesystem representing the PID namespace\n"); if (mount("proc", "/proc", "proc", MS_NOSUID | MS_NOEXEC | MS_NODEV | MS_REC, NULL) < 0) errExit("mounting /proc"); // create all other /run/firejail files and directories preproc_build_firejail_dir(); // update /var directory in order to support multiple sandboxes running on the same root directory // if (!arg_private_dev) // fs_dev_shm(); fs_var_lock(); if (!arg_keep_var_tmp) fs_var_tmp(); if (!arg_writable_var_log) fs_var_log(); fs_var_lib(); fs_var_cache(); fs_var_utmp(); fs_machineid(); // don't leak user information restrict_users(); // when starting as root, firejail config is not disabled; if (getuid() != 0) disable_config(); } #endif // HAVE_CHROOT