/* * Copyright (C) 2014-2022 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 #ifndef O_PATH #define O_PATH 010000000 #endif #include #ifndef PR_SET_NO_NEW_PRIVS #define PR_SET_NO_NEW_PRIVS 38 #endif static int apply_caps = 0; static uint64_t caps = 0; static unsigned display = 0; #define BUFLEN 4096 static void signal_handler(int sig){ flush_stdin(); exit(128 + sig); } static void install_handler(void) { struct sigaction sga; // handle SIGTERM sigemptyset(&sga.sa_mask); sga.sa_handler = signal_handler; sga.sa_flags = 0; sigaction(SIGTERM, &sga, NULL); } static void extract_x11_display(pid_t pid) { char *fname; if (asprintf(&fname, "%s/%d", RUN_FIREJAIL_X11_DIR, pid) == -1) errExit("asprintf"); FILE *fp = fopen(fname, "re"); free(fname); if (!fp) return; if (1 != fscanf(fp, "%u", &display)) { fprintf(stderr, "Error: cannot read X11 display file\n"); fclose(fp); return; } fclose(fp); // check display range if (display < X11_DISPLAY_START || display > X11_DISPLAY_END) { fprintf(stderr, "Error: invalid X11 display range\n"); return; } // store the display number for join process in /run/firejail/x11 EUID_ROOT(); set_x11_run_file(getpid(), display); EUID_USER(); } static void extract_command(int argc, char **argv, int index) { EUID_ASSERT(); if (index >= argc) return; // doubledash followed by positional parameters if (strcmp(argv[index], "--") == 0) { arg_doubledash = 1; index++; if (index >= argc) return; } // first argv needs to be a valid command if (arg_doubledash == 0 && *argv[index] == '-') { fprintf(stderr, "Error: invalid option %s after --join\n", argv[index]); exit(1); } // build command build_cmdline(&cfg.command_line, &cfg.window_title, argc, argv, index, true); } #if 0 static int open_shell(void) { EUID_ASSERT(); if (arg_debug) printf("Opening shell %s\n", cfg.usershell); // file descriptor will leak if not opened with O_CLOEXEC !! int fd = open(cfg.usershell, O_PATH|O_CLOEXEC); if (fd == -1) { fprintf(stderr, "Error: cannot open shell %s\n", cfg.usershell); exit(1); } // file descriptor needs to reach final fexecve if (asprintf(&cfg.keep_fd, "%s,%d", cfg.keep_fd ? cfg.keep_fd : "", fd) == -1) errExit("asprintf"); return fd; } #endif static void extract_nogroups(ProcessHandle sandbox) { struct stat s; if (process_rootfs_stat(sandbox, RUN_GROUPS_CFG, &s) == 0) arg_nogroups = 1; else if (errno != ENOENT) errExit("stat"); } static void extract_nonewprivs(ProcessHandle sandbox) { struct stat s; if (process_rootfs_stat(sandbox, RUN_NONEWPRIVS_CFG, &s) == 0) arg_nonewprivs = 1; else if (errno != ENOENT) errExit("stat"); } static void extract_caps(ProcessHandle sandbox) { // open status file FILE *fp = process_fopen(sandbox, "status"); char buf[BUFLEN]; while (fgets(buf, BUFLEN, fp)) { if (strncmp(buf, "CapBnd:", 7) == 0) { unsigned long long val; if (sscanf(buf + 7, "%llx", &val) != 1) goto errexit; apply_caps = 1; caps = val; } else if (strncmp(buf, "NoNewPrivs:", 11) == 0) { int val; if (sscanf(buf + 11, "%d", &val) != 1) goto errexit; if (val) arg_nonewprivs = 1; } } fclose(fp); return; errexit: fprintf(stderr, "Error: cannot read /proc/%d/status\n", process_get_pid(sandbox)); exit(1); } static void extract_user_namespace(ProcessHandle sandbox) { // test user namespaces available in the kernel struct stat self_userns; if (stat("/proc/self/ns/user", &self_userns) != 0) return; // check sandbox user namespace struct stat dest_userns; process_stat(sandbox, "ns/user", &dest_userns); if (dest_userns.st_ino != self_userns.st_ino || dest_userns.st_dev != self_userns.st_dev) arg_noroot = 1; } static void extract_cpu(ProcessHandle sandbox) { int fd = process_rootfs_open(sandbox, RUN_CPU_CFG); if (fd < 0) return; // not configured FILE *fp = fdopen(fd, "r"); if (!fp) errExit("fdopen"); unsigned tmp; if (fscanf(fp, "%x", &tmp) == 1) cfg.cpus = (uint32_t) tmp; fclose(fp); } static void extract_umask(ProcessHandle sandbox) { int fd = process_rootfs_open(sandbox, RUN_UMASK_FILE); if (fd < 0) { fprintf(stderr, "Error: cannot open umask file\n"); exit(1); } FILE *fp = fdopen(fd, "r"); if (!fp) errExit("fdopen"); if (fscanf(fp, "%3o", &orig_umask) != 1) { fprintf(stderr, "Error: cannot read umask\n"); exit(1); } fclose(fp); } // returns false if the sandbox is not fully set up yet, // or true if the sandbox is complete static bool has_join_file(ProcessHandle sandbox) { // check if a file /run/firejail/mnt/join exists int fd = process_rootfs_open(sandbox, RUN_JOIN_FILE); if (fd == -1) return false; struct stat s; if (fstat(fd, &s) == -1) errExit("fstat"); if (!S_ISREG(s.st_mode) || s.st_uid != 0 || s.st_size != 1) { close(fd); return false; } char status; if (read(fd, &status, 1) == 1 && status == SANDBOX_DONE) { close(fd); return true; } close(fd); return false; } #define SNOOZE 10000 // sleep interval in microseconds static void check_joinable(ProcessHandle sandbox) { // check if pid belongs to a fully set up firejail sandbox unsigned long i; for (i = SNOOZE; has_join_file(sandbox) == false; i += SNOOZE) { // give sandbox some time to start up if (i > join_timeout) { fprintf(stderr, "Error: no valid sandbox\n"); exit(1); } usleep(SNOOZE); } } static ProcessHandle find_pidns_parent(pid_t pid) { // identify current pid namespace struct stat self_pidns; if (stat("/proc/self/ns/pid", &self_pidns) < 0) errExit("stat"); ProcessHandle process = pin_process(pid); // in case pid is member of a different pid namespace // find parent who created that namespace while (1) { struct stat dest_pidns; process_stat(process, "ns/pid", &dest_pidns); if (dest_pidns.st_ino == self_pidns.st_ino && dest_pidns.st_dev == self_pidns.st_dev) break; // always true for init process // next parent process ProcessHandle next = pin_parent_process(process); unpin_process(process); process = next; } return process; } static void check_firejail_comm(ProcessHandle process) { // open /proc/pid/comm // note: comm value is under control of the target process FILE *fp = process_fopen(process, "comm"); char comm[16]; if (fscanf(fp, "%15s", comm) != 1) { fprintf(stderr, "Error: cannot read /proc file\n"); exit(1); } fclose(fp); if (strcmp(comm, "firejail") != 0) { fprintf(stderr, "Error: no valid sandbox\n"); exit(1); } return; } static void check_firejail_credentials(ProcessHandle process) { // open /proc/pid/status FILE *fp = process_fopen(process, "status"); uid_t ruid = -1; uid_t suid = -1; char buf[4096]; while (fgets(buf, sizeof(buf), fp)) { if (sscanf(buf, "Uid: %u %*u %u", &ruid, &suid) == 2) break; } fclose(fp); // target process should be privileged and owned by the user if (suid != 0) goto errexit; uid_t u = getuid(); if (ruid != u && u != 0) goto errexit; return; errexit: fprintf(stderr, "Error: no valid sandbox\n"); exit(1); } static pid_t read_sandbox_pidfile(pid_t parent) { char *pidfile; if (asprintf(&pidfile, "%s/%d", RUN_FIREJAIL_SANDBOX_DIR, parent) == -1) errExit("asprintf"); // open the pidfile EUID_ROOT(); int pidfile_fd = open(pidfile, O_RDWR|O_CLOEXEC); free(pidfile); EUID_USER(); if (pidfile_fd < 0) goto errexit; // assume pidfile is outdated if parent doesn't hold a lock struct flock pidfile_lock = { .l_type = F_WRLCK, .l_whence = SEEK_SET, .l_start = 0, .l_len = 0, .l_pid = 0, }; if (fcntl(pidfile_fd, F_GETLK, &pidfile_lock) < 0) errExit("fcntl"); if (pidfile_lock.l_type == F_UNLCK) goto errexit; if (pidfile_lock.l_pid != parent) goto errexit; // read pidfile pid_t sandbox; FILE *fp = fdopen(pidfile_fd, "r"); if (!fp) errExit("fdopen"); if (fscanf(fp, "%d", &sandbox) != 1) goto errexit; fclose(fp); return sandbox; errexit: fprintf(stderr, "Error: no valid sandbox\n"); exit(1); } static ProcessHandle switch_to_sandbox(ProcessHandle parent) { // firejail forks many children, identify the sandbox child // using a pidfile created by the sandbox parent pid_t pid = read_sandbox_pidfile(process_get_pid(parent)); // pin the sandbox child fmessage("Switching to pid %d, the first child process inside the sandbox\n", pid); ProcessHandle sandbox = pin_child_process(parent, pid); return sandbox; } ProcessHandle pin_sandbox_process(pid_t pid) { EUID_ASSERT(); ProcessHandle parent = find_pidns_parent(pid); check_firejail_comm(parent); check_firejail_credentials(parent); ProcessHandle sandbox = switch_to_sandbox(parent); check_joinable(sandbox); unpin_process(parent); return sandbox; } void join(pid_t pid, int argc, char **argv, int index) { EUID_ASSERT(); ProcessHandle sandbox = pin_sandbox_process(pid); extract_x11_display(pid); int shfd = -1; // Note: this might be used by joining appimages!!!! // if (!arg_shell_none) // shfd = open_shell(); // in user mode set caps seccomp, cpu etc. if (getuid() != 0) { extract_nonewprivs(sandbox); // redundant on Linux >= 4.10; duplicated in function extract_caps extract_caps(sandbox); extract_cpu(sandbox); extract_nogroups(sandbox); extract_user_namespace(sandbox); extract_umask(sandbox); } // join namespaces EUID_ROOT(); if (arg_join_network) { if (process_join_namespace(sandbox, "net")) exit(1); } else if (arg_join_filesystem) { if (process_join_namespace(sandbox, "mnt")) exit(1); } else { if (process_join_namespace(sandbox, "ipc") || process_join_namespace(sandbox, "net") || process_join_namespace(sandbox, "pid") || process_join_namespace(sandbox, "uts") || process_join_namespace(sandbox, "mnt")) exit(1); } pid_t child = fork(); if (child < 0) errExit("fork"); if (child == 0) { // drop discretionary access control capabilities for root sandboxes caps_drop_dac_override(); if (!arg_join_network) { // mount namespace doesn't know about --chroot fmessage("Changing root to /proc/%d/root\n", process_get_pid(sandbox)); process_rootfs_chroot(sandbox); // load seccomp filters if (getuid() != 0) seccomp_load_file_list(); } // set caps filter if (apply_caps == 1) // not available for uid 0 caps_set(caps); // user namespace if (arg_noroot) { // not available for uid 0 if (arg_debug) printf("Joining user namespace\n"); if (process_join_namespace(sandbox, "user")) exit(1); // user namespace resets capabilities // set caps filter if (apply_caps == 1) // not available for uid 0 caps_set(caps); } EUID_USER(); unpin_process(sandbox); int cwd = 0; if (cfg.cwd) { if (chdir(cfg.cwd) == 0) cwd = 1; } if (!cwd) { if (chdir("/") < 0) errExit("chdir"); if (cfg.homedir) { struct stat s; if (stat(cfg.homedir, &s) == 0) { /* coverity[toctou] */ if (chdir(cfg.homedir) < 0) errExit("chdir"); } } } // set nonewprivs #ifndef HAVE_FORCE_NONEWPRIVS if (arg_nonewprivs == 1) // not available for uid 0 #endif { if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) errExit("prctl"); if (arg_debug) printf("NO_NEW_PRIVS set\n"); } // drop privileges drop_privs(arg_nogroups); // kill the child in case the parent died prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0); extract_command(argc, argv, index); if (cfg.command_line == NULL) cfg.window_title = cfg.usershell; else if (arg_debug) printf("Extracted command #%s#\n", cfg.command_line); // set cpu affinity if (cfg.cpus) // not available for uid 0 set_cpu_affinity(); // add x11 display if (display) { char *display_str; if (asprintf(&display_str, ":%d", display) == -1) errExit("asprintf"); env_store_name_val("DISPLAY", display_str, SETENV); free(display_str); } #ifdef HAVE_DBUSPROXY // set D-Bus environment variables struct stat s; if (stat(RUN_DBUS_USER_SOCKET, &s) == 0) dbus_set_session_bus_env(); if (stat(RUN_DBUS_SYSTEM_SOCKET, &s) == 0) dbus_set_system_bus_env(); #endif start_application(arg_join_network || arg_join_filesystem, shfd, NULL); __builtin_unreachable(); } EUID_USER(); unpin_process(sandbox); if (shfd != -1) close(shfd); int status = 0; //***************************** // following code is signal-safe install_handler(); // wait for the child to finish waitpid(child, &status, 0); // restore default signal action signal(SIGTERM, SIG_DFL); // end of signal-safe code //***************************** if (WIFEXITED(status)) { // if we had a proper exit, return that exit status status = WEXITSTATUS(status); } else if (WIFSIGNALED(status)) { // distinguish fatal signals by adding 128 status = 128 + WTERMSIG(status); } else { status = -1; } flush_stdin(); exit(status); }