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(¤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,
migrate_native_capsules_with_report,
};
fn canonical_legacy_archive(
home: &astrid_core::dirs::AstridHome,View on GitHub (pinned to affd8760f4)
Solutions
- Use a fresh, empty destination directory for materialization.
- 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.
- 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
- Always materialize into a fresh, empty, private temp directory.
- Audit shared destination directories for symlinks (find dest -type l) before reuse.
- Treat this error as a security event: investigate the archive and environment, don't just retry.
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
- capsule projection contains a symbolic link: {}
- projected path is redirected or not a regular file: {}
- manifest exceeds its installed capability approval: {details
- installed WASM executable differs from its authority receipt
- capsule '{}' is {}; explicit local approval is required
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/adaa1373b7a5b833.
Report an issue: GitHub.