astrid-runtime/astrid · error

capsule path is not canonical: {path}

Error message

capsule path is not canonical: {path}

What it means

Raised by append_entry while writing an entry into the canonical tar archive: the relative path is absolute, contains a `..` segment, or has an empty segment. Canonical capsules must contain only normalized, relative, root-anchored paths, so the entry is rejected rather than written.

Source

Thrown at crates/astrid-capsule-install/src/storage.rs:863

        let metadata = fs::symlink_metadata(&name)
            .with_context(|| format!("inspect capsule source entry {}", name.display()))?;
        children.push((name, metadata));
    }
    children.sort_by(|left, right| left.0.cmp(&right.0));
    Ok(children)
}

fn append_entry(
    builder: &mut Builder<GzEncoder<Vec<u8>>>,
    root: &Path,
    relative: &Path,
    metadata: &Metadata,
) -> anyhow::Result<()> {
    let path = relative
        .to_str()
        .ok_or_else(|| anyhow::anyhow!("capsule path is not valid UTF-8"))?;
    if path.starts_with('/') || path.split('/').any(|part| part == ".." || part.is_empty()) {
        bail!("capsule path is not canonical: {path}");
    }
    let mut header = Header::new_gnu();
    header.set_uid(0);
    header.set_gid(0);
    header.set_mtime(0);
    header.set_mode(if metadata.is_dir() { 0o755 } else { 0o644 });
    if metadata.is_dir() {
        header.set_entry_type(EntryType::Directory);
        header.set_size(0);
        header.set_cksum();
        builder
            .append_data(&mut header, path, io::empty())
            .with_context(|| format!("append capsule directory {path}"))?;
    } else {
        let mut file =
            File::open(root.join(relative)).with_context(|| format!("open capsule file {path}"))?;
        let mut bytes = Vec::new();
        file.read_to_end(&mut bytes)

View on GitHub (pinned to affd8760f4)

Solutions

  1. Normalize the offending path: make it relative to the capsule root with no `..`, `.`, empty, or duplicate slash segments.
  2. Fix any in-tree symlink whose stored relative target contains `..`; use paths anchored at the root instead.
  3. If you call collect_entries/canonical_capsule_archive with custom paths, derive them via path joins from the root rather than string concatenation.

Example fix

// before: relative path with a parent segment
let relative = "assets/../lib/main.wit";

// after: normalized canonical path
let relative = "lib/main.wit";
Defensive patterns

Strategy: validation

Validate before calling

fn path_is_canonical(p: &str) -> bool {
    !p.starts_with('/') && p.split('/').all(|part| !part.is_empty() && part != "..")
}

Type guard

fn is_canonical_relative(p: &Path) -> bool {
    p.is_relative() && p.components().all(|c| matches!(c, std::path::Component::Normal(_)))
}

Try / catch

if !path_is_canonical(&rel) {
    eprintln!("normalize archive paths before publishing: {rel}");
    std::process::exit(1);
}

Prevention

When it happens

Trigger: canonical_capsule_archive encountering a collected relative path that is not canonical — typically from a symlink relative target with `..` components, or unusual file names with `//` produced by joining; entries reaching append_entry fail the starts_with('/') / `..` / empty-segment check.

Common situations: Symlink targets written as `../foo` inside the tree; files created with odd names containing double slashes via tooling; custom code paths that hand-build relative paths instead of deriving them from the root walk.

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/0e5c8d7c85abd946. Report an issue: GitHub.