Skip to content

Commit 9c5df0b

Browse files
authored
Fix GNU long-name extension stream corruption on validation error (#434)
In prepare_header_path, the GNU long-name extension entry was written to the stream before validating the truncated path via set_truncated_path_for_gnu_header. If validation failed (e.g., the truncated path contained '..'), the extension entry was already committed to the stream with no rollback. The Builder remained usable, so subsequent writes succeeded — but their data got associated with the orphaned long-name path, silently corrupting the archive. Fix by moving the truncation and validation above the append() call. Since set_truncated_path_for_gnu_header only writes to the in-memory header buffer (not the stream), reordering is safe. Also audited prepare_header_link — it does not have this issue because link names allow all path components (ParentDir, RootDir, etc.) and there is no post-append validation step.
1 parent 88b1e3b commit 9c5df0b

File tree

2 files changed

+48
-5
lines changed

2 files changed

+48
-5
lines changed

src/builder.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -756,21 +756,25 @@ fn prepare_header_path(dst: &mut dyn Write, header: &mut Header, path: &Path) ->
756756
if data.len() < max {
757757
return Err(e);
758758
}
759-
let header2 = prepare_header(data.len() as u64, b'L');
760-
// null-terminated string
761-
let mut data2 = data.chain(io::repeat(0).take(1));
762-
append(dst, &header2, &mut data2)?;
763-
764759
// Truncate the path to store in the header we're about to emit to
765760
// ensure we've got something at least mentioned. Note that we use
766761
// `str`-encoding to be compatible with Windows, but in general the
767762
// entry in the header itself shouldn't matter too much since extraction
768763
// doesn't look at it.
764+
//
765+
// Validate the truncated path BEFORE writing the long-name extension
766+
// to the stream. If validation fails after writing, the orphaned
767+
// extension entry corrupts subsequent archive entries.
769768
let truncated = match str::from_utf8(&data[..max]) {
770769
Ok(s) => s,
771770
Err(e) => str::from_utf8(&data[..e.valid_up_to()]).unwrap(),
772771
};
773772
header.set_truncated_path_for_gnu_header(truncated)?;
773+
774+
let header2 = prepare_header(data.len() as u64, b'L');
775+
// null-terminated string
776+
let mut data2 = data.chain(io::repeat(0).take(1));
777+
append(dst, &header2, &mut data2)?;
774778
}
775779
Ok(())
776780
}

tests/all.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1787,3 +1787,42 @@ fn pax_and_gnu_uid_gid() {
17871787
}
17881788
}
17891789
}
1790+
1791+
#[test]
1792+
fn append_data_error_does_not_corrupt_subsequent_entries() {
1793+
// When append_data fails (e.g., path contains ".."), subsequent
1794+
// successful writes must not be corrupted by an orphaned GNU
1795+
// long-name extension entry left in the stream.
1796+
let mut ar = Builder::new(Vec::new());
1797+
1798+
// First write: a long path (>100 bytes to trigger GNU long-name extension)
1799+
// containing ".." not as the last component, which will fail validation
1800+
// in set_truncated_path_for_gnu_header.
1801+
let dotdot_path = "a/../b/".to_string() + &"c".repeat(100);
1802+
let mut header = Header::new_gnu();
1803+
header.set_size(5);
1804+
header.set_cksum();
1805+
let result = ar.append_data(&mut header, &dotdot_path, &b"first"[..]);
1806+
assert!(result.is_err());
1807+
1808+
// Second write: a clean path that should succeed normally.
1809+
let mut header = Header::new_gnu();
1810+
header.set_size(6);
1811+
header.set_cksum();
1812+
ar.append_data(&mut header, "clean.txt", &b"second"[..])
1813+
.unwrap();
1814+
1815+
// Verify: the archive should contain exactly one entry at "clean.txt"
1816+
// with content "second". Before the fix, it contained an entry at the
1817+
// dotdot path with content "second" — the orphaned long-name stole the data.
1818+
let data = ar.into_inner().unwrap();
1819+
let mut archive = Archive::new(&data[..]);
1820+
let entries: Vec<_> = archive
1821+
.entries()
1822+
.unwrap()
1823+
.collect::<Result<_, _>>()
1824+
.unwrap();
1825+
1826+
assert_eq!(entries.len(), 1);
1827+
assert_eq!(entries[0].path().unwrap().to_str().unwrap(), "clean.txt");
1828+
}

0 commit comments

Comments
 (0)