Skip to content

bwrap fails inside a distrobox/podman container once the host rewrites /etc/resolv.conf (mount root becomes //deleted) #219

Description

@FeliciousX

Environment

  • fence v0.1.65 (installed via mise go install, so fence --version reports dev); code path still present on main
  • Host: Bluefin (Fedora Atomic desktop), systemd-resolved
  • Sandbox host env: distrobox container (podman-5.8.4, quay.io/fedora/fedora:43), rootless
  • Kernel 7.0.12-201.fc44.x86_64

Symptom

Every fence invocation fails at sandbox setup:

$ fence -- true
bwrap: Can't bind mount /oldroot/etc/resolv.conf on /newroot/etc/resolv.conf: Unable to mount source on destination: No such file or directory

The catch is that it doesn't fail immediately after the container starts — it starts failing after the host's /etc/resolv.conf changes (VPN connect/disconnect, Wi-Fi switch, suspend/resume, DHCP renewal). Once it starts, every subsequent fence run in that container fails until the container is restarted.

Root cause

Inside a distrobox/podman container, /etc/resolv.conf is a bind mount of the host's /run/systemd/resolve/stub-resolv.conf, not a regular file:

$ grep resolv /proc/self/mountinfo
3419 2692 0:29 /systemd/resolve/stub-resolv.conf//deleted /etc/resolv.conf ro,nosuid,nodev master:1462 - tmpfs tmpfs rw,seclabel,...
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

systemd-resolved rewrites stub-resolv.conf atomically (write temp file + rename) whenever the DNS configuration changes. That unlinks the inode the container's bind mount was created from. The mount stays alive pointing at the now-unlinked inode, and the kernel marks it by appending //deleted to the mount root field in mountinfo. The file still stats fine, but with zero links:

$ stat -L /etc/resolv.conf
  File: /etc/resolv.conf
  Size: 948        Blocks: 8          IO Block: 4096   regular file
Device: 0,29    Inode: 5202        Links: 0
                                   ^^^^^^^^^

fence then trips over this in appendLinuxReadableHostSubmounts:

for _, root := range linuxReadableHostSubmountRoots() {
    if !fileExists(root) || sameDevice("/", root) {
        continue
    }
    bwrapArgs = append(bwrapArgs, "--ro-bind", root, root)

/etc/resolv.conf qualifies as a host submount to preserve — linuxShouldPreserveHostSubmount only excludes /dev, /proc, /sys, /run and /tmp, and /etc is not on that list — and it survives both guards: fileExists succeeds (the unlinked inode still stats), and sameDevice("/", ...) is false because the mount is on the host's /run tmpfs (0:29) rather than the container's root device. So fence emits --ro-bind /etc/resolv.conf /etc/resolv.conf, and bwrap fails with ENOENT because the source mount's root dentry no longer exists.

parseLinuxMountInfoLine only keeps fields[4] (mount point) and the fs type, so the //deleted marker in fields[3] (mount root) is discarded before anything can act on it:

return linuxMountInfoEntry{
    MountPoint: linuxUnescapeMountInfoPath(fields[4]),
    FSType:     fields[separator+1],
}, true

Note this is a different failure from #30 (WSL, /etc/resolv.conf a dangling symlink across a mount boundary) — here it's not a symlink at all, and the resolv.conf handling at linux.go:1164 is skipped entirely. It's the same shape as #195 (/opt -> /var/opt on Silverblue): an entry in a mount list that can't actually be bound.

Workaround

Restart the distrobox container so podman re-creates the bind mount from the current inode:

$ distrobox stop <name> && distrobox enter <name>

This works until the host's DNS configuration changes again.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions