astrid-runtime/astrid · error

capsule projection contains a symbolic link: {}

Error message

capsule projection contains a symbolic link: {}

What it means

This error means the walk of the capsule's projected directory found a symbolic link, which the kernel forbids: symlinks can redirect reads outside the capsule projection and bypass its containment guarantees. The library throws it during projection inventory so that any capsule whose on-disk projection contains a symlink is rejected rather than traversed.

Source

Thrown at crates/astrid-kernel/src/lib.rs:1640

        ) -> anyhow::Result<()> {
            for entry in std::fs::read_dir(directory).map_err(|error| {
                anyhow::anyhow!("read capsule projection {}: {error}", directory.display())
            })? {
                let entry = entry
                    .map_err(|error| anyhow::anyhow!("read capsule projection entry: {error}"))?;
                let path = entry.path();
                let relative = path.strip_prefix(root).map_err(|_| {
                    anyhow::anyhow!("capsule projection escaped its root: {}", path.display())
                })?;
                let relative_text = relative.to_str().ok_or_else(|| {
                    anyhow::anyhow!("capsule projection path is not UTF-8: {}", path.display())
                })?;
                let metadata = std::fs::symlink_metadata(&path).map_err(|error| {
                    anyhow::anyhow!("inspect capsule projection {}: {error}", path.display())
                })?;
                let file_type = metadata.file_type();
                if file_type.is_symlink() {
                    anyhow::bail!(
                        "capsule projection contains a symbolic link: {}",
                        path.display()
                    );
                }
                if file_type.is_dir() {
                    inventory.directories.insert(relative_text.to_owned());
                    walk(root, &path, inventory)?;
                } else if file_type.is_file() {
                    inventory.files.insert(relative_text.to_owned());
                } else {
                    anyhow::bail!(
                        "capsule projection contains a special file: {}",
                        path.display()
                    );
                }
            }
            Ok(())
        }

View on GitHub (pinned to affd8760f4)

Solutions

  1. Repackage the capsule so it contains only regular files and directories (dereference symlinks at packaging time, e.g. tar -h / cp -L)
  2. Find the offending link (the path in the error) and replace it with a real copy of its target, or remove it
  3. Fix the build pipeline to not emit symlinks into the capsule output (e.g. configure the bundler to inline/duplicate linked files)
  4. If the symlink appeared post-materialization, investigate what wrote into the capsule directory and re-materialize

Example fix

# before: packaging preserves symlinks
tar -cf capsule.capsule -C build .

# after: dereference symlinks into regular files
tar -h -cf capsule.capsule -C build .
Defensive patterns

Strategy: validation

Validate before calling

fn projection_has_symlinks(root: &Path) -> anyhow::Result<bool> {
    for entry in walkdir::WalkDir::new(root).follow_links(false) {
        if entry?.path_is_symlink() { return Ok(true); }
    }
    Ok(false)
}

Try / catch

match result {
    Err(e) if e.to_string().contains("symbolic link") => {
        // clean the offending path and repackage
    }
    other => other?,
}

Prevention

When it happens

Trigger: Inventorying a projection (the recursive walk over the materialized capsule directory) when std::fs::symlink_metadata reports a symlink at any path within the projection — e.g. the capsule archive contained a symlink, or something on the host placed one inside the materialized directory.

Common situations: Packaging a capsule on macOS/Linux where a build step created symlinks (e.g. node_modules, vendored deps) and tar preserved them; extracting an archive with symlink-preserving flags; an attacker or misconfigured script planting symlinks in the capsule output directory.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


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