google / google/gvisor

runsc: bind mounts are invisible inside the sandbox when root.path is "/" (regression since 2023 releases; any other bind of / as root works)

Open
#14,470 1 comment 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

With a bundle whose `root.path` is literally `/` (the host root, read-only) and a plain bind mount in `mounts`, the current release mounts the bind (the gofer logs `Mounting src: "/srv/src", dst: "/home"`) but inside the sandbox the destination shows the **host's real directory** at that path, not the bind source. Any other `root.path` — including a plain `mount --bind / /srv/rootfs` of the same host root — makes the same bundle work. A 2023 release (`0.0~20230807.0`, the version Debian/Ubuntu apt currently carry for arm64) works with `root.path: "/"` too.

### Steps to reproduce

```sh
mkdir -p /srv/src && echo hello-from-source > /srv/src/y.txt && chown -R 10001:10001 /srv/src
mkdir /srv/b && cd /srv/b && runsc spec -- /bin/sh -c 'ls -lan /home | head -3; cat /home/y.txt; mount | grep home'
python3 - <<'PY'
import json
c=json.load(open('config.json'))
c['root']={'path':'/','readonly':True}
c['mounts'].append({'destination':'/home','type':'bind','source':'/srv/src','options':['bind','rw']})
json.dump(c,open('config.json','w'),indent=1)
PY
runsc --platform=systrap --network=none --ignore-cgroups run t1
```

Observed with `release-20260817.0`:

```
total 13
drwxr-xr-x 3 0 0 4096 Aug 28 07:35 . <- the HOST's /home (its real contents were listed)
cat: /home/y.txt: No such file or directory
none on /home type 9p (rw,...,aname=/,...)
```

Now change only `root.path`:

```sh
mkdir -p /srv/rootfs2 && mount --bind / /srv/rootfs2 # non-recursive; --rbind behaves the same
# c['root']['path'] = '/srv/rootfs2'
runsc --platform=systrap --network=none --ignore-cgroups run t2
```

```
total 9
drwxr-xr-x 2 10001 10001 4096 Aug 28 07:36 .
hello-from-source
```

Writes through the working variant land on the host with the right ownership. The result is the same with `--overlay2=none` and with `--directfs=false`, and the same whether the destination exists on the host (`/home`) or not (`/state`). The gofer's debug log is identical in the failing and working cases apart from the `dst:` prefix.

| runsc | kernel / arch | `root.path` | bind mount inside |
| --- | --- | --- | --- |
| `0.0~20230807.0` | 6.8 arm64 | `/` | works |
| `release-20260817.0` | 6.8 arm64 | `/` | **empty / host dir** |
| `release-20260817.0` | 6.12 amd64 (Fly.io guest) | `/` | **empty / host dir** |
| `release-20260817.0` | 6.8 arm64 | `mount --rbind / /srv/rootfs` | works |
| `release-20260817.0` | 6.8 arm64 | `mount --bind / /srv/rootfs2` | works |

### Where it seems to be

`runsc/cmd/sandboxsetup/gofer_mount.go` `SetupRootFS`: the rootfs is self-bind-mounted recursively, each bind source is mounted at `Root.Path + Destination`, then `Root.Path` is bind-mounted recursively into `/proc/fs/root` before the pivot. When `Root.Path` is `/`, the self-bind is `mount --rbind / /` and the bind sources land on the host's own paths in the gofer's private namespace; the later recursive rebind into `/proc/fs/root` then appears not to carry those submounts, while a `Root.Path` that is any other mount point round-trips them. The 2023 gofer mounted binds straight into `/proc/root/` instead, which is why it is unaffected.

### runsc version

```
runsc version release-20260817.0
spec: 1.2.1
```

### docker version (if using docker)

Not using Docker: plain OCI bundle, `runsc run`.

### uname

`6.12.105-fly x86_64` (Fly.io Firecracker guest) and `6.8.0-117-generic aarch64` (Ubuntu 24.04 VM).

### repo

n/a

Contributor guide

Open the contributing guide

Research direction

Run the provided OCI bundle reproduction with runsc, comparing root.path "/" against the bind-mounted root variant. Then inspect runsc/cmd/sandboxsetup/gofer_mount.go, especially SetupRootFS and its mount sequence. Done means a bind at /home exposes /srv/src inside the sandbox when root.path is "/", while the existing working variants remain unaffected.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, linux
Domain
operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.