Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: create the link only if its endpoint is available #6504

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/firejail/firejail.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#define DEFAULT_ROOT_PROFILE "server"
#define MAX_INCLUDE_LEVEL 16 // include levels in profile files

#define MAX_LINKS_LEVEL 5 // maximum link depth

#define ASSERT_PERMS(file, uid, gid, mode) \
do { \
Expand Down Expand Up @@ -162,6 +163,13 @@ typedef struct landlock_entry_t {
char *data;
} LandlockEntry;

typedef struct delayed_link_entry_t {
struct delayed_link_entry_t *next;
char link_filenames[MAX_LINKS_LEVEL][PATH_MAX]; // contains all links from initial file to final one
char dst[PATH_MAX];
int size; // size of link_filenames
} DelayedLinkEntry;

typedef struct config_t {
// user data
char *username;
Expand All @@ -172,6 +180,7 @@ typedef struct config_t {
ProfileEntry *profile;
ProfileEntry *profile_rebuild_etc; // blacklist files in /etc directory used by fs_rebuild_etc()
LandlockEntry *lprofile;
DelayedLinkEntry *delayed_links; // delay links until we can copy them correctly

#define MAX_PROFILE_IGNORE 32
char *profile_ignore[MAX_PROFILE_IGNORE];
Expand Down Expand Up @@ -495,6 +504,10 @@ char *profile_list_normalize(char *list);
char *profile_list_compress(char *list);
void profile_list_augment(char **list, const char *items);

void fs_create_delayed_links();
void remove_blacklisted_delayed_links(const char *blacklist_path, bool can_whitelist_save);
void delayed_links_add(char *src, char *dst);

// list.c
void list(void);
void tree(void);
Expand Down Expand Up @@ -614,6 +627,8 @@ int ascii_isupper(unsigned char c);
int ascii_isxdigit(unsigned char c);
int invalid_name(const char *name);
void check_homedir(const char *dir);
void normalize_link_path(const char *link_src, const char *link_dst, char *res);
bool dir_contains(const char *directory, const char *item);

// Get info regarding the last kernel mount operation from /proc/self/mountinfo
// The return value points to a static area, and will be overwritten by subsequent calls.
Expand Down
2 changes: 2 additions & 0 deletions src/firejail/fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ static void disable_file(OPERATION op, const char *filename) {

// modify the file
if (op == BLACKLIST_FILE || op == BLACKLIST_NOLOG) {
remove_blacklisted_delayed_links(fname, false);

// some distros put all executables under /usr/bin and make /bin a symbolic link
if ((strcmp(fname, "/bin") == 0 || strcmp(fname, "/usr/bin") == 0) &&
is_link(filename) &&
Expand Down
29 changes: 22 additions & 7 deletions src/firejail/fs_etc.c
Original file line number Diff line number Diff line change
Expand Up @@ -285,19 +285,34 @@ static void duplicate(const char *fname, const char *private_dir, const char *pr

build_dirs(src, dst, strlen(private_dir), strlen(private_run_dir));

// follow links by default, thus making a copy of the file or directory pointed by the symlink
// this will solve problems such as NixOS #4887
//
// don't follow links to dynamic directories such as /proc
if (strcmp(src, "/etc/mtab") == 0)
sbox_run(SBOX_ROOT | SBOX_SECCOMP, 3, PATH_FCOPY, src, dst);
else
// Delay link creation until we are sure that the final link file
// will (or will not) be available in the sandbox. If the final link file is not available,
// it will not be created. A warning will be written.
if (is_link(src)) {
if (arg_debug)
printf("Delay creating link %s in the sandbox\n", src);
delayed_links_add(src, dst);
} else
sbox_run(SBOX_ROOT | SBOX_SECCOMP, 4, PATH_FCOPY, "--follow-link", src, dst);

free(dst);
fs_logger2("clone", src);
}

void fs_create_delayed_links() {
DelayedLinkEntry *ptr = cfg.delayed_links;
DelayedLinkEntry *tmp = NULL;

while (ptr) {
if (arg_debug)
printf("Create delayed link %s\n", ptr->link_filenames[0]);
sbox_run(SBOX_ROOT | SBOX_SECCOMP, 3, PATH_FCOPY, ptr->link_filenames[0], ptr->dst);
tmp = ptr;
ptr = ptr->next;
free(tmp);
}
}

static void duplicate_globbing(const char *fname, const char *private_dir, const char *private_run_dir) {
assert(fname);

Expand Down
5 changes: 5 additions & 0 deletions src/firejail/fs_whitelist.c
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,11 @@ static void tmpfs_topdirs(const TopDir * const topdirs) {
continue;
}

// Check whether bind mount of tmpfs on topdirs[i].path will affect delayed links
// Bind mount of tmpfs on topdirs[i].path has the same meaning that blacklisting topdirs[i].path)
// exсept that whitelisted entries will be restored inside topdirs[i].path later.
remove_blacklisted_delayed_links(topdirs[i].path, true);

// special case /run
// open /run/firejail, so it can be restored right after mounting the tmpfs
int fd = -1;
Expand Down
116 changes: 116 additions & 0 deletions src/firejail/profile.c
Original file line number Diff line number Diff line change
Expand Up @@ -1759,6 +1759,122 @@ int profile_check_line(char *ptr, int lineno, const char *fname) {
return 1;
}

bool does_whitelist_entry_save_file(const char *file, const char *whitelisted_file) {
bool is_whitelisted = false;
if (!is_dir(whitelisted_file))
is_whitelisted = strcmp(whitelisted_file, file) == 0;
else
is_whitelisted = dir_contains(whitelisted_file, file);

return is_whitelisted;
}

bool does_whitelist_save_file(const char *file) {
ProfileEntry *entry = cfg.profile;
bool is_whitelisted = false;

while (entry) {
if (entry->wparam) {
char *wfile = entry->wparam->file;
char *wlink = entry->wparam->link;

is_whitelisted = does_whitelist_entry_save_file(file, wfile);
if (!is_whitelisted && wlink) {
is_whitelisted = does_whitelist_entry_save_file(file, wlink);
}

if (is_whitelisted)
return true;
}

entry = entry->next;
}

return false;
}

bool is_link_blacklisted(DelayedLinkEntry *link, const char *rblacklist_path, bool can_whitelist_save) {
for (int i = 0; i < link->size; i++) {
bool is_blacklisted = false;

if (!is_dir(rblacklist_path))
is_blacklisted = strcmp(link->link_filenames[i], rblacklist_path) == 0;
else
is_blacklisted = dir_contains(rblacklist_path, link->link_filenames[i]);

if (is_blacklisted) {
// used to check whether whitelist will save link
if (!can_whitelist_save || can_whitelist_save && !does_whitelist_save_file(link->link_filenames[i]))
return true;
}
}

return false;
}

void remove_blacklisted_delayed_links(const char *blacklist_path, bool can_whitelist_save) {
char *rblacklist_path = realpath(blacklist_path, NULL);
if (rblacklist_path == NULL)
errExit("realpath");

DelayedLinkEntry *prev = NULL;
DelayedLinkEntry *ptr = cfg.delayed_links;

while (ptr) {
if (is_link_blacklisted(ptr, rblacklist_path, can_whitelist_save)) {

fprintf(stderr, "***\n");
fprintf(stderr, "*** Warning: cannot create delayed link %s\n", ptr->link_filenames[0]);
fprintf(stderr, "*** One of the intermediate links or the final file blacklisted by path %s\n", rblacklist_path);
fprintf(stderr, "***\n");

// Remove link from the delayed links
if (prev)
prev->next = ptr->next;
else
cfg.delayed_links = ptr->next;

DelayedLinkEntry *tmp = ptr;
ptr = ptr->next;
free(tmp);
} else {
prev = ptr;
ptr = ptr->next;
}
}
}

// Accept normalized absolute path
void delayed_links_add(char *src, char *dst) {
char link_point[PATH_MAX];
DelayedLinkEntry *link = malloc(sizeof(DelayedLinkEntry));

if (link == NULL)
errExit("malloc");

link->size = 0;
char *link_src = link->link_filenames[link->size++];
strncpy(link_src, src, PATH_MAX);
link_src[PATH_MAX - 1] = '\0';
strncpy(link->dst, dst, PATH_MAX);
link->dst[PATH_MAX - 1] = '\0';

char *cur_absolute = src;
size_t link_point_len = 0;

while ((link_point_len = readlink(cur_absolute, link_point, PATH_MAX)) != (size_t)-1) {
if (link->size == MAX_LINKS_LEVEL)
errExit("too many intermediate links");

link_point[link_point_len] = '\0';
normalize_link_path(cur_absolute, link_point, link->link_filenames[link->size]);
cur_absolute = link->link_filenames[link->size++];
}

link->next = cfg.delayed_links;
cfg.delayed_links = link;
}

// add a profile entry in cfg.profile list; use str to populate the list
void profile_add(char *str) {
EUID_ASSERT();
Expand Down
29 changes: 19 additions & 10 deletions src/firejail/sandbox.c
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,7 @@ int sandbox(void* sandbox_arg) {
fs_mnt(0);

// Install new /etc last, so we can use it as long as possible
float time_private_etc = 0;
if (arg_private_etc) {
if (cfg.chrootdir)
fwarning("private-etc feature is disabled in chroot\n");
Expand All @@ -1051,17 +1052,8 @@ int sandbox(void* sandbox_arg) {
if (umount2("/etc/passwd", MNT_DETACH) == -1)
fprintf(stderr, "/etc/passwd: unmount: %s\n", strerror(errno));

fs_private_dir_mount("/etc", RUN_ETC_DIR);
fmessage("Private /etc installed in %0.2f ms\n", timetrace_end());
time_private_etc += timetrace_end();

// create /etc/ld.so.preload file again
if (need_preload)
fs_trace_touch_preload();

// openSUSE configuration is split between /etc and /usr/etc
// process private-etc a second time
if (access("/usr/etc", F_OK) == 0)
fs_private_dir_list("/usr/etc", RUN_USR_ETC_DIR, cfg.etc_private_keep);
}
}

Expand All @@ -1076,6 +1068,23 @@ int sandbox(void* sandbox_arg) {
fs_blacklist(); // mkdir and mkfile are processed all over again
EUID_ROOT();

if (arg_private_etc && !cfg.chrootdir && !arg_overlay) {
timetrace_start();
// create delayed links from etc
fs_create_delayed_links();
fs_private_dir_mount("/etc", RUN_ETC_DIR);
fmessage("Private /etc installed in %0.2f ms\n", timetrace_end());

// create /etc/ld.so.preload file again
if (need_preload)
fs_trace_touch_preload();

// openSUSE configuration is split between /etc and /usr/etc
// process private-etc a second time
if (access("/usr/etc", F_OK) == 0)
fs_private_dir_list("/usr/etc", RUN_USR_ETC_DIR, cfg.etc_private_keep);
}

//****************************
// nosound/no3d/notv/novideo and fix for pulseaudio 7.0
//****************************
Expand Down
79 changes: 79 additions & 0 deletions src/firejail/util.c
Original file line number Diff line number Diff line change
Expand Up @@ -1541,3 +1541,82 @@ void check_homedir(const char *dir) {
fmessage("No full support for symbolic links in path of user directory.\n"
"Please provide resolved path in password database (/etc/passwd).\n\n");
}

/*
* Normalize link destination path according to link source path
*
* Examples:
* Input: /usr/share/timezone/localtime, Europe/Moscow, ...
* Output: /usr/share/timezone/Europe/Moscow
*
* Input: /usr/share/timezone/localtime, ../../../../////usr/share//././timezone/Europe/Moscow, ...
* Output: /usr/share/timezone/Europe/Moscow
*
* Input: /usr/share/timezone/localtime, /some/other/location/./Europe/Moscow, ...
* Output: /some/other/location/Europe/Moscow
*/
void normalize_link_path(const char *link_src, const char *link_dst, char *res)
{
size_t res_len = 0;
size_t link_dst_len = strlen(link_dst);
const char *ptr = link_dst;
const char *end = &link_dst[link_dst_len];
const char *next;

if (link_dst[0] != '/') {
// relative path
size_t link_src_len = strlen(link_src);
if (!is_dir(link_src)) {
char *slash = strrchr(link_src, '/');
if (slash != NULL)
link_src_len = slash - link_src;
}

memcpy(res, link_src, link_src_len);
res_len = link_src_len;
}

for (ptr = link_dst; ptr < end; ptr = next + 1) {
size_t len;
next = memchr(ptr, '/', end - ptr);
if (next == NULL)
next = end;
len = next - ptr;

switch (len) {
case 2:
if (ptr[0] == '.' && ptr[1] == '.') {
const char *slash = memrchr(res, '/', res_len);
if (slash != NULL)
res_len = slash - res;
continue;
}
break;
case 1:
if (ptr[0] == '.')
continue;
break;
case 0:
continue;
}
res[res_len++] = '/';
memcpy(&res[res_len], ptr, len);
res_len += len;
}

if (res_len == 0)
res[res_len++] = '/';

res[res_len] = '\0';
}

// Checks if the path is a subpath
// example: /some/path /some/path/some/subpath
bool dir_contains(const char *directory, const char *item) {
char tmp[PATH_MAX];
strcpy(tmp, directory);
size_t tmp_len = strlen(tmp);
tmp[tmp_len++] = '/';
tmp[tmp_len] = '\0';
return strncmp(tmp, item, tmp_len) == 0;
}
Loading