jdx/mise · error

refusing cleanup through symlink component {}

Error message

refusing cleanup through symlink component {}

What it means

During apk transient cleanup, mise walks each path component of the target with symlink_metadata and refuses to continue if any component is itself a symlink. This prevents a planted or unexpected symlink from redirecting deletions outside the rootfs (TOCTOU-style escape).

Source

Thrown at src/oci/packages.rs:562

    remove_path(&log)
}

/// Reject a cleanup target when it or any path component below `rootfs` is a
/// symlink. OCI package layers are untrusted, so following one during cleanup
/// could remove files outside the layer root.
fn reject_symlink_components(rootfs: &Path, target: &Path) -> Result<()> {
    let relative = target
        .strip_prefix(rootfs)
        .wrap_err_with(|| format!("cleanup target {} is outside rootfs", target.display()))?;
    let mut current = rootfs.to_path_buf();
    for component in relative.components() {
        let std::path::Component::Normal(component) = component else {
            bail!("invalid cleanup target {}", target.display());
        };
        current.push(component);
        match fs::symlink_metadata(&current) {
            Ok(metadata) if metadata.file_type().is_symlink() => {
                bail!(
                    "refusing cleanup through symlink component {}",
                    current.display()
                );
            }
            Ok(_) => {}
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
            Err(err) => {
                return Err(err)
                    .wrap_err_with(|| format!("reading metadata for {}", current.display()));
            }
        }
    }
    Ok(())
}

/// Remove every regular file under `dir` (recursively), leaving the directory
/// structure and any symlinks intact. A missing `dir` is a no-op.
fn remove_files_recursively(dir: &Path) -> Result<()> {

View on GitHub (pinned to afd2eddd3a)

Solutions

  1. Use a base image without symlinked components on the apk paths, or normalize the layout
  2. Remove/repoint the offending symlink inside the rootfs before building
  3. Skip custom transient cleanup if your image layout legitimately relies on symlinks

Example fix

null
Defensive patterns

Strategy: try-catch

Validate before calling

# check for symlinked components on apk paths in the base image
docker run --rm --entrypoint sh BASE_IMAGE -c 'for p in /etc/apk /lib/apk /lib/apk/db /var/cache/apk; do [ -L "$p" ] && echo "symlink: $p"; done; true'

Try / catch

try {
  buildOciImage(config);
} catch (e) {
  if (String(e.message).includes("refusing cleanup through symlink component")) {
    // flatten the symlinked layout in the base image or adjust it
  } else throw e;
}

Prevention

When it happens

Trigger: A directory component of an apk transient path inside the rootfs is a symlink when clean_apk_transients walks it — e.g. an image where /lib, /var/cache, or the apk db path is symlinked.

Common situations: Base images using symlinked directory layouts (common with merged-/usr or musl layouts); attacker-influenced layers; apk creating cache dirs as symlinks.

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 jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/8b61388a2d837ad5. Report an issue: GitHub.