Skip to content

Partial host-mount unmounts in getBlockVolumes are silently discarded on error, permanently orphaning already-unmounted block volumes #859

Description

@Anand-240

Description

When a container's OCI spec declares more than one bind mount backed by a loop device for a filesystem the guest supports (e.g. two ext2 image files bind-mounted from the host), getBlockVolumes in pkg/unikontainers/block.go iterates the mounts and, for each supported one, clears the loop-device autoclear flag and calls mount.Unmount(mInfo.MountPoint) to detach it from the host so it can be handed to the guest as a block device.

If any iteration after the first fails (getMountInfo erroring on a later entry, setLoopAutoclear failing, or mount.Unmount failing), the function immediately does return nil, err, discarding the entire blkImgs slice built so far. The volumes already unmounted in earlier loop iterations are never returned to the caller and never recorded anywhere.

Root cause

pkg/unikontainers/block.go:196-241 (getBlockVolumes):

func getBlockVolumes(mounts []specs.Mount, ukernel types.Unikernel) ([]types.BlockDevParams, error) {
	blkImgs := []types.BlockDevParams{}
	for i, m := range mounts {
		...
		if ukernel.SupportsFS(mInfo.FsType) {
			cleared, err := setLoopAutoclear(mInfo.Source, false)
			if err != nil {
				return nil, err          // discards blkImgs already unmounted in earlier iterations
			}
			mInfo.LoopAutoclear = cleared
			err = mount.Unmount(mInfo.MountPoint)
			if err != nil {
				return nil, err          // same problem
			}
			...
			blkImgs = append(blkImgs, mInfo)
		}
	}
	return blkImgs, nil
}

This error propagates up through blockRootfs.getBlockDevs() and getMonitorResources() (pkg/unikontainers/unikontainers.go:363-392), causing InitialSetup() (pkg/unikontainers/unikontainers.go:145-233) to return before it ever writes state.json or the monitor-resources file. cmd/urunc/create.go's createUnikontainer() just propagates the error with no cleanup of its own.

The only code that restores unmounted block volumes is restoreBlockVolumes() (pkg/unikontainers/block.go:248-280), called exclusively from Unikontainer.Delete() (pkg/unikontainers/unikontainers.go:857) using monRes.BlockArgs loaded from the very state file InitialSetup never got to write. cmd/urunc/delete.go's os.ErrNotExist fallback (no state file found) just does os.RemoveAll on the container directory, and it has no Unikontainer/monRes to call restoreBlockVolumes with. So there is no code path, in either create or delete, that can put the earlier, successfully-unmounted volumes back.

Expected behavior

Either getBlockVolumes should roll back (re-mount and restore the autoclear flag) any volumes it already unmounted before returning an error, or InitialSetup/createUnikontainer should perform equivalent rollback whenever a later step fails, so a failed create never leaves pre-existing host mounts permanently detached.

Impact

A transient, single-volume failure during container creation permanently detaches unrelated, already-processed host bind mounts with no automatic recovery. This is host-state corruption, since these are pre-existing host mounts that other processes or data pipelines may depend on. It requires manual host intervention (mount by hand) to fix, and the impact scales with the number of block-backed bind mounts declared per container.

Suggested fix

Track already-unmounted entries during the loop in getBlockVolumes and remount/restore-autoclear them before returning an error (mirroring restoreBlockVolumes), or have InitialSetup/createUnikontainer invoke a rollback helper whenever InitialSetup fails after the unmount stage. Also make cmd/urunc/delete.go's missing-state-file fallback account for the possibility that partial unmount state still needs restoring, instead of assuming nothing needs cleanup.

System info

  • Urunc version: main branch (HEAD as of 2026-07-29)
  • Arch: any (x86_64 / arm64)
  • VMM: any (bug is in the guest-rootfs/volume layer, independent of VMM backend)
  • Unikernel: any block-capable guest using extra bind-mounted block volumes (e.g. Unikraft)

Steps to reproduce

  1. Configure an OCI bundle for a block-capable guest with two or more bind mounts whose sources are loop-device-backed filesystems the guest supports (e.g. two separate ext2 image files).
  2. Arrange for the second (or later) mount's processing to fail, for example by revoking permission on that loop device so setLoopAutoclear fails, or making mount.Unmount return EBUSY for that entry.
  3. Run urunc create. The first volume's host mountpoint is already unmounted (visible in /proc/self/mountinfo before the failure returns). create fails and no state.json is written.
  4. Run urunc delete <id>. It hits the os.ErrNotExist branch in cmd/urunc/delete.go and just removes the container directory; restoreBlockVolumes is never invoked.
  5. Inspect the host. The first volume's original mountpoint remains unmounted indefinitely.

Disclosure per the project's LLM policy: this report was drafted with help from an LLM (Claude, Anthropic, model claude-sonnet-5) to search the codebase and put the description together. I reviewed and confirmed the finding myself by reading the exact source lines in a fresh clone of the repository and by checking open/closed issues and open/merged PRs for duplicates before submitting.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions