Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/install/TarballStream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ pub struct TarballStream {

bytes_received: usize,
entry_count: u32,
/// Headers `read_next_header` has returned; compared against
/// `archive_file_count()` in `check_no_header_discarded`.
headers_read: c_int,
fail: Option<crate::Error>,
invalid_name: bool,

Expand Down Expand Up @@ -262,6 +265,7 @@ impl TarballStream {
deferred_symlinks: Vec::new(),
bytes_received: 0,
entry_count: 0,
headers_read: 0,
fail: None,
invalid_name: false,
drain_task: thread_pool::Task {
Expand Down Expand Up @@ -542,9 +546,14 @@ impl TarballStream {
Phase::WantHeader => {
let mut entry: *mut lib::Entry = core::ptr::null_mut();
match archive.read_next_header(&mut entry) {
// Either an out-of-input yield from the read callback
// or a block libarchive discarded; the latter fails
// the extraction in `check_no_header_discarded` once
// the next header (or EOF) is reached.
lib::Result::Retry if (*this).archive_holds_reading => continue,
lib::Result::Retry => return Ok(()),
lib::Result::Eof => {
Self::check_no_header_discarded(this, archive)?;
#[cfg(unix)]
{
let dest = (*this).dest.unwrap();
Expand All @@ -557,6 +566,8 @@ impl TarballStream {
return Ok(());
}
lib::Result::Ok | lib::Result::Warn => {
(*this).headers_read += 1;
Self::check_no_header_discarded(this, archive)?;
// libarchive returned OK/WARN with a valid entry
// pointer owned by `archive`; it stays valid until
// the next `read_next_header`. No other Rust
Expand Down Expand Up @@ -605,6 +616,35 @@ impl TarballStream {
} // unsafe
}

/// Fail the extraction if libarchive started a header it never handed to
/// `step()`. The only way that happens is a block with a bad header
/// checksum: the tar reader discards it and reports `Retry`, the status
/// the read callback also uses for "out of input", so `step()` cannot
/// fail on the `Retry` itself. Reading on from there resyncs on the next
/// intact header and drops the damaged member from the package, which is
/// why the count is checked at every header and at EOF instead.
/// `Archiver::extract_to_dir` fails on the same tarball.
///
/// # Safety
/// `this` must be live; raw-ptr field read only.
unsafe fn check_no_header_discarded(
this: *mut Self,
archive: &lib::Archive,
) -> crate::Result<()> {
let started = archive.file_count();
// SAFETY: see fn-level # Safety.
let returned = unsafe { (*this).headers_read };
if started == returned {
return Ok(());
}
bun_output::scoped_log!(
TarballStream,
"libarchive discarded {} damaged header block(s)",
started - returned
);
Err(crate::Error::Fail)
}

/// # Safety
/// `this` must be live and rooted at the Box allocation (i.e. the
/// pointer threaded from `drain_callback` → `drain` → `step`, NOT a
Expand Down
37 changes: 32 additions & 5 deletions src/libarchive/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ pub mod lib {
pub enum Result {
Eof = 1,
Ok = 0,
/// From `read_next_header` on a blocking source (`read_open_memory`)
/// this has exactly one meaning: the tar reader discarded a 512-byte
/// block whose header checksum did not match and is positioned on the
/// block after it. Calling again resyncs on whatever header comes next,
/// so the member that block belonged to is silently lost. Header loops
/// therefore treat it as a corrupt archive, the same as `Fatal`.
/// `TarballStream`'s non-blocking read callback also produces it when
/// it runs out of input (see the BUN PATCHes in `vendor/libarchive`).
Retry = -10,
Warn = -20,
Failed = -25,
Expand Down Expand Up @@ -90,6 +98,7 @@ pub mod lib {
offset: *mut la_int64_t,
) -> Result;
fn archive_error_string(a: *mut Archive) -> *const c_char;
fn archive_file_count(a: *mut Archive) -> c_int;
// streaming-read setup (used by TarballStream's resumable extractor)
pub fn archive_read_set_format(a: *mut Archive, code: c_int) -> c_int;
pub fn archive_read_append_filter(a: *mut Archive, code: c_int) -> c_int;
Expand Down Expand Up @@ -365,6 +374,17 @@ pub mod lib {
unsafe { ZStr::from_c_ptr(p) }.as_bytes()
}

/// `archive_file_count`: the number of headers `read_next_header` has
/// started so far. libarchive counts a header when it starts reading
/// it, so a block it then discards (see [`Result::Retry`]) is counted
/// even though no entry is returned for it; the final call that
/// returns `Eof` is not counted. A non-blocking yield and its resume
/// count once (`archive_read.c`, `read_header_in_progress`).
pub fn file_count(&self) -> c_int {
// SAFETY: `self` is a live archive handle.
unsafe { archive_file_count(self.as_mut_ptr()) }
}

// ── write side ─────────────────────────────────────────────────────
pub fn write_new() -> *mut Archive {
// SAFETY: FFI call with no preconditions.
Expand Down Expand Up @@ -723,7 +743,6 @@ pub mod lib {
let mut entry: *mut Entry = core::ptr::null_mut();
loop {
return match a.read_next_header(&mut entry) {
Result::Retry => continue,
Result::Eof => IteratorResult::init_res(None),
// `Warn` still yields a fully populated entry; see `Result::succeeded`.
Result::Ok | Result::Warn => {
Expand Down Expand Up @@ -1292,8 +1311,7 @@ impl Archiver {

match r {
lib::Result::Eof => break 'loop_,
lib::Result::Retry => continue 'loop_,
lib::Result::Failed | lib::Result::Fatal => {
lib::Result::Retry | lib::Result::Failed | lib::Result::Fatal => {
return Err(crate::Error::Fail);
}
_ => {
Expand Down Expand Up @@ -1438,8 +1456,17 @@ impl Archiver {

match r {
lib::Result::Eof => break 'loop_,
lib::Result::Retry => continue 'loop_,
lib::Result::Failed | lib::Result::Fatal => {
lib::Result::Retry | lib::Result::Failed | lib::Result::Fatal => {
if options.log {
// SAFETY: `archive` is the live `read_new()` handle this
// extraction loop is iterating.
let archive_error = slice_to_nul(unsafe { &*archive }.error_string());
Output::err(
"libarchive error",
"reading next header: {}",
(bstr::BStr::new(archive_error),),
);
}
return Err(crate::Error::Fail);
}
_ => {
Expand Down
Loading
Loading