astrid-runtime/astrid · error

process path {} overlaps a copy-on-write mask

Error message

process path {} overlaps a copy-on-write mask

What it means

After computing the process paths, wrap_with_process_paths rejects any granted read or write path that overlaps a copy-on-write mask. Overlap would let the child read or write through a path the mask is supposed to deny, defeating the CoW guarantee, so the call fails with PermissionDenied before spawning.

Source

Thrown at crates/astrid-workspace/src/sandbox/mod.rs:307

        for masked in extra_masks {
            let _ = validate_sandbox_str(masked, "workspace CoW mask")?;
            if !masked.exists() {
                return Err(io::Error::new(
                    io::ErrorKind::NotFound,
                    format!(
                        "workspace CoW mask path does not exist: {} — refusing to spawn \
                         a child without the intended copy-on-write deny",
                        masked.display()
                    ),
                ));
            }
        }
        for granted in extra_read_paths.iter().chain(extra_write_paths) {
            if extra_masks
                .iter()
                .any(|masked| paths_overlap(granted, masked))
            {
                return Err(io::Error::new(
                    io::ErrorKind::PermissionDenied,
                    format!(
                        "process path {} overlaps a copy-on-write mask",
                        granted.display()
                    ),
                ));
            }
        }

        #[cfg(target_os = "linux")]
        {
            // Bubblewrap implementation - paths are passed as separate argv entries (no injection).
            // The process can only read the root OS, but can only write to the worktree and /tmp.
            let mut bwrap = Command::new("bwrap");
            if clear_env {
                bwrap.env_clear();
            }
            bwrap

View on GitHub (pinned to affd8760f4)

Solutions

  1. Remove or narrow the granted path so it no longer overlaps the mask (grant a sibling/subpath instead)
  2. Remove the conflicting mask if the path is legitimately meant to be writable
  3. Normalize both paths (canonicalize) and compare to spot overlap from symlinks or relative components
  4. Split the operation: run the masked child and the granted child as separate spawns

Example fix

// before
write_paths.push(PathBuf::from("/workspace")); // overlaps mask /workspace/secrets
// after
write_paths.push(PathBuf::from("/workspace/public"));
Defensive patterns

Strategy: validation

Validate before calling

fn overlaps(a: &Path, b: &Path) -> bool { a.starts_with(b) || b.starts_with(a) }
assert!(grants.iter().all(|g| !masks.iter().any(|m| overlaps(g, m))),
    "grant overlaps a CoW mask");

Type guard

fn grant_safe(grant: &Path, masks: &[PathBuf]) -> bool {
    !masks.iter().any(|m| paths_overlap(grant, m))
}

Try / catch

match wrap_with_process_paths(...) {
    Err(e) if e.kind() == io::ErrorKind::PermissionDenied
        && e.to_string().contains("overlaps a copy-on-write mask") =>
        eprintln!("narrow grant: {e}"),
    other => other?,
}

Prevention

When it happens

Trigger: Calling wrap_with_process_paths where an entry in extra_read_paths or extra_write_paths shares a path prefix (per paths_overlap) with any entry in extra_masks.

Common situations: Granting the workspace root as writable while also masking a subdirectory of it; copy-pasted path lists where a grant and a mask point at the same directory; symlink or relative-path aliases that resolve to the same target.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/f4e1617495b222f0. Report an issue: GitHub.