fix(ci): stop image-build /dev bind from wiping host /dev

A killed ubuntu system-image build can leave `mount --bind /dev` under
its build dir. The e2e Clean Workspace step then ran
`sudo rm -rf "$GITHUB_WORKSPACE"/*` as root, traversed the leaked bind,
and deleted the host's /dev (incl. /dev/null), wedging the runner.

- e2e.yml: umount anything under the workspace (deepest-first) before
  deleting, and pass `rm --one-file-system` so rm can never cross a mount
  boundary into host /dev. Hard backstop, applied to all 3 Clean Workspace
  steps.
- build-system-image.sh: recursive lazy unmount in cleanup() detaches the
  chroot binds even when busy, and mark the build subtree rprivate so the
  /dev bind can't propagate into the host namespace.
This commit is contained in:
joshsiv-mulga
2026-06-05 22:52:49 +10:00
parent f9c08130ba
commit da26b1db69
2 changed files with 38 additions and 9 deletions
+27 -3
View File
@@ -70,7 +70,15 @@ jobs:
true
- name: Clean Workspace
run: sudo rm -rf "$GITHUB_WORKSPACE"/*
run: |
# Detach any leaked mounts under the workspace before deleting. A
# killed image build can leave a `mount --bind /dev` under its build
# dir; if `rm -rf` traverses it as root it wipes the host's /dev
# (deletes /dev/null and wedges the box). umount deepest-first, then
# forbid rm from crossing any mount boundary as a hard backstop.
awk -v w="$GITHUB_WORKSPACE" '$2 ~ "^"w {print $2}' /proc/mounts \
| sort -r | xargs -r -I{} sudo umount -l -R {} || true
sudo rm -rf --one-file-system "$GITHUB_WORKSPACE"/*
- uses: actions/create-github-app-token@v3
id: app-token
@@ -385,7 +393,15 @@ jobs:
true
- name: Clean Workspace
run: sudo rm -rf "$GITHUB_WORKSPACE"/*
run: |
# Detach any leaked mounts under the workspace before deleting. A
# killed image build can leave a `mount --bind /dev` under its build
# dir; if `rm -rf` traverses it as root it wipes the host's /dev
# (deletes /dev/null and wedges the box). umount deepest-first, then
# forbid rm from crossing any mount boundary as a hard backstop.
awk -v w="$GITHUB_WORKSPACE" '$2 ~ "^"w {print $2}' /proc/mounts \
| sort -r | xargs -r -I{} sudo umount -l -R {} || true
sudo rm -rf --one-file-system "$GITHUB_WORKSPACE"/*
- uses: actions/create-github-app-token@v3
id: app-token
@@ -793,7 +809,15 @@ jobs:
true
- name: Clean Workspace
run: sudo rm -rf "$GITHUB_WORKSPACE"/*
run: |
# Detach any leaked mounts under the workspace before deleting. A
# killed image build can leave a `mount --bind /dev` under its build
# dir; if `rm -rf` traverses it as root it wipes the host's /dev
# (deletes /dev/null and wedges the box). umount deepest-first, then
# forbid rm from crossing any mount boundary as a hard backstop.
awk -v w="$GITHUB_WORKSPACE" '$2 ~ "^"w {print $2}' /proc/mounts \
| sort -r | xargs -r -I{} sudo umount -l -R {} || true
sudo rm -rf --one-file-system "$GITHUB_WORKSPACE"/*
- uses: actions/create-github-app-token@v3
id: app-token
+11 -6
View File
@@ -98,12 +98,13 @@ esac
cleanup() {
echo "Cleaning up..."
# Ubuntu chroot has bind mounts that must be released first
sudo umount "${MOUNT_DIR}/dev/pts" 2>/dev/null || true
sudo umount "${MOUNT_DIR}/dev" 2>/dev/null || true
sudo umount "${MOUNT_DIR}/sys" 2>/dev/null || true
sudo umount "${MOUNT_DIR}/proc" 2>/dev/null || true
sudo umount "${MOUNT_DIR}" 2>/dev/null || true
# Recursive lazy unmount detaches the chroot binds (/dev, /dev/pts, /sys,
# /proc) and the root mount from the path hierarchy at once, even if a
# straggler still holds one busy. This MUST succeed: a leaked /dev bind left
# under the build dir would be traversed by a later `rm -rf` and wipe the
# host's /dev (deletes /dev/null, wedges the box). -R recurses children,
# -l detaches busy mounts immediately so the path no longer crosses /dev.
sudo umount -R -l "${MOUNT_DIR}" 2>/dev/null || true
sudo qemu-nbd --disconnect "${NBD_DEV}" 2>/dev/null || true
exec 9>&- 2>/dev/null || true # release nbd lock if held
echo "Done."
@@ -288,6 +289,10 @@ fi
# Step 4: Mount and customize
echo "Mounting root filesystem..."
sudo mount "${ROOT_PART}" "$MOUNT_DIR"
# Mark the build-dir subtree private so the chroot's /dev bind below cannot
# propagate into the host mount namespace (a propagated bind is what turned a
# leaked mount into a host /dev wipe on the CI runners).
sudo mount --make-rprivate "$MOUNT_DIR" 2>/dev/null || true
# Set up resolv.conf for DNS inside chroot.
# Ubuntu cloud images symlink /etc/resolv.conf → /run/systemd/resolve/stub-resolv.conf