modif: do not follow symlinks to /dev/null on disable (#7129)

When trying to prevent a file or directory in the user home from being
written to, it is not uncommon to replace it with a symlink to
/dev/null.

If this path is also blacklisted (such as by disable-common.inc), the
symlink will be followed, resulting in /dev/null itself being
blacklisted, which can cause issues with (unrelated) programs that have
their output redirected to /dev/null (for example).

To avoid disabling /dev/null, when applying commands from
`disable_file()` (such as `blacklist` and `read-only`), if a file is a
symlink to /dev/null, avoid following the symlink and perform the
operation on the link itself instead.

Using these commands with "/dev/null" directly as the argument (that is,
without going through a symlink) should still work the same way.

It has been confirmed to work on Linux 3.8[1], so it should work on at
least 3.8 and later.

Closes #5803.

[1] https://github.com/netblue30/firejail/pull/7129#issuecomment-4233141574

Reported-by: @fgpietersz
Suggested-by: @Changaco
Tested-by: @Changaco
Tested-by: @Zopolis4
This commit is contained in:
Kelvin M. Klann 2026-04-17 18:30:28 +00:00 committed by GitHub
parent a7a66c5e6e
commit a4e6495fd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 101 additions and 10 deletions

View file

@ -60,11 +60,11 @@ static int disable_file(OPERATION op, const char *filename) {
EUID_ASSERT(); EUID_ASSERT();
// Resolve all symlinks // Resolve all symlinks
char* fname = realpath(filename, NULL); char* rpath = realpath(filename, NULL);
if (fname == NULL && errno != EACCES) { if (rpath == NULL && errno != EACCES) {
return 1; return 1;
} }
if (fname == NULL && errno == EACCES) { if (rpath == NULL && errno == EACCES) {
// realpath and stat functions will fail on FUSE filesystems // realpath and stat functions will fail on FUSE filesystems
// they don't seem to like a uid of 0 // they don't seem to like a uid of 0
// force mounting // force mounting
@ -95,22 +95,38 @@ static int disable_file(OPERATION op, const char *filename) {
} }
} }
assert(fname); assert(rpath);
// check for firejail executable // check for firejail executable
// we might have a file found in ${PATH} pointing to /usr/bin/firejail // we might have a file found in ${PATH} pointing to /usr/bin/firejail
// blacklisting it here will end up breaking situations like user clicks on a link in Thunderbird // blacklisting it here will end up breaking situations like user clicks on a link in Thunderbird
// and expects Firefox to open in the same sandbox // and expects Firefox to open in the same sandbox
if (strcmp(BINDIR "/firejail", fname) == 0) { if (strcmp(BINDIR "/firejail", rpath) == 0) {
free(fname); free(rpath);
return 1; return 1;
} }
int follow_symlinks = 1;
// Users may try to disable writing to certain paths in the user home
// by replacing them with symlinks to /dev/null, so avoid following
// symlinks to /dev/null to prevent disabling /dev/null itself, as that
// can lead to unexpected issues (see #5803).
if (strcmp(rpath, "/dev/null") == 0 && is_link(filename))
follow_symlinks = 0;
int extraflags = 0;
const char *fname = rpath; // filename to be used for operations
if (!follow_symlinks) {
fname = filename;
extraflags |= O_NOFOLLOW;
}
// if the file is not present, do nothing // if the file is not present, do nothing
int fd = open(fname, O_PATH|O_CLOEXEC); int fd = open(fname, O_PATH|O_CLOEXEC|extraflags);
if (fd < 0) { if (fd < 0) {
if (arg_debug) if (arg_debug)
printf("Warning (blacklisting): cannot open %s: %s\n", fname, strerror(errno)); printf("Warning (blacklisting): cannot open %s: %s\n", fname, strerror(errno));
free(fname); free(rpath);
return 1; return 1;
} }
@ -118,7 +134,7 @@ static int disable_file(OPERATION op, const char *filename) {
if (fstat(fd, &s) < 0) { if (fstat(fd, &s) < 0) {
if (arg_debug) if (arg_debug)
printf("Warning (blacklisting): cannot stat %s: %s\n", fname, strerror(errno)); printf("Warning (blacklisting): cannot stat %s: %s\n", fname, strerror(errno));
free(fname); free(rpath);
close(fd); close(fd);
return 1; return 1;
} }
@ -214,7 +230,7 @@ static int disable_file(OPERATION op, const char *filename) {
out: out:
close(fd); close(fd);
free(fname); free(rpath);
return retval; return retval;
} }

View file

@ -266,6 +266,13 @@ Symbolic link handling: Blacklisting a path that is a symbolic link will also
blacklist the path that it points to. blacklist the path that it points to.
For example, if ~/foo is blacklisted and it points to /bar, then /bar will also For example, if ~/foo is blacklisted and it points to /bar, then /bar will also
be blacklisted. be blacklisted.
Symlinks to /dev/null are not followed (only the symlink itself will be
affected), as replacing certain paths with symlinks to /dev/null is not
uncommon and restricting access to /dev/null can lead to unexpected issues (see
.UR https://github.com/netblue30/firejail/issues/5803
#5803
.UE ).
Using /dev/null directly (instead of through a symlink) should work normally.
.br .br
.br .br

View file

@ -247,6 +247,13 @@ Symbolic link handling: Blacklisting a path that is a symbolic link will also
blacklist the path that it points to. blacklist the path that it points to.
For example, if ~/foo is blacklisted and it points to /bar, then /bar will also For example, if ~/foo is blacklisted and it points to /bar, then /bar will also
be blacklisted. be blacklisted.
Symlinks to /dev/null are not followed (only the symlink itself will be
affected), as replacing certain paths with symlinks to /dev/null is not
uncommon and restricting access to /dev/null can lead to unexpected issues (see
.UR https://github.com/netblue30/firejail/issues/5803
#5803
.UE ).
Using /dev/null directly (instead of through a symlink) should work normally.
.br .br
.br .br

1
test/fs/devnull_symlink Symbolic link
View file

@ -0,0 +1 @@
/dev/null

View file

@ -0,0 +1,57 @@
#!/usr/bin/expect -f
# This file is part of Firejail project
# Copyright (C) 2014-2026 Firejail Authors
# License GPL v2
set timeout 2
spawn $env(SHELL)
match_max 100000
set PWD $env(PWD)
send -- "ls -l /dev/null\r"
expect {
timeout {puts "TESTING ERROR 0\n";exit}
"crw-rw-rw-" {puts "OK\n"}
}
after 100
# Test that disabling a symlink to /dev/null does not disable /dev/null itself
# (see #5803).
send -- "firejail --noprofile --blacklist=./devnull_symlink ls -l /dev/null\r"
expect {
timeout {puts "TESTING ERROR 1\n";exit}
"crw-rw-rw-" {puts "OK\n"}
"cr--r--r--" {puts "TESTING ERROR 2\n";exit}
}
after 100
send -- "firejail --noprofile --blacklist=./devnull_symlink touch /dev/null; echo ret \$?\r"
expect {
timeout {puts "TESTING ERROR 3\n";exit}
-re {ret 0} {puts "OK\n"}
-re {ret [1-9]} {puts "TESTING ERROR 4\n";exit}
}
after 100
# Test that /dev/null can still be disabled directly (without a symlink).
send -- "firejail --noprofile --blacklist=/dev/null ls -l /dev/null\r"
expect {
timeout {puts "TESTING ERROR 5\n";exit}
"\\-r--------" {puts "OK\n"}
"crw-rw-rw-" {puts "TESTING ERROR 6\n";exit}
}
after 100
send -- "firejail --noprofile --blacklist=/dev/null touch /dev/null; echo ret \$?\r"
expect {
timeout {puts "TESTING ERROR 7\n";exit}
-re {ret 0} {puts "TESTING ERROR 8\n";exit}
-re {ret [1-9]} {puts "OK\n"}
}
after 100
send -- "exit\r"
after 100
puts "\nall done\n"

View file

@ -125,6 +125,9 @@ echo "TESTING: blacklist glob (test/fs/option_blacklist_glob.exp)"
./option_blacklist_glob.exp ./option_blacklist_glob.exp
rm -fr ~/_firejail_test_dir rm -fr ~/_firejail_test_dir
echo "TESTING: disable symlink to /dev/null (test/fs/disable-devnull-symlink.exp)"
./disable-devnull-symlink.exp
echo "TESTING: noblacklist blacklist noexec (test/fs/noblacklist-blacklist-noexec.exp)" echo "TESTING: noblacklist blacklist noexec (test/fs/noblacklist-blacklist-noexec.exp)"
./noblacklist-blacklist-noexec.exp ./noblacklist-blacklist-noexec.exp