google / google/gvisor

devpts: mode= and gid= are applied to the mount root instead of replica devices

Open
#14,761 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.