astrid-runtime/astrid · error
materialization path escaped destination
Error message
materialization path escaped destination
What it means
Thrown by reject_symlink_ancestors when the path being materialized cannot be expressed as a path relative to the destination root, meaning the materialization path points outside the destination directory. The library refuses to write outside the destination to prevent path traversal attacks.
Solutions
- Rebuild the capsule package ensuring all member paths are relative and stay inside the archive root; republish.
- Check the package source for entries with ../ or absolute paths and strip/normalize them before materialization.
- Only materialize packages from trusted sources and verify archive member paths before calling materialize_capsule_package.
Example fix
// before: archive members recorded as "../../etc/evil" // after: build the package with root-relative paths, e.g. "lib/component.wasm"
Defensive patterns
Strategy: validation
Validate before calling
fn is_safe_member_path(root: &Path, member: &str) -> bool {
let p = Path::new(member);
!p.is_absolute() && p.components().all(|c| !matches!(c, std::path::Component::ParentDir))
&& Path::new(root).join(p).starts_with(root)
} Prevention
- Only materialize capsules from trusted, signed sources
- Pre-scan archive member paths for ../ or absolute entries before extraction
- Normalize paths when building packages so every member is root-relative
When it happens
Trigger: materialize_capsule_package receives a package whose internal entry paths, joined onto the destination root, are not under that root (e.g. paths containing ../ segments or absolute entries), so path.strip_prefix(root) fails.
Common situations: Extracting a maliciously crafted capsule archive with path-escaping entries ('zip-slip' style archives); archives built on other platforms or by buggy tooling that stored absolute/parent-relative member names.
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
- cache contains a redirect or special entry
- capsule archive contains an unsafe path
- capsule projection escaped its root
- durable capsule archive contains unsafe path
- durable capsule has unsafe WIT metadata path
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/9c8b7a2ebb98f458.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-capsule-install/src/storage.rs:612
}
let mut bytes = Vec::new();
entry
.read_to_end(&mut bytes)
.context("read durable capsule archive file")?;
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(¤t).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,View on GitHub (pinned to affd8760f4)