devpts: mode= and gid= are applied to the mount root instead of replica devices
- Dominant language
- Go
- Stars
- 19.3k
- Forks
- 2k
- Avg merge
- 3d 5h
- Merged PRs (30d)
- 264
Description
### Description
`pkg/sentry/fsimpl/devpts/devpts.go` applies the `mode=` mount option to the root directory of the devpts mount. Linux applies it to replica (slave) devices and keeps the mount root at 0755.
Expected: a non-root process in a container can open `/dev/ptmx`.
Observed: `open("/dev/ptmx")` returns `EACCES`. The process cannot `stat` `/dev/pts/ptmx` either.
Container runtimes mount devpts with `nosuid,noexec,newinstance,ptmxmode=0666,mode=0620,gid=5` per the OCI specification. gVisor therefore sets `/dev/pts` itself to 0620, which gives other users no search bit, so they cannot traverse the directory to reach `ptmx`. The `ptmx` node is already 0666 and is not the obstacle.
Both of the following ran Docker Engine 29.7.2 from the same `docker:29.7.2-dind` image, with the same `docker run --rm --user 1000:1000 alpine:3.22`. The runtime is the only difference.
runc:
```
inner: version=29.7.2 runtime=runc
drwxr-xr-x 2 root root 0 /dev/pts
crw-rw-rw- 1 root root 5, 2 /dev/pts/ptmx
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666)
open /dev/ptmx: OK
```
gVisor (GKE Sandbox):
```
drw--w---- 2 root tty 0 /dev/pts
crw-rw-rw- 1 root tty 5, 2 /dev/pts/ptmx
none on /dev/pts type devpts (rw,noexec,nosuid)
(cannot stat /dev/pts/ptmx)
open /dev/ptmx: FAILED -- Permission denied
```
Running the same command as root succeeds under both, which isolates the failure to directory traversal rather than to the device. 0620 root:tty is exactly the `mode=620` and `gid=5` visible in the runc mount line, so the options reach gVisor and are applied to the wrong inode.
### Where the two implementations differ
Linux, `fs/devpts/inode.c`:
- The root inode is fixed, line 379: `inode->i_mode = S_IFDIR | S_IRUGO | S_IXUGO | S_IWUSR` (0755).
- A replica takes the option, line 518: `init_special_inode(inode, S_IFCHR|opts->mode, MKDEV(UNIX98_PTY_SLAVE_MAJOR, index))`.
gVisor, `pkg/sentry/fsimpl/devpts/devpts.go`:
- The root takes the option, line 185: `root.InodeAttrs.InitWithIDs(ctx, opts.uid, opts.gid, linux.UNNAMED_MAJOR, devMinor, 1, linux.ModeDirectory|opts.mode)`
- A replica ignores it and is hardcoded, line 281: `replica.InodeAttrs.Init(ctx, creds, i.InodeAttrs.DevMajor(), i.InodeAttrs.DevMinor(), uint64(idx+3), linux.ModeCharacterDevice|0600)`
So there are two deviations, not one. Replicas also miss `opts.gid`: they are created with `creds`, while Linux gives them `opts->gid`, which is what lets the `tty` group write to another user's terminal.
`ptmx` already follows the spec, line 197, which is why only the directory blocks access: `master.InodeAttrs.InitWithIDs(..., linux.ModeCharacterDevice|opts.ptmxMode)`.
The option handling arrived in 639488b65a18 ("fsimpl/devpts: handle mode, ptmxmode, uid, gid mount options", 2023-10-24). It was adapted from tmpfs, where `mode=` does set the root directory. Six warning strings still name tmpfs, for example line 76: `ctx.Warningf("tmpfs.FilesystemType.GetFilesystem: invalid mode: %q", modeStr)`.
Worth noting the default is unaffected: lines 67 and 68 set `mode: 0555` and `ptmxMode: 0666`, so a plain `mount -t devpts` leaves the directory traversable. The failure appears only when standard container options are supplied.
### Steps to reproduce
Any container runtime on gVisor. No Java or buildpacks involved.
```sh
docker run --rm --user 1000:1000 alpine:3.22 sh -c '
ls -ld /dev/pts
ls -l /dev/pts/ptmx
if (exec 3<>/dev/ptmx); then echo "open OK"; else echo "open FAILED"; fi
'
```
Under runc this prints `open OK`. Under gVisor it prints `open FAILED` and cannot stat the node.
### Possible solution
Keep the devpts root at 0755, and apply `mode=` and `gid=` when a replica is created in `allocateTerminal`, matching `fs/devpts/inode.c`. Both changes are needed: moving `mode` alone would still leave replicas owned by the creating credentials instead of the `tty` group.
`TEST(BasicPtyTest, SetMode)` in `test/syscalls/linux/pty.cc` encodes the current behaviour. It mounts `newinstance,mode=0600,ptmxmode=0620` and asserts `EXPECT_EQ(st.st_mode, 0600 | S_IFDIR)` on the directory, so it would need updating alongside the fix. The test begins with `SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)))`, and mounting devpts requires that capability, so it is skipped wherever it is absent.
### Impact
Any workload that allocates a PTY as a non-root user fails. Cloud Native Buildpacks cannot build images under gVisor: the CNB lifecycle runs as uid 1000, and libpak, which every Paketo buildpack uses to run subprocesses, always allocates a PTY (paketo-buildpacks/libpak#468). `pack build` and Spring Boot's `bootBuildImage` both fail with `unable to start PTY / open /dev/ptmx: permission denied`.
### runsc version
GKE Sandbox manages runsc. The node does not expose its version.
### docker version (if using docker)
Docker Engine 29.7.2 on both sides. In the sandbox it runs as a Docker-in-Docker sidecar started with `--iptables=false --ip6tables=false --data-root=/d` and a tmpfs at the data root. The runc comparison ran the same `docker:29.7.2-dind` image on a GitHub-hosted runner.
### uname
`/proc/version` reports `Linux version 4.19.0-gvisor`.
### kubectl (if using Kubernetes)
GKE 1.36.3-gke.1537000. Node pool: `imageType COS_CONTAINERD`, `sandboxConfig.type GVISOR`.
Contributor guide
Research direction
Start in pkg/sentry/fsimpl/devpts/devpts.go, especially option parsing, root initialization, and allocateTerminal; compare them with the Linux inode.c behavior described in the issue. Update the behavior so the root and replicas receive the expected ownership and modes, then revise and run TEST(BasicPtyTest, SetMode) in test/syscalls/linux/pty.cc; done means non-root PTY allocation succeeds with standard container mount options.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, go, linux
- Domain
- operating-systems
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100