experimental: remounts child mount points as well (read-only, read-write, noexec)

This commit is contained in:
smitsohu 2018-10-25 22:07:19 +02:00
parent 5df7c4d862
commit a7164cb381
5 changed files with 305 additions and 94 deletions

View file

@ -440,8 +440,10 @@ void preproc_clean_run(void);
void fs_blacklist(void);
// remount a directory read-only
void fs_rdonly(const char *dir);
void fs_rdonly_rec(const char *dir);
// remount a directory noexec, nodev and nosuid
void fs_noexec(const char *dir);
void fs_noexec_rec(const char *dir);
// mount /proc and /sys directories
void fs_proc_sys_dev_boot(void);
// build a basic read-only filesystem
@ -550,12 +552,16 @@ int invalid_sandbox(const pid_t pid);
// The return value points to a static area, and will be overwritten by subsequent calls.
// The function does an exit(1) if anything goes wrong.
typedef struct {
int mountid; // id of the mount
char *fsname; // the pathname of the directory in the filesystem which forms the root of this mount
char *dir; // mount destination
char *fstype; // filesystem type
} MountData;
MountData *get_last_mount(void);
// mountinfo.c
MountData *get_last_mount(void);
int get_mount_id(const char *path);
char **get_all_mounts(const int mountid, const char *path);
// fs_var.c
void fs_var_log(void); // mounting /var/log

View file

@ -29,12 +29,14 @@
#include <errno.h>
#include <fcntl.h>
#define MAX_BUF 4096
// check noblacklist statements not matched by a proper blacklist in disable-*.inc files
//#define TEST_NO_BLACKLIST_MATCHING
static void fs_rdwr(const char *dir);
static void fs_rdwr_rec(const char *dir);
@ -147,21 +149,15 @@ static void disable_file(OPERATION op, const char *filename) {
}
}
else if (op == MOUNT_READONLY) {
if (arg_debug)
printf("Mounting read-only %s\n", fname);
fs_rdonly(fname);
fs_rdonly_rec(fname);
// todo: last_disable = SUCCESSFUL;
}
else if (op == MOUNT_RDWR) {
if (arg_debug)
printf("Mounting read-only %s\n", fname);
fs_rdwr(fname);
fs_rdwr_rec(fname);
// todo: last_disable = SUCCESSFUL;
}
else if (op == MOUNT_NOEXEC) {
if (arg_debug)
printf("Mounting noexec %s\n", fname);
fs_noexec(fname);
fs_noexec_rec(fname);
// todo: last_disable = SUCCESSFUL;
}
else if (op == MOUNT_TMPFS) {
@ -457,9 +453,10 @@ static int get_mount_flags(const char *path, unsigned long *flags) {
//***********************************************
// mount namespace
// - functions need fully resolved paths
//***********************************************
// remount a directory read-only
// remount directory read-only
void fs_rdonly(const char *dir) {
assert(dir);
// check directory exists
@ -471,77 +468,138 @@ void fs_rdonly(const char *dir) {
if ((flags & MS_RDONLY) == MS_RDONLY)
return;
flags |= MS_RDONLY;
if (arg_debug)
printf("Mounting read-only %s\n", dir);
// mount --bind /bin /bin
// mount --bind -o remount,ro /bin
if (mount(dir, dir, NULL, MS_BIND|MS_REC, NULL) < 0 ||
mount(NULL, dir, NULL, flags|MS_BIND|MS_REMOUNT|MS_REC, NULL) < 0)
mount(NULL, dir, NULL, flags|MS_BIND|MS_REMOUNT, NULL) < 0)
errExit("mount read-only");
fs_logger2("read-only", dir);
}
}
static void fs_rdwr(const char *dir) {
// remount directory read-only recursively
void fs_rdonly_rec(const char *dir) {
assert(dir);
// check directory exists and ensure we have a resolved path
// the resolved path allows to run a sanity check after the mount
char *path = realpath(dir, NULL);
if (path == NULL)
return;
// allow only user owned directories, except the user is root
uid_t u = getuid();
struct stat s;
int rv = stat(path, &s);
if (rv) {
free(path);
EUID_USER();
// get mount point of the directory
int mountid = get_mount_id(dir);
if (mountid == 0)
return;
// build array with all mount points that need to get remounted
char **arr = get_all_mounts(mountid, dir);
assert(arr);
// remount
EUID_ROOT();
char **tmp = arr;
while (*tmp) {
fs_rdonly(*tmp);
free(*tmp++);
}
if (u != 0 && s.st_uid != u) {
fwarning("you are not allowed to change %s to read-write\n", path);
free(path);
return;
}
// mount --bind /bin /bin
// mount --bind -o remount,rw /bin
unsigned long flags = 0;
get_mount_flags(path, &flags);
if ((flags & MS_RDONLY) == 0) {
free(path);
return;
}
flags &= ~MS_RDONLY;
if (mount(path, path, NULL, MS_BIND|MS_REC, NULL) < 0 ||
mount(NULL, path, NULL, flags|MS_BIND|MS_REMOUNT|MS_REC, NULL) < 0)
errExit("mount read-write");
fs_logger2("read-write", path);
// run a check on /proc/self/mountinfo to validate the mount
MountData *mptr = get_last_mount();
if (strncmp(mptr->dir, path, strlen(path)) != 0)
errLogExit("invalid read-write mount");
free(path);
free(arr);
}
// remount directory read-write
static void fs_rdwr(const char *dir) {
assert(dir);
// check directory exists
struct stat s;
int rv = stat(dir, &s);
if (rv == 0) {
// allow only user owned directories, except the user is root
uid_t u = getuid();
if (u != 0 && s.st_uid != u) {
fwarning("you are not allowed to change %s to read-write\n", dir);
return;
}
unsigned long flags = 0;
get_mount_flags(dir, &flags);
if ((flags & MS_RDONLY) == 0)
return;
flags &= ~MS_RDONLY;
if (arg_debug)
printf("Mounting read-write %s\n", dir);
// mount --bind /bin /bin
// mount --bind -o remount,rw /bin
if (mount(dir, dir, NULL, MS_BIND|MS_REC, NULL) < 0 ||
mount(NULL, dir, NULL, flags|MS_BIND|MS_REMOUNT, NULL) < 0)
errExit("mount read-write");
fs_logger2("read-write", dir);
// run a sanity check on /proc/self/mountinfo
MountData *mptr = get_last_mount();
size_t len = strlen(dir);
if (strncmp(mptr->dir, dir, len) != 0 ||
(*(mptr->dir + len) != '\0' && *(mptr->dir + len) != '/'))
errLogExit("invalid read-write mount");
}
}
// remount directory read-write recursively
static void fs_rdwr_rec(const char *dir) {
assert(dir);
EUID_USER();
// get mount point of the directory
int mountid = get_mount_id(dir);
if (mountid == 0)
return;
// build array with all mount points that need to get remounted
char **arr = get_all_mounts(mountid, dir);
assert(arr);
// remount
EUID_ROOT();
char **tmp = arr;
while (*tmp) {
fs_rdwr(*tmp);
free(*tmp++);
}
free(arr);
}
// remount directory noexec, nodev, nosuid
void fs_noexec(const char *dir) {
assert(dir);
// check directory exists
struct stat s;
int rv = stat(dir, &s);
if (rv == 0) {
// mount --bind /bin /bin
// mount --bind -o remount,ro /bin
unsigned long flags = 0;
get_mount_flags(dir, &flags);
if ((flags & (MS_NOEXEC|MS_NODEV|MS_NOSUID)) == (MS_NOEXEC|MS_NODEV|MS_NOSUID))
return;
flags |= MS_NOEXEC|MS_NODEV|MS_NOSUID;
if (arg_debug)
printf("Mounting noexec %s\n", dir);
// mount --bind /bin /bin
// mount --bind -o remount,noexec /bin
if (mount(dir, dir, NULL, MS_BIND|MS_REC, NULL) < 0 ||
mount(NULL, dir, NULL, flags|MS_BIND|MS_REMOUNT|MS_REC, NULL) < 0)
mount(NULL, dir, NULL, flags|MS_BIND|MS_REMOUNT, NULL) < 0)
errExit("mount noexec");
fs_logger2("noexec", dir);
}
}
// remount directory noexec, nodev, nosuid recursively
void fs_noexec_rec(const char *dir) {
assert(dir);
EUID_USER();
// get mount point of the directory
int mountid = get_mount_id(dir);
if (mountid == 0)
return;
// build array with all mount points that need to get remounted
char **arr = get_all_mounts(mountid, dir);
assert(arr);
// remount
EUID_ROOT();
char **tmp = arr;
while (*tmp) {
fs_noexec(*tmp);
free(*tmp++);
}
free(arr);
}
// Disable /mnt, /media, /run/mount and /run/media access
void fs_mnt(const int enforce) {
if (enforce) {

View file

@ -19,16 +19,19 @@
*/
#include "firejail.h"
#include <fcntl.h>
#define MAX_BUF 4096
static char mbuf[MAX_BUF];
static MountData mdata;
// Convert octal escape sequence to decimal value
static int read_oct(const char *path) {
int decimal = 0;
int digit, i;
// there are always three octal digits
// there are always exactly three octal digits
for (i = 1; i < 4; i++) {
digit = *(path + i);
if (digit < '0' || digit > '7') {
@ -61,43 +64,38 @@ static void unmangle_path(char *path) {
}
}
// Get info regarding the last kernel mount operation.
// The return value points to a static area, and will be overwritten by subsequent calls.
// The function does an exit(1) if anything goes wrong.
MountData *get_last_mount(void) {
// open /proc/self/mountinfo
FILE *fp = fopen("/proc/self/mountinfo", "r");
if (!fp)
goto errexit;
mbuf[0] = '\0';
while (fgets(mbuf, MAX_BUF, fp));
fclose(fp);
if (arg_debug)
printf("%s", mbuf);
// extract filesystem name, directory and filesystem type
// Parse a line from /proc/self/mountinfo,
// the function does an exit(1) if anything goes wrong.
static void parse_line(char *line, MountData *output) {
assert(line && *line);
memset(output, 0, sizeof(*output));
// extract filesystem name, directory and filesystem types
// examples:
// 587 543 8:1 /tmp /etc rw,relatime master:1 - ext4 /dev/sda1 rw,errors=remount-ro,data=ordered
// mdata.fsname: /tmp
// mdata.dir: /etc
// mdata.fstype: ext4
// output.mountid: 587
// output.fsname: /tmp
// output.dir: /etc
// output.fstype: ext4
// 585 564 0:76 / /home/netblue/.cache rw,nosuid,nodev - tmpfs tmpfs rw
// mdata.fsname: /
// mdata.dir: /home/netblue/.cache
// mdata.fstype: tmpfs
memset(&mdata, 0, sizeof(mdata));
char *ptr = strtok(mbuf, " ");
// output.mountid: 585
// output.fsname: /
// output.dir: /home/netblue/.cache
// output.fstype: tmpfs
char *ptr = strtok(line, " ");
if (!ptr)
goto errexit;
if (ptr != line)
goto errexit;
output->mountid = atoi(ptr);
int cnt = 1;
while ((ptr = strtok(NULL, " ")) != NULL) {
cnt++;
if (cnt == 4)
mdata.fsname = ptr;
output->fsname = ptr;
else if (cnt == 5) {
mdata.dir = ptr;
output->dir = ptr;
break;
}
}
@ -109,21 +107,156 @@ MountData *get_last_mount(void) {
ptr = strtok(NULL, " ");
if (!ptr)
goto errexit;
mdata.fstype = ptr++;
output->fstype = ptr++;
if (mdata.fsname == NULL ||
mdata.dir == NULL ||
mdata.fstype == NULL)
if (output->mountid == 0 ||
output->fsname == NULL ||
output->dir == NULL ||
output->fstype == NULL)
goto errexit;
unmangle_path(mdata.fsname);
unmangle_path(mdata.dir);
// restore empty spaces
unmangle_path(output->fsname);
unmangle_path(output->dir);
if (arg_debug)
printf("fsname=%s dir=%s fstype=%s\n", mdata.fsname, mdata.dir, mdata.fstype);
return &mdata;
return;
errexit:
fprintf(stderr, "Error: cannot read /proc/self/mountinfo\n");
exit(1);
}
// The return value points to a static area, and will be overwritten by subsequent calls.
MountData *get_last_mount(void) {
// open /proc/self/mountinfo
FILE *fp = fopen("/proc/self/mountinfo", "re");
if (!fp) {
perror("fopen");
fprintf(stderr, "Error: cannot read /proc/self/mountinfo\n");
exit(1);
}
mbuf[0] = '\0';
// go to the last line
while (fgets(mbuf, MAX_BUF, fp));
fclose(fp);
if (arg_debug)
printf("%s", mbuf);
parse_line(mbuf, &mdata);
if (arg_debug)
printf("mountid=%d fsname=%s dir=%s fstype=%s\n", mdata.mountid, mdata.fsname, mdata.dir, mdata.fstype);
return &mdata;
}
// Extract the mount id from /proc/self/fdinfo and return it.
int get_mount_id(const char *path) {
EUID_ASSERT();
int fd = open(path, O_PATH|O_CLOEXEC);
if (fd == -1)
return 0;
char *fdinfo;
if (asprintf(&fdinfo, "/proc/self/fdinfo/%d", fd) == -1)
errExit("asprintf");
EUID_ROOT();
FILE *fp = fopen(fdinfo, "re");
EUID_USER();
if (!fp)
goto errexit;
// go to the last line
char buf[MAX_BUF];
while (fgets(buf, MAX_BUF, fp));
fclose(fp);
close(fd);
// go to the mount id
if (strncmp(buf, "mnt_id:", 7) != 0)
goto errexit;
char *ptr = buf + 7;
while (*ptr != '\0' && (*ptr == ' ' || *ptr == '\t')) {
ptr++;
}
if (*ptr == '\0')
goto errexit;
free(fdinfo);
return atoi(ptr);
errexit:
fprintf(stderr, "Error: cannot read file in /proc/self/fdinfo\n");
exit(1);
}
// Return array with all paths that might need a remount.
char **get_all_mounts(const int mountid, const char *path) {
// open /proc/self/mountinfo
FILE *fp = fopen("/proc/self/mountinfo", "re");
if (!fp) {
perror("fopen");
fprintf(stderr, "Error: cannot read /proc/self/mountinfo\n");
exit(1);
}
size_t size = 32;
size_t cnt = 0;
char **rv = malloc(size * sizeof(*rv));
if (!rv)
errExit("malloc");
// read /proc/self/mountinfo
size_t pathlen = strlen(path);
int found = 0;
while (fgets(mbuf, MAX_BUF, fp)) {
// find mount point with mount id
if (!found) {
parse_line(mbuf, &mdata);
if (mdata.mountid == mountid) {
// don't remount blacklisted paths,
// give up if mount id has been reassigned
if (strstr(mdata.fsname, "firejail.ro.dir") ||
strstr(mdata.fsname, "firejail.ro.file") ||
strncmp(mdata.dir, path, strlen(mdata.dir)))
break;
*rv = strdup(path);
if (*rv == NULL)
errExit("strdup");
cnt++;
found = 1;
continue;
}
else
continue;
}
// from here on add all mount points below path
parse_line(mbuf, &mdata);
if (strncmp(mdata.dir, path, pathlen) == 0 &&
mdata.dir[pathlen] == '/' &&
strstr(mdata.fsname, "firejail.ro.dir") == NULL &&
strstr(mdata.fsname, "firejail.ro.file") == NULL) {
if (cnt >= size) {
size *= 2;
rv = realloc(rv, size * sizeof(*rv));
if (!rv)
errExit("realloc");
}
rv[cnt] = strdup(mdata.dir);
if (!rv[cnt])
errExit("strdup");
cnt++;
}
}
if (cnt == size) {
size++;
rv = realloc(rv, size * sizeof(*rv));
if (!rv)
errExit("realloc");
}
rv[cnt] = NULL; // end of the array
fclose(fp);
return rv;
}

View file

@ -20,6 +20,7 @@
#include "firejail.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sys/mount.h>
#include <dirent.h>
#include <sys/wait.h>
@ -82,10 +83,8 @@ void pulseaudio_init(void) {
// create the new user pulseaudio directory
if (mkdir(RUN_PULSE_DIR, 0700) == -1)
errExit("mkdir");
// make it a mount point and add mount flags
if (mount(RUN_PULSE_DIR, RUN_PULSE_DIR, NULL, MS_BIND, NULL) < 0 ||
mount(NULL, RUN_PULSE_DIR, NULL, MS_NOEXEC|MS_NODEV|MS_NOSUID|MS_BIND|MS_REMOUNT, NULL) < 0)
errExit("mount RUN_PULSE_DIR");
// mount it nosuid, noexec, nodev
fs_noexec(RUN_PULSE_DIR);
// create the new client.conf file
char *pulsecfg = NULL;
@ -189,7 +188,12 @@ void pulseaudio_init(void) {
// confirm the actual mount destination is owned by the user
if (fstat(fd, &s) == -1 || s.st_uid != getuid())
errExit("fstat");
// preserve a read-only mount
struct statvfs vfs;
if (fstatvfs(fd, &vfs) == -1)
errExit("fstatvfs");
if ((vfs.f_flag & MS_RDONLY) == MS_RDONLY)
fs_rdonly(RUN_PULSE_DIR);
// mount via the link in /proc/self/fd
char *proc;
if (asprintf(&proc, "/proc/self/fd/%d", fd) == -1)

View file

@ -20,6 +20,7 @@
#include "firejail.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
@ -1163,6 +1164,9 @@ void x11_xorg(void) {
unlink(tmpfname);
umount("/tmp");
// remount RUN_XAUTHORITY_SEC_FILE noexec, nodev, nosuid
fs_noexec(RUN_XAUTHORITY_SEC_FILE);
// Ensure there is already a file in the usual location, so that bind-mount below will work.
char *dest;
if (asprintf(&dest, "%s/.Xauthority", cfg.homedir) == -1)
@ -1184,6 +1188,12 @@ void x11_xorg(void) {
fprintf(stderr, "Error: .Xauthority is not a user owned regular file\n");
exit(1);
}
// preserve a read-only mount
struct statvfs vfs;
if (fstatvfs(fd, &vfs) == -1)
errExit("fstatvfs");
if ((vfs.f_flag & MS_RDONLY) == MS_RDONLY)
fs_rdonly(RUN_XAUTHORITY_SEC_FILE);
// mount via the link in /proc/self/fd
char *proc;