You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#881 hardened image archive extraction in dstack-verifier (download_image → extract_image_archive): entry paths are restricted to normal relative components, only regular files and directories are accepted, and tar::Entry::unpack_in confinement failures are treated as errors.
The same class of input — a tar archive fetched over the network and unpacked into a local directory — exists at two other call sites that #881 intentionally left out of scope. This issue tracks auditing them.
Call site 1: VMM OCI layer extraction (primary)
dstack/vmm/src/app/registry.rs:260 (extract_layer, reached from download_and_extract_layers at line 253):
let decoder = GzDecoder::new(data);letmut archive = tar::Archive::new(decoder);
archive.unpack(dest).context("failed to extract gzipped tar layer")?;
This unpacks guest-image layers pulled from a container registry over the OCI Distribution API, before any measurement or signature check binds the content. Compared to the verifier path after #881:
No entry-type allowlist. Symlinks, hardlinks, character/block devices and FIFOs in a layer are materialised on the host. tar-rs only special-cases dir/symlink/hardlink; other node types fall through to the generic unpack path.
Escaping entries are silently skipped, not rejected.Archive::unpack calls Entry::unpack_in per entry and discards the bool, so a member containing .. is dropped without any error. The extraction reports success with a partial result. fix(verifier): confine image archive extraction #881 explicitly turned this into a hard error for the verifier.
Single-member gzip only.flate2::read::GzDecoder stops at the first gzip member and returns clean EOF; tar::Archive then ends iteration with no error, so a multi-member layer extracts partially and silently. (Verified locally: a 2048-byte tar split across two concatenated gzip members yields 1024 bytes and 1 entry, err=None.)
Post-extraction cleanup is best-effort. The for dir in &["dev", "etc", "proc", "sys"] loop uses fs_err::remove_dir (non-recursive) and ignores the result, so a non-empty etc/ from a layer survives.
Mitigations that are already present, for the record: tar-rsunpack_in drops .. members, canonicalises the parent directory via validate_inside_dst before writing (so symlink-through-parent traversal is blocked), and masks setuid/setgid off unless set_preserve_permissions(true) is called. So this is a hardening/robustness gap and an unhelpful-failure-mode problem, not a known traversal vulnerability.
Note that this call site cannot simply reuse #881's rule set: container rootfs layers legitimately contain symlinks and whiteout entries, so it needs its own policy (e.g. an explicit type allowlist, erroring on skipped members, MultiGzDecoder, and a decompressed-size / entry-count cap) rather than a copy of the verifier logic.
Call site 2: dstackup (secondary)
dstack/crates/dstackup/src/image.rs:752 (extract) shells out to tar -xzf ... --no-same-owner --no-same-permissions. Ownership and permission carry-over are already handled and the intent is documented in a comment. Remaining gaps are symlink/hardlink members and the absence of a size cap. Lower priority than call site 1.
Suggested scope
Define and document the entry-type policy for OCI layer extraction in vmm.
Turn silently-skipped (escaping) members into an error.
Switch GzDecoder → MultiGzDecoder in extract_layer.
Context
#881 hardened image archive extraction in
dstack-verifier(download_image→extract_image_archive): entry paths are restricted to normal relative components, only regular files and directories are accepted, andtar::Entry::unpack_inconfinement failures are treated as errors.The same class of input — a tar archive fetched over the network and unpacked into a local directory — exists at two other call sites that #881 intentionally left out of scope. This issue tracks auditing them.
Call site 1: VMM OCI layer extraction (primary)
dstack/vmm/src/app/registry.rs:260(extract_layer, reached fromdownload_and_extract_layersat line 253):This unpacks guest-image layers pulled from a container registry over the OCI Distribution API, before any measurement or signature check binds the content. Compared to the verifier path after #881:
tar-rsonly special-cases dir/symlink/hardlink; other node types fall through to the generic unpack path.Archive::unpackcallsEntry::unpack_inper entry and discards thebool, so a member containing..is dropped without any error. The extraction reports success with a partial result. fix(verifier): confine image archive extraction #881 explicitly turned this into a hard error for the verifier.flate2::read::GzDecoderstops at the first gzip member and returns clean EOF;tar::Archivethen ends iteration with no error, so a multi-member layer extracts partially and silently. (Verified locally: a 2048-byte tar split across two concatenated gzip members yields 1024 bytes and 1 entry,err=None.)for dir in &["dev", "etc", "proc", "sys"]loop usesfs_err::remove_dir(non-recursive) and ignores the result, so a non-emptyetc/from a layer survives.Mitigations that are already present, for the record:
tar-rsunpack_indrops..members, canonicalises the parent directory viavalidate_inside_dstbefore writing (so symlink-through-parent traversal is blocked), and masks setuid/setgid off unlessset_preserve_permissions(true)is called. So this is a hardening/robustness gap and an unhelpful-failure-mode problem, not a known traversal vulnerability.Note that this call site cannot simply reuse #881's rule set: container rootfs layers legitimately contain symlinks and whiteout entries, so it needs its own policy (e.g. an explicit type allowlist, erroring on skipped members,
MultiGzDecoder, and a decompressed-size / entry-count cap) rather than a copy of the verifier logic.Call site 2: dstackup (secondary)
dstack/crates/dstackup/src/image.rs:752(extract) shells out totar -xzf ... --no-same-owner --no-same-permissions. Ownership and permission carry-over are already handled and the intent is documented in a comment. Remaining gaps are symlink/hardlink members and the absence of a size cap. Lower priority than call site 1.Suggested scope
vmm.GzDecoder→MultiGzDecoderinextract_layer.dev/etc/proc/syscleanup explicit about what it does and does not remove, or drop it in favour of the type allowlist.Refs: #881