xai-org/grok-build · error

unmount {}: {err}

Error message

unmount {}: {err}

What it means

unmount_overlay lazily detaches a btrfs/mount overlay snapshot with umount2(MNT_DETACH). When the kernel returns a non-zero exit code, the raw OS error (last_os_error) is wrapped with the target path and bailed out. This means the kernel refused (or could not complete) the detach of the mount point at `target`.

Source

Thrown at crates/codegen/xai-fast-worktree/src/overlay/snapshot.rs:573

        let err = std::io::Error::last_os_error();
        bail!("mount overlay at {}: {err}", target.display());
    }

    Ok(())
}

/// Unmount a filesystem (lazy/detach to avoid EBUSY).
fn unmount_overlay(target: &Path) -> Result<()> {
    use std::ffi::CString;

    let c_target =
        CString::new(target.as_os_str().as_encoded_bytes()).context("target path not C-safe")?;

    // SAFETY: c_target is a valid CString.
    let rc = unsafe { libc::umount2(c_target.as_ptr(), libc::MNT_DETACH) };
    if rc != 0 {
        let err = std::io::Error::last_os_error();
        bail!("unmount {}: {err}", target.display());
    }

    Ok(())
}

/// Delete a btrfs subvolume/snapshot (delegates to shared btrfs module).
fn delete_btrfs_snapshot(path: &Path) -> Result<()> {
    crate::btrfs::snapshot::delete_snapshot(path)
}

/// Write metadata JSON to the worktree base dir for crash recovery.
///
/// Written to `<wt_base>/.fast-worktree-meta.json` (next to `upper/` and
/// `work/` dirs), NOT inside the overlay. This ensures the metadata is
/// always readable from the btrfs filesystem regardless of overlay mount state.
fn write_metadata(
    wt_base: &Path,
    snapshot_root: &Path,

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Check the target is actually a mount point (e.g. /proc/self/mounts or findmnt) before calling; skip/treat already-unmounted as success.
  2. Run as root or with CAP_SYS_ADMIN — umount2 requires privileges.
  3. Find and stop processes holding the mount open (fuser -vm <target> / lsof +f -- <target>) and retry.
  4. Since MNT_DETACH is lazy, a persistent EBUSY may need a plain retry loop or a stronger umount after closing holders.

Example fix

// before: unconditional unmount, fails on non-mount targets
unmount_overlay(&snapshot.target)?;
// after: skip targets that are not mounted
if is_mount_point(&snapshot.target) {
    unmount_overlay(&snapshot.target)?;
}
Defensive patterns

Strategy: validation

Validate before calling

fn is_mount_point(p: &Path) -> bool {
    let st = std::fs::metadata(p).map(|m| m.dev()).ok();
    let parent_st = p.parent().and_then(|p| std::fs::metadata(p).ok()).map(|m| m.dev());
    match (st, parent_st) {
        (Some(a), Some(b)) => a != b,
        _ => false,
    }
}
if !is_mount_point(&target) { skip_or_log(); }

Try / catch

match unmount_overlay(&target) {
    Err(e) if e.to_string().contains("Invalid argument") => {
        tracing::debug!("already unmounted: {target:?}");
    }
    Err(e) => return Err(e.context(format!("unmount {target:?} failed"))),
    Ok(()) => {}
}

Prevention

When it happens

Trigger: Calling cleanup_orphaned_overlay_snapshots -> unmount_overlay for a target path that is not currently a mount point (EINVAL/ENOENT), is busy (EBUSY because a process holds a file/dir open inside it), or requires privileges not available (EPERM when not root / lacking CAP_SYS_ADMIN).

Common situations: Running the cleanup as a non-root user; a leaked file descriptor or shell cd'd into the snapshot keeps it busy; the snapshot was already unmounted by another process (stale bookkeeping) so umount2 returns EINVAL; NFS/overlay races leaving the mount in a partially-detached state.

Related errors


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/295f824e806e555f. Report an issue: GitHub.