From aff7cb630c80248791273a2bec4bc87461e38a3e Mon Sep 17 00:00:00 2001 From: "Kelvin M. Klann" Date: Tue, 21 Apr 2026 06:45:11 -0300 Subject: [PATCH] feature: allow subpaths in xdg macros Currently it is not possible to use XDG-related macros (such as `${DOCUMENTS}`) with subpaths (such as `${DOCUMENTS}/foo`) and so profiles just use `${HOME}` with a hardcoded path using the English directory name and the subpath (such as `${HOME}/Documents/foo`). Allow using subpaths after XDG macros, so that they automatically use the auto-detected XDG path, just as when currently using the XDG macros without subpaths. Before: ${HOME}/Documents/foo After: ${DOCUMENTS}/foo This is a follow-up to #7147. Closes #2359. Relates to #4229. --- src/firejail/firejail.h | 6 +- src/firejail/fs_whitelist.c | 4 +- src/firejail/macros.c | 43 ++--- test/fs/fs.sh | 21 +++ test/fs/macro-subpath-blacklist.profile | 6 + test/fs/macro-subpath-readonly.profile | 6 + test/fs/macro-subpath-whitelist.profile | 6 + test/fs/macro-subpath.exp | 200 ++++++++++++++++++++++++ 8 files changed, 268 insertions(+), 24 deletions(-) create mode 100644 test/fs/macro-subpath-blacklist.profile create mode 100644 test/fs/macro-subpath-readonly.profile create mode 100644 test/fs/macro-subpath-whitelist.profile create mode 100644 test/fs/macro-subpath.exp diff --git a/src/firejail/firejail.h b/src/firejail/firejail.h index 4ad489209..332456b6b 100644 --- a/src/firejail/firejail.h +++ b/src/firejail/firejail.h @@ -542,10 +542,10 @@ uint32_t arp_assign(const char *dev, Bridge *br); // macros.c char *expand_macros(const char *path); -char *resolve_macro(const char *name); +char *resolve_macro(const char *path); void invalid_filename(const char *fname, int globbing); -int is_macro(const char *name); -int macro_id(const char *name); +int is_macro(const char *path); +int macro_id(const char *path); // util.c diff --git a/src/firejail/fs_whitelist.c b/src/firejail/fs_whitelist.c index 7ed190572..389b2aaeb 100644 --- a/src/firejail/fs_whitelist.c +++ b/src/firejail/fs_whitelist.c @@ -596,8 +596,8 @@ void fs_whitelist(void) { if (is_macro(expanded) && macro_id(expanded) > -1) { if (!nowhitelist_flag && (have_topdir(cfg.homedir, topdirs) || add_topdir(cfg.homedir, topdirs, expanded)) && !arg_quiet) { fprintf(stderr, "***\n"); - fprintf(stderr, "*** Warning: cannot whitelist %s directory\n", expanded); - fprintf(stderr, "*** Any file saved in this directory will be lost when the sandbox is closed.\n"); + fprintf(stderr, "*** Warning: cannot whitelist %s path\n", expanded); + fprintf(stderr, "*** Any file saved in this path will be lost when the sandbox is closed.\n"); fprintf(stderr, "***\n"); } entry = entry->next; diff --git a/src/firejail/macros.c b/src/firejail/macros.c index 23952aaaf..d3048ca02 100644 --- a/src/firejail/macros.c +++ b/src/firejail/macros.c @@ -71,10 +71,11 @@ Macro macro[] = { }; // return -1 if not found -int macro_id(const char *name) { +int macro_id(const char *path) { int i = 0; while (macro[i].name != NULL) { - if (strcmp(name, macro[i].name) == 0) + size_t len = strlen(macro[i].name); + if (strncmp(path, macro[i].name, len) == 0) return i; i++; } @@ -82,12 +83,12 @@ int macro_id(const char *name) { return -1; } -int is_macro(const char *name) { - assert(name); - int len = strlen(name); +int is_macro(const char *path) { + assert(path); + int len = strlen(path); if (len <= 4) return 0; - if (*name == '$' && name[1] == '{' && name[len - 1] == '}') + if (*path == '$' && path[1] == '{' && strchr(&path[2], '}')) return 1; return 0; } @@ -175,17 +176,25 @@ static char *resolve_hardcoded(char *entries[]) { } // returns mallocated memory -char *resolve_macro(const char *name) { +char *resolve_macro(const char *path) { char *rv = NULL; - int id = macro_id(name); + int id = macro_id(path); if (id == -1) return NULL; - rv = resolve_xdg(macro[id].xdg); - if (rv == NULL) - rv = resolve_hardcoded(macro[id].translation); + char *directory = resolve_xdg(macro[id].xdg); + if (!directory) + directory = resolve_hardcoded(macro[id].translation); + if (!directory) + return NULL; + + size_t len = strlen(macro[id].name); + if (asprintf(&rv, "%s/%s%s", cfg.homedir, directory, path + len) == -1) + errExit("asprintf"); + free(directory); + if (rv && arg_debug) - printf("Directory %s resolved as %s\n", name, rv); + printf("Path %s resolved as %s\n", path, rv); return rv; } @@ -235,13 +244,9 @@ char *expand_macros(const char *path) { goto out; } else { - char *directory = resolve_macro(path); - if (directory) { - if (asprintf(&rv, "%s/%s", cfg.homedir, directory) == -1) - errExit("asprintf"); - free(directory); + rv = resolve_macro(path); + if (rv) goto out; - } } assert(rv == NULL); @@ -269,7 +274,7 @@ void invalid_filename(const char *fname, int globbing) { else { int id = macro_id(fname); if (id != -1) - return; + ptr = fname + strlen(macro[id].name); } reject_meta_chars(ptr, globbing); diff --git a/test/fs/fs.sh b/test/fs/fs.sh index 1f32529e7..ae5c52445 100755 --- a/test/fs/fs.sh +++ b/test/fs/fs.sh @@ -105,6 +105,27 @@ rm -f ~/Music/_firejail_test_file rm -f ~/Pictures/_firejail_test_file rm -f ~/Videos/_firejail_test_file +mkdir -p ~/Desktop/_firejail_test_dir/a +mkdir -p ~/Desktop/_firejail_test_dir/b +mkdir -p ~/Documents/_firejail_test_dir/a +mkdir -p ~/Documents/_firejail_test_dir/b +mkdir -p ~/Downloads/_firejail_test_dir/a +mkdir -p ~/Downloads/_firejail_test_dir/b +mkdir -p ~/Music/_firejail_test_dir/a +mkdir -p ~/Music/_firejail_test_dir/b +mkdir -p ~/Pictures/_firejail_test_dir/a +mkdir -p ~/Pictures/_firejail_test_dir/b +mkdir -p ~/Videos/_firejail_test_dir/a +mkdir -p ~/Videos/_firejail_test_dir/b +echo "TESTING: macro subpaths (test/fs/macro-subpath.exp)" +./macro-subpath.exp +rm -fr ~/Desktop/_firejail_test_dir +rm -fr ~/Documents/_firejail_test_dir +rm -fr ~/Downloads/_firejail_test_dir +rm -fr ~/Music/_firejail_test_dir +rm -fr ~/Pictures/_firejail_test_dir +rm -fr ~/Videos/_firejail_test_dir + echo "TESTING: whitelist empty (test/fs/whitelist-empty.exp)" ./whitelist-empty.exp diff --git a/test/fs/macro-subpath-blacklist.profile b/test/fs/macro-subpath-blacklist.profile new file mode 100644 index 000000000..548021c71 --- /dev/null +++ b/test/fs/macro-subpath-blacklist.profile @@ -0,0 +1,6 @@ +blacklist ${DESKTOP}/_firejail_test_dir +blacklist ${DOCUMENTS}/_firejail_test_dir +blacklist ${DOWNLOADS}/_firejail_test_dir +blacklist ${MUSIC}/_firejail_test_dir +blacklist ${PICTURES}/_firejail_test_dir +blacklist ${VIDEOS}/_firejail_test_dir diff --git a/test/fs/macro-subpath-readonly.profile b/test/fs/macro-subpath-readonly.profile new file mode 100644 index 000000000..c5a639236 --- /dev/null +++ b/test/fs/macro-subpath-readonly.profile @@ -0,0 +1,6 @@ +read-only ${DESKTOP}/_firejail_test_dir +read-only ${DOCUMENTS}/_firejail_test_dir +read-only ${DOWNLOADS}/_firejail_test_dir +read-only ${MUSIC}/_firejail_test_dir +read-only ${PICTURES}/_firejail_test_dir +read-only ${VIDEOS}/_firejail_test_dir diff --git a/test/fs/macro-subpath-whitelist.profile b/test/fs/macro-subpath-whitelist.profile new file mode 100644 index 000000000..9d39fe295 --- /dev/null +++ b/test/fs/macro-subpath-whitelist.profile @@ -0,0 +1,6 @@ +whitelist ${DESKTOP}/_firejail_test_dir/a +whitelist ${DOCUMENTS}/_firejail_test_dir/a +whitelist ${DOWNLOADS}/_firejail_test_dir/a +whitelist ${MUSIC}/_firejail_test_dir/a +whitelist ${PICTURES}/_firejail_test_dir/a +whitelist ${VIDEOS}/_firejail_test_dir/a diff --git a/test/fs/macro-subpath.exp b/test/fs/macro-subpath.exp new file mode 100644 index 000000000..b95263cd9 --- /dev/null +++ b/test/fs/macro-subpath.exp @@ -0,0 +1,200 @@ +#!/usr/bin/expect -f +# This file is part of Firejail project +# Copyright (C) 2014-2026 Firejail Authors +# License GPL v2 + +set timeout 3 +spawn $env(SHELL) +match_max 100000 + +# Test that macros work with subpaths (see #2359). +send -- "firejail --profile=./macro-subpath-whitelist.profile ls \ + ~/Desktop/_firejail_test_dir \ + ~/Documents/_firejail_test_dir \ + ~/Downloads/_firejail_test_dir \ + ~/Music/_firejail_test_dir \ + ~/Pictures/_firejail_test_dir \ + ~/Videos/_firejail_test_dir \ +\r" + +expect { + timeout {puts "TESTING ERROR 0\n";exit} + -re "Child process initialized in \[0-9\]+.\[0-9\]+ ms" +} +expect { + timeout {puts "TESTING ERROR 1\n";exit} + "Desktop/_firejail_test_dir/a" {} + "Desktop/_firejail_test_dir/b" {puts "TESTING ERROR 1.1\n";exit} +} +expect { + timeout {puts "TESTING ERROR 2\n";exit} + "Documents/_firejail_test_dir/a" {} + "Documents/_firejail_test_dir/b" {puts "TESTING ERROR 2.1\n";exit} +} +expect { + timeout {puts "TESTING ERROR 3\n";exit} + "Downloads/_firejail_test_dir/a" {} + "Downloads/_firejail_test_dir/b" {puts "TESTING ERROR 3.1\n";exit} +} +expect { + timeout {puts "TESTING ERROR 4\n";exit} + "Music/_firejail_test_dir/a" {} + "Music/_firejail_test_dir/b" {puts "TESTING ERROR 4.1\n";exit} +} +expect { + timeout {puts "TESTING ERROR 5\n";exit} + "Pictures/_firejail_test_dir/a" {} + "Pictures/_firejail_test_dir/b" {puts "TESTING ERROR 5.1\n";exit} +} +expect { + timeout {puts "TESTING ERROR 6\n";exit} + "Videos/_firejail_test_dir/a" {} + "Videos/_firejail_test_dir/b" {puts "TESTING ERROR 6.1\n";exit} +} +after 100 + +send -- "firejail --profile=./macro-subpath-blacklist.profile ls ~/Desktop/_firejail_test_dir; echo ret \$?\r" +expect { + timeout {puts "TESTING ERROR 7\n";exit} + -re "Child process initialized in \[0-9\]+.\[0-9\]+ ms" +} +expect { + timeout {puts "TESTING ERROR 8\n";exit} + "Permission denied" {} + -re {ret 0} {puts "TESTING ERROR 8.1\n";exit} +} +after 100 + +send -- "firejail --profile=./macro-subpath-blacklist.profile ls ~/Documents/_firejail_test_dir; echo ret \$?\r" +expect { + timeout {puts "TESTING ERROR 9\n";exit} + -re "Child process initialized in \[0-9\]+.\[0-9\]+ ms" +} +expect { + timeout {puts "TESTING ERROR 10\n";exit} + "Permission denied" {} + -re {ret 0} {puts "TESTING ERROR 10.1\n";exit} +} +after 100 + +send -- "firejail --profile=./macro-subpath-blacklist.profile ls ~/Downloads/_firejail_test_dir; echo ret \$?\r" +expect { + timeout {puts "TESTING ERROR 11\n";exit} + -re "Child process initialized in \[0-9\]+.\[0-9\]+ ms" +} +expect { + timeout {puts "TESTING ERROR 12\n";exit} + "Permission denied" {} + -re {ret 0} {puts "TESTING ERROR 12.1\n";exit} +} +after 100 + +send -- "firejail --profile=./macro-subpath-blacklist.profile ls ~/Music/_firejail_test_dir; echo ret \$?\r" +expect { + timeout {puts "TESTING ERROR 13\n";exit} + -re "Child process initialized in \[0-9\]+.\[0-9\]+ ms" +} +expect { + timeout {puts "TESTING ERROR 14\n";exit} + "Permission denied" {} + -re {ret 0} {puts "TESTING ERROR 14.1\n";exit} +} +after 100 + +send -- "firejail --profile=./macro-subpath-blacklist.profile ls ~/Pictures/_firejail_test_dir; echo ret \$?\r" +expect { + timeout {puts "TESTING ERROR 15\n";exit} + -re "Child process initialized in \[0-9\]+.\[0-9\]+ ms" +} +expect { + timeout {puts "TESTING ERROR 16\n";exit} + "Permission denied" {} + -re {ret 0} {puts "TESTING ERROR 16.1\n";exit} +} +after 100 + +send -- "firejail --profile=./macro-subpath-blacklist.profile ls ~/Videos/_firejail_test_dir; echo ret \$?\r" +expect { + timeout {puts "TESTING ERROR 17\n";exit} + -re "Child process initialized in \[0-9\]+.\[0-9\]+ ms" +} +expect { + timeout {puts "TESTING ERROR 18\n";exit} + "Permission denied" {} + -re {ret 0} {puts "TESTING ERROR 18.1\n";exit} +} +after 100 + +send -- "firejail --profile=./macro-subpath-readonly.profile touch ~/Desktop/_firejail_test_dir/_firejail_test_file; echo ret \$?\r" +expect { + timeout {puts "TESTING ERROR 19\n";exit} + -re "Child process initialized in \[0-9\]+.\[0-9\]+ ms" +} +expect { + timeout {puts "TESTING ERROR 20\n";exit} + "Read-only file system" {} + -re {ret 0} {puts "TESTING ERROR 20.1\n";exit} +} +after 100 + +send -- "firejail --profile=./macro-subpath-readonly.profile touch ~/Documents/_firejail_test_dir/_firejail_test_file; echo ret \$?\r" +expect { + timeout {puts "TESTING ERROR 21\n";exit} + -re "Child process initialized in \[0-9\]+.\[0-9\]+ ms" +} +expect { + timeout {puts "TESTING ERROR 22\n";exit} + "Read-only file system" {} + -re {ret 0} {puts "TESTING ERROR 22.1\n";exit} +} +after 100 + +send -- "firejail --profile=./macro-subpath-readonly.profile touch ~/Downloads/_firejail_test_dir/_firejail_test_file; echo ret \$?\r" +expect { + timeout {puts "TESTING ERROR 23\n";exit} + -re "Child process initialized in \[0-9\]+.\[0-9\]+ ms" +} +expect { + timeout {puts "TESTING ERROR 24\n";exit} + "Read-only file system" {} + -re {ret 0} {puts "TESTING ERROR 24.1\n";exit} +} +after 100 + +send -- "firejail --profile=./macro-subpath-readonly.profile touch ~/Music/_firejail_test_dir/_firejail_test_file; echo ret \$?\r" +expect { + timeout {puts "TESTING ERROR 25\n";exit} + -re "Child process initialized in \[0-9\]+.\[0-9\]+ ms" +} +expect { + timeout {puts "TESTING ERROR 26\n";exit} + "Read-only file system" {} + -re {ret 0} {puts "TESTING ERROR 26.1\n";exit} +} +after 100 + +send -- "firejail --profile=./macro-subpath-readonly.profile touch ~/Pictures/_firejail_test_dir/_firejail_test_file; echo ret \$?\r" +expect { + timeout {puts "TESTING ERROR 27\n";exit} + -re "Child process initialized in \[0-9\]+.\[0-9\]+ ms" +} +expect { + timeout {puts "TESTING ERROR 28\n";exit} + "Read-only file system" {} + -re {ret 0} {puts "TESTING ERROR 28.1\n";exit} +} +after 100 + +send -- "firejail --profile=./macro-subpath-readonly.profile touch ~/Videos/_firejail_test_dir/_firejail_test_file; echo ret \$?\r" +expect { + timeout {puts "TESTING ERROR 29\n";exit} + -re "Child process initialized in \[0-9\]+.\[0-9\]+ ms" +} +expect { + timeout {puts "TESTING ERROR 30\n";exit} + "Read-only file system" {} + -re {ret 0} {puts "TESTING ERROR 30.1\n";exit} +} +after 100 + +puts "\nall done\n"