Skip to content

gofer, overlay: Omit whiteouts that Linux overlayfs cannot look up - #17663

Open
copybara-service[bot] wants to merge 1 commit into
masterfrom
test/cl1021732572
Open

copybara-service[bot] wants to merge 1 commit into
masterfrom
test/cl1021732572

Conversation

@copybara-service

Copy link
Copy Markdown

gofer, overlay: Omit whiteouts that Linux overlayfs cannot look up

Summary

Since #16469 made --inode-numbers=passthrough the default, a directory on a
Linux overlayfs on the host can look empty in the sandbox if overlayfs does
not merge it and it contains a whiteout. getdents64(2) on it fails with
ENOENT, which glibc's readdir(3) treats as the end of the directory.
rmdir(2) and rename(2) of the directory fail the same way.

Linux overlayfs reads an unmerged directory straight from its layer
(ovl_iterate()), so the listing includes whiteouts, even though
ovl_lookup() treats them as missing. Native Linux shows the same listing.
Before #16469, the gofer called stat(2) on every entry and dropped the ones
that failed, which hid these whiteouts from the overlay. It no longer does.
The overlay then calls stat(2) on each DT_CHR entry of the lower layer to
check whether it is a whiteout (getDirentsLocked(),
collectWhiteoutsForRmdirLocked()), gets ENOENT, and fails the whole call.

Images built with Docker running inside gVisor before 6d278f8 have a lot of
these directories. In those versions, the overlay's RenameAt() left a
whiteout at the source of every rename, and Docker's native overlay diff put
it into the layer. So every temporary file that a build step renamed (git's
*.lock and tmp_pack_*, dpkg's *.dpkg-new, and so on) became a .wh.
entry, and containerd's overlayfs snapshotter turns each one into a 0:0
character device. With these images, git can't find the packs in
.git/objects/pack, and git cat-file -e <sha>^{commit} fails with
fatal: Not a valid object name. The same images run fine on runc, and ran
fine on gVisor before #16469, so this is a regression
(COMPATIBILITY:REGRESSION-GVISOR).

This PR makes the overlay skip entries that a layer lists but can't look up,
like Linux's ovl_check_whiteouts(), instead of failing getdents64(2) and
rmdir(2). As with lookup, such an entry doesn't hide a file with the same
name on a lower layer. Mounts without an overlay keep the host's listing, as
on Linux.

Reproduction

As root, create a Linux overlayfs on the host in which dir and onlywo
exist in one layer only and contain a whiteout:

mkdir -p $W/lower/dir $W/lower/onlywo $W/upper $W/work $W/merged
touch $W/lower/dir/file
mknod $W/lower/dir/whiteout c 0 0
mknod $W/lower/onlywo/whiteout c 0 0
mount -t overlay overlay -o lowerdir=$W/lower,upperdir=$W/upper,workdir=$W/work $W/merged
runsc --root=$W/root --network=none do sh -c "$CMD"

Linux (sh -c "$CMD" on the host):

file
whiteout
ls: 0
rmdir: 0

runsc before this PR:

ls: 0
rmdir: failed to remove '/tmp/wo/merged/onlywo': No such file or directory
rmdir: 1

ls prints nothing. With --strace:

ls X getdents64(0x3 /tmp/wo/merged/dir, 0x55bde68b45b0, 0x8000) = -1 errno=2 (no such file or directory) (45.177µs)

runsc with this PR:

file
ls: 0
rmdir: 0

Without the overlay (runsc do --force-overlay=false), runsc lists file and
whiteout, like Linux, both before and after this PR.

A real image shows the same thing. Build this Dockerfile with Docker running
inside gVisor at release-20261019.0 or earlier (DOCKER_BUILDKIT=0), and
import it into containerd:

FROM alpine:latest
RUN mkdir /data && echo a > /data/conf && sed -i s/a/b/ /data/conf
RUN mkdir /scratch && echo a > /scratch/x && sed -i s/a/b/ /scratch/x && rm /scratch/x

The two RUN layers end up with data/.wh.confJPIHhN and
scratch/.wh.xGlngLk, left over from the temporary files of sed -i. Then run
ls /data; cat /data/conf; rmdir /scratch in the image. runc lists conf and
removes /scratch. runsc before this PR lists nothing in /data, still reads
/data/conf, and fails rmdir /scratch with ENOENT.

Testing

  • The new container test TestHostOverlayfsWhiteouts lists such a directory
    with and without directfs, and with and without the overlay
    (--overlay2=all:memory). With the overlay it expects only file; without
    it, the host's file and whiteout. On master it fails in both overlay
    configurations, where ls prints nothing. The test has to mknod(2) a
    whiteout and mount overlayfs, and skips if it can't.
  • The existing getdents, rename, mkdir and unlink syscall tests pass
    with the overlay (_runsc_systrap_overlay).
    Auto-fixed-by: [🤖] @EtiennePerot
    FUTURE_COPYBARA_INTEGRATE_REVIEW=gofer, overlay: Omit whiteouts that Linux overlayfs cannot look up #17090 from tanyifeng:fix-overlayfs-whiteouts 977845d

@copybara-service copybara-service Bot added the exported Issue was exported automatically label Nov 14, 2026
@copybara-service
copybara-service Bot force-pushed the test/cl1021732572 branch 3 times, most recently from 7b8b8e5 to fad87ef Compare November 15, 2026 14:43
## Summary

Since #16469 made `--inode-numbers=passthrough` the default, a directory on a
Linux overlayfs on the host can look empty in the sandbox if overlayfs does
not merge it and it contains a whiteout. `getdents64(2)` on it fails with
`ENOENT`, which glibc's `readdir(3)` treats as the end of the directory.
`rmdir(2)` and `rename(2)` of the directory fail the same way.

Linux overlayfs reads an unmerged directory straight from its layer
(`ovl_iterate()`), so the listing includes whiteouts, even though
`ovl_lookup()` treats them as missing. Native Linux shows the same listing.
Before #16469, the gofer called `stat(2)` on every entry and dropped the ones
that failed, which hid these whiteouts from the overlay. It no longer does.
The overlay then calls `stat(2)` on each `DT_CHR` entry of the lower layer to
check whether it is a whiteout (`getDirentsLocked()`,
`collectWhiteoutsForRmdirLocked()`), gets `ENOENT`, and fails the whole call.

Images built with Docker running inside gVisor before 6d278f8 have a lot of
these directories. In those versions, the overlay's `RenameAt()` left a
whiteout at the source of every rename, and Docker's native overlay diff put
it into the layer. So every temporary file that a build step renamed (git's
`*.lock` and `tmp_pack_*`, dpkg's `*.dpkg-new`, and so on) became a `.wh.`
entry, and containerd's overlayfs snapshotter turns each one into a `0:0`
character device. With these images, git can't find the packs in
`.git/objects/pack`, and `git cat-file -e <sha>^{commit}` fails with
`fatal: Not a valid object name`. The same images run fine on runc, and ran
fine on gVisor before #16469, so this is a regression
(`COMPATIBILITY:REGRESSION-GVISOR`).

This PR makes the overlay skip entries that a layer lists but can't look up,
like Linux's `ovl_check_whiteouts()`, instead of failing `getdents64(2)` and
`rmdir(2)`. As with lookup, such an entry doesn't hide a file with the same
name on a lower layer. Mounts without an overlay keep the host's listing, as
on Linux.

## Reproduction

As root, create a Linux overlayfs on the host in which `dir` and `onlywo`
exist in one layer only and contain a whiteout:

```
mkdir -p $W/lower/dir $W/lower/onlywo $W/upper $W/work $W/merged
touch $W/lower/dir/file
mknod $W/lower/dir/whiteout c 0 0
mknod $W/lower/onlywo/whiteout c 0 0
mount -t overlay overlay -o lowerdir=$W/lower,upperdir=$W/upper,workdir=$W/work $W/merged
runsc --root=$W/root --network=none do sh -c "$CMD"
```

Linux (`sh -c "$CMD"` on the host):

```
file
whiteout
ls: 0
rmdir: 0
```

runsc before this PR:

```
ls: 0
rmdir: failed to remove '/tmp/wo/merged/onlywo': No such file or directory
rmdir: 1
```

`ls` prints nothing. With `--strace`:

```
ls X getdents64(0x3 /tmp/wo/merged/dir, 0x55bde68b45b0, 0x8000) = -1 errno=2 (no such file or directory) (45.177µs)
```

runsc with this PR:

```
file
ls: 0
rmdir: 0
```

Without the overlay (`runsc do --force-overlay=false`), runsc lists `file` and
`whiteout`, like Linux, both before and after this PR.

A real image shows the same thing. Build this Dockerfile with Docker running
inside gVisor at release-20261019.0 or earlier (`DOCKER_BUILDKIT=0`), and
import it into containerd:

```
FROM alpine:latest
RUN mkdir /data && echo a > /data/conf && sed -i s/a/b/ /data/conf
RUN mkdir /scratch && echo a > /scratch/x && sed -i s/a/b/ /scratch/x && rm /scratch/x
```

The two `RUN` layers end up with `data/.wh.confJPIHhN` and
`scratch/.wh.xGlngLk`, left over from the temporary files of `sed -i`. Then run
`ls /data; cat /data/conf; rmdir /scratch` in the image. runc lists `conf` and
removes `/scratch`. runsc before this PR lists nothing in `/data`, still reads
`/data/conf`, and fails `rmdir /scratch` with `ENOENT`.

## Testing

- The new container test `TestHostOverlayfsWhiteouts` lists such a directory
  with and without directfs, and with and without the overlay
  (`--overlay2=all:memory`). With the overlay it expects only `file`; without
  it, the host's `file` and `whiteout`. On master it fails in both overlay
  configurations, where `ls` prints nothing. The test has to `mknod(2)` a
  whiteout and mount overlayfs, and skips if it can't.
- The existing `getdents`, `rename`, `mkdir` and `unlink` syscall tests pass
  with the overlay (`_runsc_systrap_overlay`).
Auto-fixed-by: [🤖] @EtiennePerot
FUTURE_COPYBARA_INTEGRATE_REVIEW=#17090 from tanyifeng:fix-overlayfs-whiteouts 977845d
PiperOrigin-RevId: 1021732572
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

exported Issue was exported automatically

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant