astrid-runtime/astrid · critical

materialization parent is a symlink: {}

Error message

materialization parent is a symlink: {}

What it means

reject_symlink_ancestors walks each path component from the destination root as files are created and bails if any existing component is a symlink. This prevents a symlinked directory inside the destination from redirecting extracted files outside the materialization root (symlink-escape attack).

Source

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

        fs::write(&output, bytes)
            .with_context(|| format!("write materialized file {}", output.display()))?;
    }
    fs::write(destination.join("meta.json"), &package.metadata)
        .context("write materialized capsule metadata")?;
    fs::write(destination.join("authority.json"), &package.authority)
        .context("write materialized capsule authority")?;
    Ok(())
}

fn reject_symlink_ancestors(root: &Path, path: &Path) -> anyhow::Result<()> {
    let relative = path
        .strip_prefix(root)
        .map_err(|_| anyhow::anyhow!("materialization path escaped destination"))?;
    let mut current = root.to_path_buf();
    for component in relative.components() {
        current.push(component.as_os_str());
        if fs::symlink_metadata(&current).is_ok_and(|metadata| metadata.file_type().is_symlink()) {
            bail!("materialization parent is a symlink: {}", current.display());
        }
    }
    Ok(())
}

mod leftover;
mod migration;

pub use leftover::retire_unmatched_legacy_authority_receipts;
pub use migration::{
    LegacyCapsuleAuthorityReceipt, LegacyCapsuleMigrationReport, LegacyEnvSecretImportStatus,
    legacy_capsule_authority_status, legacy_env_secret_import_status, migrate_all_native_capsules,
    migrate_all_native_capsules_with_report, migrate_native_capsules,
    migrate_native_capsules_with_report,
};

fn canonical_legacy_archive(
    home: &astrid_core::dirs::AstridHome,

View on GitHub (pinned to affd8760f4)

Solutions

  1. Use a fresh, empty destination directory for materialization.
  2. Since the extractor already rejects link entries (error 145), treat this error as evidence of pre-existing or injected symlinks and inspect/remove them: find dest -type l.
  3. Re-obtain and re-verify the archive; combined with this error it likely indicates tampering.

Example fix

// before
let dest = Path::new("/var/tmp/shared-capsule"); // may contain symlinks
// after
let dest = tempdir()?.path().to_path_buf(); // fresh empty dir
assert!(fs::read_dir(&dest)?.next().is_none());
Defensive patterns

Strategy: validation

Validate before calling

fn has_no_symlinks_under(dest: &Path) -> std::io::Result<bool> {
    for entry in fs::read_dir(dest)? {
        let e = entry?;
        if fs::symlink_metadata(e.path())?.file_type().is_symlink() {
            return Ok(false);
        }
    }
    Ok(true)
}
// call before materializing into a reused directory

Try / catch

match materialize_capsule_package(&pkg, &dest) {
    Err(e) if e.to_string().contains("symlink") => {
        error!("symlink escape detected at {} — aborting, do not retry in place", dest.display());
        use_fresh_tempdir_and_retry()?;
    }
    other => other,
}

Prevention

When it happens

Trigger: Extracting an archive whose entries create a directory path where an ancestor within the destination is a symlink — e.g. archive contains 'link -> /tmp/evil' plus 'link/file.wasm', or an attacker pre-created a symlink at an intermediate path.

Common situations: Materializing into a destination directory that already contains attacker-controlled symlinks; archives crafted to bypass plain path checks via link entries; shared temp directories with leftover symlinks from prior runs.

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