/* * 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. */ #include "firejail.h" #include "../include/firejail_user.h" #include #include #include #include #include #include #include #include #ifndef O_PATH #define O_PATH 010000000 #endif #define MAXBUF 1024 // linked list of users typedef struct user_list { struct user_list *next; const char *user; } USER_LIST; USER_LIST *ulist = NULL; static void ulist_add(const char *user) { assert(user); USER_LIST *nlist = malloc(sizeof(USER_LIST)); if (!nlist) errExit("malloc"); memset(nlist, 0, sizeof(USER_LIST)); nlist->user = user; nlist->next = ulist; ulist = nlist; } static USER_LIST *ulist_find(const char *user) { assert(user); USER_LIST *ptr = ulist; while (ptr) { if (strcmp(ptr->user, user) == 0) return ptr; ptr = ptr->next; } return NULL; } static void sanitize_home(void) { assert(getuid() != 0); // this code works only for regular users struct stat s; if (arg_debug) printf("Cleaning /home directory\n"); // open user home directory in order to keep it around 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 if (errno != EACCES) errExit("fstat"); close(fd); goto errout; } // mount tmpfs on /home if (mount("tmpfs", "/home", "tmpfs", MS_NOSUID | MS_NODEV | MS_STRICTATIME, "mode=755,gid=0") < 0) errExit("mount tmpfs"); selinux_relabel_path("/home", "/home"); fs_logger("tmpfs /home"); // create new user home directory if (mkdir(cfg.homedir, 0755) == -1) { if (mkpath_as_root(cfg.homedir) == -1) errExit("mkpath"); if (mkdir(cfg.homedir, 0755) == -1) errExit("mkdir"); } fs_logger2("mkdir", cfg.homedir); // set mode and ownership if (set_perms(cfg.homedir, s.st_uid, s.st_gid, s.st_mode)) errExit("set_perms"); selinux_relabel_path(cfg.homedir, cfg.homedir); // bring back real user home directory if (bind_mount_fd_to_path(fd, cfg.homedir)) errExit("mount bind"); close(fd); if (!arg_private) fs_logger2("whitelist", cfg.homedir); return; errout: fwarning("cannot clean /home directory\n"); } static void sanitize_run(void) { if (arg_debug) printf("Cleaning /run/user directory\n"); char *runuser; if (asprintf(&runuser, "/run/user/%u", getuid()) == -1) errExit("asprintf"); // open /run/user/$UID directory in order to keep it around int fd = open(runuser, O_PATH|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); if (fd == -1) { if (arg_debug) printf("Cannot open %s directory\n", runuser); free(runuser); return; } // mount tmpfs on /run/user if (mount("tmpfs", "/run/user", "tmpfs", MS_NOSUID | MS_NODEV | MS_STRICTATIME, "mode=755,gid=0") < 0) errExit("mount tmpfs"); selinux_relabel_path("/run/user", "/run/user"); fs_logger("tmpfs /run/user"); // create new user directory if (mkdir(runuser, 0700) == -1) errExit("mkdir"); fs_logger2("mkdir", runuser); // set mode and ownership if (set_perms(runuser, getuid(), getgid(), 0700)) errExit("set_perms"); selinux_relabel_path(runuser, runuser); // bring back real run/user/$UID directory if (bind_mount_fd_to_path(fd, runuser)) errExit("mount bind"); close(fd); fs_logger2("whitelist", runuser); free(runuser); } static void sanitize_passwd(void) { struct stat s; if (stat("/etc/passwd", &s) == -1) return; assert(uid_min); if (arg_debug) printf("Sanitizing /etc/passwd, UID_MIN %d\n", uid_min); if (is_link("/etc/passwd")) { fprintf(stderr, "Error: invalid /etc/passwd\n"); exit(1); } FILE *fpin = NULL; FILE *fpout = NULL; // open files /* coverity[toctou] */ fpin = fopen("/etc/passwd", "re"); if (!fpin) goto errout; fpout = fopen(RUN_PASSWD_FILE, "we"); if (!fpout) goto errout; // read the file line by line char buf[MAXBUF]; uid_t myuid = getuid(); while (fgets(buf, MAXBUF, fpin)) { // comments and empty lines if (*buf == '\0' || *buf == '#') continue; // sample line: // www-data:x:33:33:www-data:/var/www:/bin/sh // drop lines with uid > 1000 and not the current user char *ptr = buf; // advance to uid while (*ptr != ':' && *ptr != '\0') ptr++; if (*ptr == '\0') goto errout; char *ptr1 = ptr; ptr++; while (*ptr != ':' && *ptr != '\0') ptr++; if (*ptr == '\0') goto errout; ptr++; if (*ptr == '\0') goto errout; // process uid int uid; int rv = sscanf(ptr, "%d:", &uid); if (rv == 0 || uid < 0) goto errout; assert(uid_min); if (uid < uid_min || uid == 65534) { // on Debian platforms user nobody is 65534 fprintf(fpout, "%s", buf); continue; } if ((uid_t) uid != myuid) { // store user name - necessary to process /etc/group *ptr1 = '\0'; char *user = strdup(buf); if (!user) errExit("malloc"); ulist_add(user); continue; // skip line } fprintf(fpout, "%s", buf); } fclose(fpin); SET_PERMS_STREAM(fpout, 0, 0, 0644); fclose(fpout); // mount-bind tne new password file if (mount(RUN_PASSWD_FILE, "/etc/passwd", "none", MS_BIND, "mode=400,gid=0") < 0) errExit("mount"); // blacklist RUN_PASSWD_FILE if (mount(RUN_RO_FILE, RUN_PASSWD_FILE, "none", MS_BIND, "mode=400,gid=0") < 0) errExit("mount"); fs_logger("create /etc/passwd"); return; errout: fwarning("failed to clean up /etc/passwd\n"); if (fpin) fclose(fpin); if (fpout) fclose(fpout); } // returns 1 if fails, 0 if OK static int copy_line(FILE *fpout, char *buf, char *ptr) { // fpout: GROUP_FILE // buf: pulse:x:115:netblue,bingo // ptr: 115:neblue,bingo while (*ptr != ':' && *ptr != '\0') ptr++; if (*ptr == '\0') return 1; ptr++; if (*ptr == '\n' || *ptr == '\0') { fprintf(fpout, "%s", buf); return 0; } // print what we have so far char tmp = *ptr; *ptr = '\0'; fprintf(fpout, "%s", buf); *ptr = tmp; // tokenize char *token = strtok(ptr, ",\n"); int first = 1; while (token) { char *newtoken = strtok(NULL, ",\n"); if (ulist_find(token)) { //skip token = newtoken; continue; } if (!first) fprintf(fpout, ","); first = 0; fprintf(fpout, "%s", token); token = newtoken; } fprintf(fpout, "\n"); return 0; } static void sanitize_group(void) { struct stat s; if (stat("/etc/group", &s) == -1) return; assert(gid_min); if (arg_debug) printf("Sanitizing /etc/group, GID_MIN %d\n", gid_min); if (is_link("/etc/group")) { fprintf(stderr, "Error: invalid /etc/group\n"); exit(1); } FILE *fpin = NULL; FILE *fpout = NULL; // open files /* coverity[toctou] */ fpin = fopen("/etc/group", "re"); if (!fpin) goto errout; fpout = fopen(RUN_GROUP_FILE, "we"); if (!fpout) goto errout; // read the file line by line char buf[MAXBUF]; gid_t mygid = getgid(); while (fgets(buf, MAXBUF, fpin)) { // comments and empty lines if (*buf == '\0' || *buf == '#') continue; // sample line: // pulse:x:115:netblue,bingo // drop lines with uid > 1000 and not the current user group char *ptr = buf; // advance to uid while (*ptr != ':' && *ptr != '\0') ptr++; if (*ptr == '\0') goto errout; ptr++; while (*ptr != ':' && *ptr != '\0') ptr++; if (*ptr == '\0') goto errout; ptr++; if (*ptr == '\0') goto errout; // process uid int gid; int rv = sscanf(ptr, "%d:", &gid); if (rv == 0 || gid < 0) goto errout; assert(gid_min); if (gid < gid_min || gid == 65534) { // on Debian platforms 65534 is group nogroup if (copy_line(fpout, buf, ptr)) goto errout; continue; } if ((gid_t) gid != mygid) { continue; // skip line } if (copy_line(fpout, buf, ptr)) goto errout; } fclose(fpin); SET_PERMS_STREAM(fpout, 0, 0, 0644); fclose(fpout); // mount-bind tne new group file if (mount(RUN_GROUP_FILE, "/etc/group", "none", MS_BIND, "mode=400,gid=0") < 0) errExit("mount"); // blacklist RUN_GROUP_FILE if (mount(RUN_RO_FILE, RUN_GROUP_FILE, "none", MS_BIND, "mode=400,gid=0") < 0) errExit("mount"); fs_logger("create /etc/group"); return; errout: fwarning("failed to clean up /etc/group\n"); if (fpin) fclose(fpin); if (fpout) fclose(fpout); } void restrict_users(void) { if (arg_allusers) return; // only in user mode if (getuid()) { if (strncmp(cfg.homedir, "/home/", 6) == 0) { // user has the home directory under /home sanitize_home(); } else { // user has the home directory outside /home // mount tmpfs on top of /home in order to hide it if (mount("tmpfs", "/home", "tmpfs", MS_NOSUID | MS_NODEV | MS_STRICTATIME, "mode=755,gid=0") < 0) errExit("mount tmpfs"); fs_logger("tmpfs /home"); } sanitize_run(); sanitize_passwd(); sanitize_group(); } }