From 6de4b9e9fc005162f436b5bac920584eb0a04d78 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:11:52 +0000 Subject: [PATCH 1/5] zlib: reach the z_stream only through a raw pointer while zlib-ng holds its back-pointer zlib-ng stores the z_stream pointer in its internal state at init and reads and writes the stream through it inside every later call. Rust formed a `&mut zStream_struct` (or a `&mut self` over a struct that held one inline) and kept it alive across those calls. That foreign access to a protected borrow is undefined behaviour under Stacked Borrows and Tree Borrows. Add `ZStream`, a heap-pinned z_stream behind a NonNull. It never hands out a reference to the stream: field reads and writes go through the raw pointer, and the C calls take `as_ptr()`. `ZlibReaderArrayList`, `ZlibCompressorArrayList`, `DeflateEncoder`, `InflateDecoder`, and the node:zlib `Context` all hold a `ZStream` now. The ArrayList wrappers no longer need to be boxed, since the stream is pinned on its own. --- src/runtime/node/zlib/NativeZlib.rs | 74 ++++---- src/zlib/lib.rs | 256 ++++++++-------------------- src/zlib/stream.rs | 145 ++++++++++++++++ 3 files changed, 249 insertions(+), 226 deletions(-) create mode 100644 src/zlib/stream.rs diff --git a/src/runtime/node/zlib/NativeZlib.rs b/src/runtime/node/zlib/NativeZlib.rs index 18156aab9c1f..2479d45c553b 100644 --- a/src/runtime/node/zlib/NativeZlib.rs +++ b/src/runtime/node/zlib/NativeZlib.rs @@ -214,7 +214,7 @@ pub(crate) use _impl::NativeZlib; pub(crate) struct Context { pub(crate) mode: c::NodeMode, - pub(crate) state: c::z_stream, + pub(crate) state: c::ZStream, pub(crate) err: c::ReturnCode, pub flush: c::FlushValue, pub dictionary: Vec, @@ -225,7 +225,7 @@ impl Default for Context { fn default() -> Self { Self { mode: c::NodeMode::NONE, - state: bun_core::ffi::zeroed::(), + state: c::ZStream::new(), err: c::ReturnCode::Ok, flush: c::FlushValue::NoFlush, dictionary: Vec::new(), @@ -275,7 +275,7 @@ impl Context { // SAFETY: FFI — `state` is a valid #[repr(C)] z_stream; zlibVersion() returns a static C string. DEFLATE | GZIP | DEFLATERAW => unsafe { self.err = c::deflateInit2_( - &raw mut self.state, + self.state.as_ptr(), level, 8, window_bits_actual, @@ -288,7 +288,7 @@ impl Context { // SAFETY: FFI — `state` is a valid #[repr(C)] z_stream; zlibVersion() returns a static C string. INFLATE | GUNZIP | UNZIP | INFLATERAW => unsafe { self.err = c::inflateInit2_( - &raw mut self.state, + self.state.as_ptr(), window_bits_actual, c::zlibVersion().cast(), c_int::try_from(mem::size_of::()).expect("int cast"), @@ -320,11 +320,11 @@ impl Context { match self.mode { // SAFETY: FFI — state is an initialized deflate stream; dict_ptr/dict_len borrow the Context-owned dictionary copy. DEFLATE | DEFLATERAW => unsafe { - self.err = c::deflateSetDictionary(&raw mut self.state, dict_ptr, dict_len); + self.err = c::deflateSetDictionary(self.state.as_ptr(), dict_ptr, dict_len); }, // SAFETY: FFI — state is an initialized inflate stream; dict_ptr/dict_len borrow the Context-owned dictionary copy. INFLATERAW => unsafe { - self.err = c::inflateSetDictionary(&raw mut self.state, dict_ptr, dict_len); + self.err = c::inflateSetDictionary(self.state.as_ptr(), dict_ptr, dict_len); }, _ => {} } @@ -344,7 +344,7 @@ impl Context { // are 0 (next_out non-null) so the internal deflate(Z_BLOCK) returns // Z_BUF_ERROR without touching any previously installed caller buffer. unsafe { - self.err = c::deflateParams(&raw mut self.state, level, strategy); + self.err = c::deflateParams(self.state.as_ptr(), level, strategy); } } _ => {} @@ -357,8 +357,8 @@ impl Context { fn error_for_message(&self, default: &'static core::ffi::CStr) -> Error { let mut message: *const c_char = default.as_ptr(); - if !self.state.err_msg.is_null() { - message = self.state.err_msg; + if !self.state.err_msg().is_null() { + message = self.state.err_msg(); } Error { msg: message, @@ -384,11 +384,11 @@ impl Context { match self.mode { // SAFETY: FFI — state was initialized as a deflate stream by deflateInit2_. DEFLATE | DEFLATERAW | GZIP => unsafe { - self.err = c::deflateReset(&raw mut self.state); + self.err = c::deflateReset(self.state.as_ptr()); }, // SAFETY: FFI — state was initialized as an inflate stream by inflateInit2_. INFLATE | INFLATERAW | GUNZIP => unsafe { - self.err = c::inflateReset(&raw mut self.state); + self.err = c::inflateReset(self.state.as_ptr()); }, _ => {} } @@ -399,22 +399,16 @@ impl Context { } pub(crate) fn set_buffers(&mut self, in_: Option<&[u8]>, out: Option<&mut [u8]>) { - self.state.avail_in = match &in_ { - Some(p) => u32::try_from(p.len()).expect("int cast"), - None => 0, + let (next_in, avail_in) = match in_ { + Some(p) => (p.as_ptr(), u32::try_from(p.len()).expect("int cast")), + None => (core::ptr::null(), 0), }; - self.state.next_in = match in_ { - Some(p) => p.as_ptr(), - None => core::ptr::null(), - }; - self.state.avail_out = match &out { - Some(p) => u32::try_from(p.len()).expect("int cast"), - None => 0, - }; - self.state.next_out = match out { - Some(p) => p.as_mut_ptr(), - None => core::ptr::null_mut(), + self.state.set_input(next_in, avail_in); + let (next_out, avail_out) = match out { + Some(p) => (p.as_mut_ptr(), u32::try_from(p.len()).expect("int cast")), + None => (core::ptr::null_mut(), 0), }; + self.state.set_output(next_out, avail_out); } pub(crate) fn flush_value_is_valid(flush: u32) -> bool { @@ -448,8 +442,8 @@ impl Context { return self.do_work_deflate(); } UNZIP => { - if self.state.avail_in > 0 { - next_expected_header_byte = Some(self.state.next_in.cast::()); + if self.state.avail_in() > 0 { + next_expected_header_byte = Some(self.state.next_in()); } if self.gzip_id_bytes_read == 0 { let Some(p) = next_expected_header_byte else { @@ -461,7 +455,7 @@ impl Context { // SAFETY: advancing within the input buffer; only dereferenced // below after confirming avail_in > 1. next_expected_header_byte = Some(unsafe { p.add(1) }); - if self.state.avail_in == 1 { + if self.state.avail_in() == 1 { // The only available byte was already read. return self.do_work_inflate(); } @@ -497,12 +491,12 @@ impl Context { fn do_work_deflate(&mut self) { // SAFETY: FFI — state is an initialized deflate stream. - self.err = unsafe { c::deflate(&raw mut self.state, self.flush) }; + self.err = unsafe { c::deflate(self.state.as_ptr(), self.flush) }; } fn do_work_inflate(&mut self) { // SAFETY: FFI — state is an initialized inflate stream. - self.err = unsafe { c::inflate(&raw mut self.state, self.flush) }; + self.err = unsafe { c::inflate(self.state.as_ptr(), self.flush) }; if self.mode != c::NodeMode::INFLATERAW && self.err == c::ReturnCode::NeedDict @@ -515,38 +509,38 @@ impl Context { (dict.as_ptr(), u32::try_from(dict.len()).expect("int cast")) }; // SAFETY: FFI — state is an initialized inflate stream; dict is the Context-owned copy. - self.err = unsafe { c::inflateSetDictionary(&raw mut self.state, dict_ptr, dict_len) }; + self.err = unsafe { c::inflateSetDictionary(self.state.as_ptr(), dict_ptr, dict_len) }; if self.err == c::ReturnCode::Ok { // SAFETY: FFI — state is an initialized inflate stream. - self.err = unsafe { c::inflate(&raw mut self.state, self.flush) }; + self.err = unsafe { c::inflate(self.state.as_ptr(), self.flush) }; } else if self.err == c::ReturnCode::DataError { self.err = c::ReturnCode::NeedDict; } } - while self.state.avail_in > 0 + while self.state.avail_in() > 0 && self.mode == c::NodeMode::GUNZIP && self.err == c::ReturnCode::StreamEnd // SAFETY: avail_in > 0 ⇒ next_in points to ≥1 readable byte. - && unsafe { *self.state.next_in } != 0 + && unsafe { *self.state.next_in() } != 0 { // Bytes remain in input buffer. Perhaps this is another compressed member in the same archive, or just trailing garbage. // Trailing zero bytes are okay, though, since they are frequently used for padding. let _ = self.reset(); // SAFETY: FFI — state was just re-initialized by reset(). - self.err = unsafe { c::inflate(&raw mut self.state, self.flush) }; + self.err = unsafe { c::inflate(self.state.as_ptr(), self.flush) }; } } pub(crate) fn update_write_result(&self, avail_in: &mut u32, avail_out: &mut u32) { - *avail_in = self.state.avail_in; - *avail_out = self.state.avail_out; + *avail_in = self.state.avail_in(); + *avail_out = self.state.avail_out(); } pub(crate) fn get_error_info(&self) -> Error { match self.err { c::ReturnCode::Ok | c::ReturnCode::BufError => { - if self.state.avail_out != 0 && self.flush == c::FlushValue::Finish { + if self.state.avail_out() != 0 && self.flush == c::FlushValue::Finish { return self.error_for_message(c"unexpected end of file"); } } @@ -571,11 +565,11 @@ impl Context { match self.mode { // SAFETY: FFI — state was initialized as a deflate stream by deflateInit2_. DEFLATE | DEFLATERAW | GZIP => unsafe { - status = c::deflateEnd(&raw mut self.state); + status = c::deflateEnd(self.state.as_ptr()); }, // SAFETY: FFI — state was initialized as an inflate stream by inflateInit2_. INFLATE | INFLATERAW | GUNZIP | UNZIP => unsafe { - status = c::inflateEnd(&raw mut self.state); + status = c::inflateEnd(self.state.as_ptr()); }, NONE => {} BROTLI_ENCODE | BROTLI_DECODE => {} diff --git a/src/zlib/lib.rs b/src/zlib/lib.rs index eb2465dd5767..9debd13b464c 100644 --- a/src/zlib/lib.rs +++ b/src/zlib/lib.rs @@ -1,6 +1,6 @@ // @link "deps/zlib/libz.a" -use core::ffi::{c_char, c_int, c_uint, c_void}; +use core::ffi::{c_char, c_int, c_uint}; use core::mem::size_of; use bun_collections::VecExt as _; @@ -53,7 +53,10 @@ pub use bun_zlib_sys::shared::{z_stream, z_streamp}; pub use bun_zlib_sys::shared::{FlushValue, ReturnCode}; -use bun_zlib_sys::shared::{DataType, zStream_struct}; +use bun_zlib_sys::shared::zStream_struct; + +mod stream; +pub use stream::ZStream; // ZEXTERN int ZEXPORT inflateInit OF((z_streamp strm)); @@ -146,22 +149,10 @@ pub enum ZlibError { bun_core::impl_tag_error!(ZlibError); -pub(crate) use bun_alloc::c_thunks::{ - mi_free_opaque as zlib_mi_free, mi_malloc_items as zlib_mi_malloc, -}; - -// zlib `alloc_func`/`free_func` thunks tagged with the "zlib" heap-breakdown zone. -#[allow(non_snake_case)] -mod ZlibAllocator { - bun_alloc::c_thunks_for_zone!("zlib"); - pub(crate) use malloc_items as alloc; -} - pub struct ZlibReaderArrayList<'a> { // We operate directly through `list_ptr` (a `&'a mut Vec`). pub(crate) list_ptr: &'a mut Vec, - pub(crate) zlib: zStream_struct, - // allocator field dropped (global mimalloc) + pub(crate) zlib: ZStream, pub(crate) state: ZlibReaderArrayListState, /// Decompression-bomb guard: `read_all` errors instead of growing the /// output past this many bytes. Defaults to unbounded. @@ -179,12 +170,12 @@ impl<'a> ZlibReaderArrayList<'a> { // always free with `inflateEnd` if self.state != ZlibReaderArrayListState::End { // SAFETY: zlib was initialized via inflateInit2_; safe to end. - unsafe { inflateEnd(&raw mut self.zlib) }; + unsafe { inflateEnd(self.zlib.as_ptr()) }; self.state = ZlibReaderArrayListState::End; } } - pub fn init(input: &'a [u8], list: &'a mut Vec) -> Result, ZlibError> { + pub fn init(input: &'a [u8], list: &'a mut Vec) -> Result { let options = Options { window_bits: 15 + 32, ..Default::default() @@ -197,79 +188,40 @@ impl<'a> ZlibReaderArrayList<'a> { input: &'a [u8], list: &'a mut Vec, options: Options, - ) -> Result, ZlibError> { - Self::init_with_options_and_list_allocator(input, list, options) - } - - // list_allocator/allocator params dropped (global mimalloc). - pub(crate) fn init_with_options_and_list_allocator( - input: &'a [u8], - list: &'a mut Vec, - options: Options, - ) -> Result, ZlibError> { - let mut zlib_reader = Box::new(Self { + ) -> Result { + let mut zlib_reader = Self { list_ptr: list, - zlib: bun_core::ffi::zeroed(), + zlib: ZStream::new(), state: ZlibReaderArrayListState::Uninitialized, max_output_size: usize::MAX, - }); + }; + zlib_reader + .zlib + .set_input(input.as_ptr(), input.len() as uInt); let list_len = zlib_reader.list_ptr.len(); - zlib_reader.zlib = zStream_struct { - next_in: input.as_ptr(), - avail_in: input.len() as uInt, - total_in: input.len() as _, - - next_out: zlib_reader.list_ptr.as_mut_ptr(), - avail_out: list_len as uInt, - total_out: list_len as _, - - err_msg: core::ptr::null(), - alloc_func: Some(ZlibAllocator::alloc), - free_func: Some(ZlibAllocator::free), - - internal_state: core::ptr::null_mut(), - user_data: (&raw mut *zlib_reader).cast::(), - - data_type: DataType::Unknown as c_int, - adler: 0, - reserved: 0, - }; + zlib_reader + .zlib + .set_output(zlib_reader.list_ptr.as_mut_ptr(), list_len as uInt); // SAFETY: zlib_reader.zlib is fully initialized; version/size match the linked zlib. match unsafe { inflateInit2_( - &raw mut zlib_reader.zlib, + zlib_reader.zlib.as_ptr(), options.window_bits, zlibVersion().cast::(), size_of::() as c_int, ) } { ReturnCode::Ok => Ok(zlib_reader), - ReturnCode::MemError => { - drop(zlib_reader); - Err(ZlibError::OutOfMemory) - } - ReturnCode::StreamError => { - drop(zlib_reader); - Err(ZlibError::InvalidArgument) - } - ReturnCode::VersionError => { - drop(zlib_reader); - Err(ZlibError::InvalidArgument) - } + ReturnCode::MemError => Err(ZlibError::OutOfMemory), + ReturnCode::StreamError | ReturnCode::VersionError => Err(ZlibError::InvalidArgument), _ => unreachable!(), } } pub fn error_message(&self) -> Option<&[u8]> { - if !self.zlib.err_msg.is_null() { - // SAFETY: err_msg is a NUL-terminated C string from zlib. - return Some( - unsafe { bun_core::ffi::cstr(self.zlib.err_msg.cast::()) }.to_bytes(), - ); - } - None + self.zlib.error_message() } pub fn read_all(&mut self, is_done: bool) -> Result<(), ZlibError> { @@ -306,8 +258,8 @@ impl<'a> ZlibReaderArrayList<'a> { // or no more space in the output buffer (see below about the // flush parameter). - if self.zlib.avail_out == 0 { - let produced = self.zlib.total_out as usize; + if self.zlib.avail_out() == 0 { + let produced = self.zlib.total_out() as usize; let remaining_budget = self.max_output_size.saturating_sub(produced); if remaining_budget == 0 { self.state = ZlibReaderArrayListState::Error; @@ -323,14 +275,14 @@ impl<'a> ZlibReaderArrayList<'a> { } // SAFETY: zlib writes the tail; len is truncated to `total_out` before any read. let (next_out, avail_out) = unsafe { self.list_ptr.reserve_expand_tail(0) }; - self.zlib.next_out = next_out; // Clamp so a single inflate call cannot write past `max_output_size`. - self.zlib.avail_out = avail_out.min(remaining_budget) as uInt; + self.zlib + .set_output(next_out, avail_out.min(remaining_budget) as uInt); } // Try to inflate even if avail_in is 0, as this could be a valid empty gzip stream // SAFETY: self.zlib was initialized via inflateInit2_. - let rc = unsafe { inflate(&raw mut self.zlib, FlushValue::NoFlush) }; + let rc = unsafe { inflate(self.zlib.as_ptr(), FlushValue::NoFlush) }; self.state = ZlibReaderArrayListState::Inflating; match rc { @@ -344,7 +296,7 @@ impl<'a> ZlibReaderArrayList<'a> { } ReturnCode::BufError => { // BufError with avail_in == 0 means we need more input data - if self.zlib.avail_in == 0 { + if self.zlib.avail_in() == 0 { if is_done { // Stream is truncated - we're at EOF but decoder needs more data self.state = ZlibReaderArrayListState::Error; @@ -370,7 +322,7 @@ impl<'a> ZlibReaderArrayList<'a> { Ok(()) })(); - let total_out = self.zlib.total_out as usize; + let total_out = self.zlib.total_out() as usize; if self.list_ptr.len() > total_out { self.list_ptr.truncate(total_out); } else if total_out < self.list_ptr.capacity() { @@ -656,8 +608,7 @@ impl NodeMode { pub struct ZlibCompressorArrayList<'a> { // We operate directly through `list_ptr` (a `&'a mut Vec`). pub(crate) list_ptr: &'a mut Vec, - pub(crate) zlib: zStream_struct, - // allocator field dropped (global mimalloc) + pub(crate) zlib: ZStream, pub(crate) state: ZlibCompressorArrayListState, } @@ -665,7 +616,7 @@ impl<'a> ZlibCompressorArrayList<'a> { pub(crate) fn end(&mut self) { if self.state != ZlibCompressorArrayListState::End { // SAFETY: zlib was initialized via deflateInit2_; safe to end. - unsafe { deflateEnd(&raw mut self.zlib) }; + unsafe { deflateEnd(self.zlib.as_ptr()) }; self.state = ZlibCompressorArrayListState::End; } } @@ -674,48 +625,23 @@ impl<'a> ZlibCompressorArrayList<'a> { input: &'a [u8], list: &'a mut Vec, options: Options, - ) -> Result, ZlibError> { - Self::init_with_list_allocator(input, list, options) - } - - // allocator/list_allocator params dropped (global mimalloc). - pub(crate) fn init_with_list_allocator( - input: &'a [u8], - list: &'a mut Vec, - options: Options, - ) -> Result, ZlibError> { - let mut zlib_reader = Box::new(Self { + ) -> Result { + let mut zlib_reader = Self { list_ptr: list, - zlib: bun_core::ffi::zeroed(), + zlib: ZStream::new(), state: ZlibCompressorArrayListState::Uninitialized, - }); - - let list_len = zlib_reader.list_ptr.len(); - zlib_reader.zlib = zStream_struct { - next_in: input.as_ptr(), - avail_in: input.len() as uInt, - total_in: input.len() as _, - - next_out: zlib_reader.list_ptr.as_mut_ptr(), - avail_out: list_len as uInt, - total_out: list_len as _, - - err_msg: core::ptr::null(), - alloc_func: Some(zlib_mi_malloc), - free_func: Some(zlib_mi_free), - - internal_state: core::ptr::null_mut(), - user_data: (&raw mut *zlib_reader).cast::(), - - data_type: DataType::Unknown as c_int, - adler: 0, - reserved: 0, }; + // `read_all` points the output at the list's spare capacity before + // each `deflate` call. + zlib_reader + .zlib + .set_input(input.as_ptr(), input.len() as uInt); + // SAFETY: zlib_reader.zlib is fully initialized; version/size match the linked zlib. match unsafe { deflateInit2_( - &raw mut zlib_reader.zlib, + zlib_reader.zlib.as_ptr(), options.level, options.method, if !options.gzip { @@ -733,7 +659,7 @@ impl<'a> ZlibCompressorArrayList<'a> { // SAFETY: zlib initialized; deflateBound returns upper bound on output. let bound = unsafe { deflateBound( - &raw mut zlib_reader.zlib, + zlib_reader.zlib.as_ptr(), uLong::try_from(input.len()).expect("int cast"), ) }; @@ -742,36 +668,19 @@ impl<'a> ZlibCompressorArrayList<'a> { .try_reserve_exact(bound as usize) .is_err() { - drop(zlib_reader); return Err(ZlibError::OutOfMemory); } Ok(zlib_reader) } - ReturnCode::MemError => { - drop(zlib_reader); - Err(ZlibError::OutOfMemory) - } - ReturnCode::StreamError => { - drop(zlib_reader); - Err(ZlibError::InvalidArgument) - } - ReturnCode::VersionError => { - drop(zlib_reader); - Err(ZlibError::InvalidArgument) - } + ReturnCode::MemError => Err(ZlibError::OutOfMemory), + ReturnCode::StreamError | ReturnCode::VersionError => Err(ZlibError::InvalidArgument), _ => unreachable!(), } } pub fn error_message(&self) -> Option<&[u8]> { - if !self.zlib.err_msg.is_null() { - // SAFETY: err_msg is a NUL-terminated C string from zlib. - return Some( - unsafe { bun_core::ffi::cstr(self.zlib.err_msg.cast::()) }.to_bytes(), - ); - } - None + self.zlib.error_message() } pub fn read_all(&mut self) -> Result<(), ZlibError> { @@ -787,15 +696,15 @@ impl<'a> ZlibCompressorArrayList<'a> { } let spare = self.list_ptr.spare_capacity_mut(); let room = spare.len().min(u32::MAX as usize); - self.zlib.next_out = spare.as_mut_ptr().cast::(); - self.zlib.avail_out = room as uInt; + self.zlib + .set_output(spare.as_mut_ptr().cast::(), room as uInt); // SAFETY: self.zlib was initialized via deflateInit2_; spare is valid for `room` bytes. - let rc = unsafe { deflate(&raw mut self.zlib, FlushValue::Finish) }; + let rc = unsafe { deflate(self.zlib.as_ptr(), FlushValue::Finish) }; self.state = ZlibCompressorArrayListState::Inflating; // `total_out` is 32 bits on Windows. - let produced = room - self.zlib.avail_out as usize; + let produced = room - self.zlib.avail_out() as usize; // SAFETY: zlib has initialized `produced` bytes at the start of spare. unsafe { bun_core::vec::commit_spare(self.list_ptr, produced) }; @@ -838,15 +747,11 @@ impl<'a> Drop for ZlibCompressorArrayList<'a> { // Unlike `Zlib*ArrayList<'a>` above, these hold only the z_stream and its // C-side `internal_state`. Input and output are passed per call, so callers // can hold a decoder across multiple chunks without lifetime erasure. -// -// The `z_stream` is boxed because zlib-ng's `inflate_state`/`deflate_state` -// store a back-pointer to it (checked in `inflateStateCheck` / -// `deflateStateCheck`), so it must not move after init. // ────────────────────────────────────────────────────────────────────────── /// RAII deflate (compression) stream. `deflateEnd` on drop. pub struct DeflateEncoder { - strm: Box, + strm: ZStream, } impl DeflateEncoder { @@ -856,13 +761,13 @@ impl DeflateEncoder { mem_level: c_int, strategy: c_int, ) -> Result { - let mut this = Self { - strm: Box::new(new_zstream()), + let this = Self { + strm: ZStream::new(), }; // SAFETY: strm is fully initialized; version/size match the linked zlib. let rc = unsafe { deflateInit2_( - &raw mut *this.strm, + this.strm.as_ptr(), level, 8, // Z_DEFLATED window_bits, @@ -881,12 +786,12 @@ impl DeflateEncoder { #[inline] pub fn avail_out(&self) -> u32 { - self.strm.avail_out as u32 + self.strm.avail_out() as u32 } pub fn reset(&mut self) -> ReturnCode { // SAFETY: strm was initialized via deflateInit2_. - unsafe { deflateReset(&raw mut *self.strm) } + unsafe { deflateReset(self.strm.as_ptr()) } } /// One `deflate()` call writing into `out`'s spare capacity. @@ -921,13 +826,13 @@ impl Drop for DeflateEncoder { // SAFETY: strm was initialized via deflateInit2_ (or init failed and // `internal_state` is null — `deflateEnd` on a null state is a // defined no-op returning Z_STREAM_ERROR). Freed exactly once here. - unsafe { deflateEnd(&raw mut *self.strm) }; + unsafe { deflateEnd(self.strm.as_ptr()) }; } } /// RAII inflate (decompression) stream. `inflateEnd` on drop. pub struct InflateDecoder { - strm: Box, + strm: ZStream, pub(crate) state: State, /// Decompression-bomb guard for [`decompress`](Self::decompress). pub(crate) max_output_size: usize, @@ -940,8 +845,8 @@ pub struct InflateDecoder { impl InflateDecoder { pub fn new(window_bits: c_int) -> Result { - let mut this = Self { - strm: Box::new(new_zstream()), + let this = Self { + strm: ZStream::new(), state: State::Uninitialized, max_output_size: usize::MAX, // `MAX_WBITS | 16` selects gzip-only decode (zlib manual). @@ -950,7 +855,7 @@ impl InflateDecoder { // SAFETY: strm is fully initialized; version/size match the linked zlib. let rc = unsafe { inflateInit2_( - &raw mut *this.strm, + this.strm.as_ptr(), window_bits, zlibVersion().cast::(), size_of::() as c_int, @@ -965,12 +870,12 @@ impl InflateDecoder { #[inline] pub fn avail_out(&self) -> u32 { - self.strm.avail_out as u32 + self.strm.avail_out() as u32 } pub fn reset(&mut self) -> ReturnCode { // SAFETY: strm was initialized via inflateInit2_. - let rc = unsafe { inflateReset(&raw mut *self.strm) }; + let rc = unsafe { inflateReset(self.strm.as_ptr()) }; if rc == ReturnCode::Ok { self.state = State::Uninitialized; } @@ -1081,7 +986,7 @@ impl InflateDecoder { return Err(ZlibError::OutOfMemory); } ReturnCode::BufError => { - if input.is_empty() && self.strm.avail_in == 0 { + if input.is_empty() && self.strm.avail_in() == 0 { if is_done { self.state = State::Error; return Err(ZlibError::ZlibError); @@ -1114,32 +1019,13 @@ impl Drop for InflateDecoder { // defined no-op returning Z_STREAM_ERROR). Freed exactly once here. // `state == End` only signals stream completion, not that the C // state was already freed. - unsafe { inflateEnd(&raw mut *self.strm) }; - } -} - -fn new_zstream() -> zStream_struct { - zStream_struct { - next_in: core::ptr::null(), - avail_in: 0, - total_in: 0, - next_out: core::ptr::null_mut(), - avail_out: 0, - total_out: 0, - err_msg: core::ptr::null(), - alloc_func: Some(ZlibAllocator::alloc), - free_func: Some(ZlibAllocator::free), - internal_state: core::ptr::null_mut(), - user_data: core::ptr::null_mut(), - data_type: DataType::Unknown as c_int, - adler: 0, - reserved: 0, + unsafe { inflateEnd(self.strm.as_ptr()) }; } } /// Shared body of [`DeflateEncoder::step`] / [`InflateDecoder::step`]. fn step( - strm: &mut zStream_struct, + strm: &mut ZStream, input: &[u8], out: &mut Vec, reserve: usize, @@ -1152,22 +1038,20 @@ fn step( } let in_len = input.len().min(u32::MAX as usize); - strm.next_in = input.as_ptr(); - strm.avail_in = in_len as uInt; + strm.set_input(input.as_ptr(), in_len as uInt); let spare = out.spare_capacity_mut(); let out_len = spare.len().min(limit).min(u32::MAX as usize); - strm.next_out = spare.as_mut_ptr().cast::(); - strm.avail_out = out_len as uInt; + strm.set_output(spare.as_mut_ptr().cast::(), out_len as uInt); // SAFETY: strm was initialized by deflateInit2_/inflateInit2_; input is // valid for `in_len` bytes; spare is valid write-only storage for // `out_len` bytes. zlib writes at most `out_len - avail_out` bytes. - let rc = unsafe { op(&raw mut *strm, flush) }; + let rc = unsafe { op(strm.as_ptr(), flush) }; - let produced = out_len - strm.avail_out as usize; + let produced = out_len - strm.avail_out() as usize; // SAFETY: zlib has initialized `produced` bytes at the start of spare. unsafe { bun_core::vec::commit_spare(out, produced) }; - let consumed = in_len - strm.avail_in as usize; + let consumed = in_len - strm.avail_in() as usize; (consumed, rc) } diff --git a/src/zlib/stream.rs b/src/zlib/stream.rs new file mode 100644 index 000000000000..cb4bce7b2f69 --- /dev/null +++ b/src/zlib/stream.rs @@ -0,0 +1,145 @@ +//! Owned `z_stream` that Rust reaches only through a raw pointer. +//! +//! zlib-ng keeps a back-pointer to the `z_stream` in its internal state from +//! init on (`deflate.c`: `s->strm = strm`, `inflate.c`: `state->strm = strm`) +//! and reads and writes `avail_in`, `next_out`, `avail_out`, `total_out` +//! through it inside every later call. A `&mut zStream_struct` (or a `&mut` +//! to a struct that holds one inline) that is alive across such a call makes +//! that access a foreign access to a protected borrow, which is undefined +//! behaviour under both Stacked Borrows and Tree Borrows, and lets LLVM treat +//! the parameter as `noalias` under cross-language LTO. rust-lang/flate2-rs#392 +//! is the same bug against the same library. +//! +//! [`ZStream`] is the one place every stream owner goes through. It never +//! hands out a reference to the stream: field access is a read or write +//! through the stored pointer, and the C calls take [`ZStream::as_ptr`]. + +use core::ffi::{c_char, c_int}; +use core::ptr::NonNull; + +use bun_zlib_sys::shared::{DataType, uInt, uLong, zStream_struct}; + +#[allow(non_snake_case)] +mod ZlibAllocator { + bun_alloc::c_thunks_for_zone!("zlib"); + pub(crate) use malloc_items as alloc; +} + +/// Heap-pinned `z_stream`. The owner calls `deflateEnd`/`inflateEnd` on +/// [`as_ptr`](Self::as_ptr) before this drops. `Drop` frees only the +/// `z_stream` allocation itself. +pub struct ZStream { + ptr: NonNull, +} + +impl Default for ZStream { + fn default() -> Self { + Self::new() + } +} + +impl ZStream { + /// A zeroed stream with Bun's zlib allocator installed. Ready for + /// `deflateInit2_`/`inflateInit2_`. + pub fn new() -> Self { + let ptr = bun_core::heap::alloc_nn(zStream_struct { + next_in: core::ptr::null(), + avail_in: 0, + total_in: 0, + next_out: core::ptr::null_mut(), + avail_out: 0, + total_out: 0, + err_msg: core::ptr::null(), + alloc_func: Some(ZlibAllocator::alloc), + free_func: Some(ZlibAllocator::free), + internal_state: core::ptr::null_mut(), + user_data: core::ptr::null_mut(), + data_type: DataType::Unknown as c_int, + adler: 0, + reserved: 0, + }); + Self { ptr } + } + + /// The pointer to hand to zlib. Every call into zlib for this stream + /// uses this same pointer, so the back-pointer zlib stores at init stays + /// valid for the life of the stream. + #[inline] + pub fn as_ptr(&self) -> *mut zStream_struct { + self.ptr.as_ptr() + } + + #[inline] + pub fn next_in(&self) -> *const u8 { + // SAFETY: `ptr` is a live allocation owned by `self`. No reference to + // the stream exists, so a read through the raw pointer is the only + // Rust-side access. + unsafe { (*self.as_ptr()).next_in } + } + + #[inline] + pub fn avail_in(&self) -> uInt { + // SAFETY: see `next_in`. + unsafe { (*self.as_ptr()).avail_in } + } + + #[inline] + pub fn avail_out(&self) -> uInt { + // SAFETY: see `next_in`. + unsafe { (*self.as_ptr()).avail_out } + } + + #[inline] + pub fn total_out(&self) -> uLong { + // SAFETY: see `next_in`. + unsafe { (*self.as_ptr()).total_out } + } + + #[inline] + pub fn err_msg(&self) -> *const c_char { + // SAFETY: see `next_in`. + unsafe { (*self.as_ptr()).err_msg } + } + + /// `msg` as bytes, if zlib set one. zlib only ever points `msg` at a + /// static string. + pub fn error_message(&self) -> Option<&[u8]> { + let msg = self.err_msg(); + if msg.is_null() { + return None; + } + // SAFETY: zlib sets `msg` to a NUL-terminated static C string. + Some(unsafe { bun_core::ffi::cstr(msg) }.to_bytes()) + } + + /// Point `next_in`/`avail_in` at the next input chunk. `avail_in` is + /// 32 bits; the caller clamps and loops. + #[inline] + pub fn set_input(&mut self, next_in: *const u8, avail_in: uInt) { + // SAFETY: see `next_in`. The write goes through the raw pointer, so + // it does not retag the stream. + unsafe { + (*self.as_ptr()).next_in = next_in; + (*self.as_ptr()).avail_in = avail_in; + } + } + + /// Point `next_out`/`avail_out` at the next output window. + #[inline] + pub fn set_output(&mut self, next_out: *mut u8, avail_out: uInt) { + // SAFETY: see `set_input`. + unsafe { + (*self.as_ptr()).next_out = next_out; + (*self.as_ptr()).avail_out = avail_out; + } + } +} + +impl Drop for ZStream { + fn drop(&mut self) { + // SAFETY: `ptr` came from `heap::alloc` in `new` and is freed once, + // here. The owner has already ended the zlib state, so zlib holds no + // live pointer into it. + unsafe { bun_core::heap::destroy(self.ptr.as_ptr()) }; + } +} From eecf281ee8ff9fdcb308515f9a62ba1f3ef394f5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:34:30 +0000 Subject: [PATCH 2/5] zlib: move the CompressionStream coder onto ZStream and drop the dead allocator thunks CompressionStreamCoder still held Box. It now holds ZStream like every other owner. The untagged zlib allocator thunks in bun_alloc lost their last user, so remove them. --- src/bun_alloc/c_thunks.rs | 19 ++----- src/runtime/webcore/CompressionStreamCoder.rs | 52 +++++++++---------- src/zlib/stream.rs | 2 +- 3 files changed, 29 insertions(+), 44 deletions(-) diff --git a/src/bun_alloc/c_thunks.rs b/src/bun_alloc/c_thunks.rs index e01d1aab3314..c787f7b3b9aa 100644 --- a/src/bun_alloc/c_thunks.rs +++ b/src/bun_alloc/c_thunks.rs @@ -6,10 +6,11 @@ //! | brotli `brotli_alloc/free_func` | `(opaque, size: usize)` | `(opaque, ptr)` | //! | JSC `JSTypedArrayBytesDeallocator` | — | `(bytes, ctx)` | //! -//! The plain (non-zone-tagged) variants are free functions below; the -//! zone-tagged variants are minted per-label by [`c_thunks_for_zone!`]. +//! The plain (non-zone-tagged) JSC variant is a free function below; the +//! zone-tagged zlib and brotli variants are minted per-label by +//! [`c_thunks_for_zone!`]. -use core::ffi::{c_uint, c_void}; +use core::ffi::c_void; use crate::default_alloc as raw; @@ -17,18 +18,6 @@ use crate::default_alloc as raw; // Plain default-allocator thunks (no heap-breakdown tagging) // ────────────────────────────────────────────────────────────────────────── -/// zlib `alloc_func` → default allocator `malloc(items * size)` (non-zeroing). -/// Null on failure, which zlib reports as `Z_MEM_ERROR`. -pub extern "C" fn mi_malloc_items(_: *mut c_void, items: c_uint, size: c_uint) -> *mut c_void { - raw::malloc(items as usize * size as usize) -} - -/// `(opaque, ptr)` → default allocator `free(ptr)`; opaque cookie ignored. -pub unsafe extern "C" fn mi_free_opaque(_: *mut c_void, ptr: *mut c_void) { - // SAFETY: ptr was allocated by the default allocator (or is null). - unsafe { raw::free(ptr) }; -} - /// JSC `JSTypedArrayBytesDeallocator` → default allocator `free(bytes)`; `ctx` ignored. pub unsafe extern "C" fn mi_free_bytes(bytes: *mut c_void, _ctx: *mut c_void) { // SAFETY: bytes was allocated by the default allocator (or is null). diff --git a/src/runtime/webcore/CompressionStreamCoder.rs b/src/runtime/webcore/CompressionStreamCoder.rs index e368629302d5..24fb65645268 100644 --- a/src/runtime/webcore/CompressionStreamCoder.rs +++ b/src/runtime/webcore/CompressionStreamCoder.rs @@ -102,9 +102,9 @@ enum Progress { } enum Backend { - Deflate(Box), + Deflate(zlib::ZStream), Inflate { - state: Box, + state: zlib::ZStream, /// Gzip only: after the first member ends, any further bytes must be /// another gzip member (RFC 1952 §2.2) — the decoder resets and /// continues. Deflate/deflate-raw have no such concatenation, so @@ -159,10 +159,10 @@ impl Drop for CompressionStreamCoder { unsafe { match &mut self.backend { Backend::Deflate(s) => { - zlib::deflateEnd(&raw mut **s); + zlib::deflateEnd(s.as_ptr()); } Backend::Inflate { state, .. } => { - zlib::inflateEnd(&raw mut **state); + zlib::inflateEnd(state.as_ptr()); } Backend::BrotliEncode(p) => brotli::BrotliEncoderDestroyInstance(p.as_ptr()), Backend::BrotliDecode(p) => brotli::BrotliDecoderDestroyInstance(p.as_ptr()), @@ -197,13 +197,13 @@ impl CompressionStreamCoder { ) -> Result, CodecError> { let backend = match (format, decompress) { (Format::Deflate | Format::DeflateRaw | Format::Gzip, false) => { - let mut s = Box::new(bun_core::ffi::zeroed::()); + let s = zlib::ZStream::new(); // Spec: "default compression level". Z_DEFAULT_COMPRESSION = -1. // SAFETY: `s` is a zeroed, #[repr(C)] z_stream; zlibVersion() is // a static C string. let rc = unsafe { zlib::deflateInit2_( - &raw mut *s, + s.as_ptr(), level.unwrap_or(-1), 8, // Z_DEFLATED format.window_bits(), @@ -219,11 +219,11 @@ impl CompressionStreamCoder { Backend::Deflate(s) } (Format::Deflate | Format::DeflateRaw | Format::Gzip, true) => { - let mut s = Box::new(bun_core::ffi::zeroed::()); + let s = zlib::ZStream::new(); // SAFETY: as above. let rc = unsafe { zlib::inflateInit2_( - &raw mut *s, + s.as_ptr(), format.window_bits(), zlib::zlibVersion().cast(), core::mem::size_of::() as c_int, @@ -392,20 +392,18 @@ impl CompressionStreamCoder { } else { zlib::FlushValue::NoFlush }; - s.next_in = remaining.as_ptr(); - s.avail_in = take as u32; + s.set_input(remaining.as_ptr(), take as u32); let spare = spare(out, cap)?; - s.next_out = spare.as_mut_ptr().cast(); - s.avail_out = spare.len().min(u32::MAX as usize) as u32; - let before = s.avail_out; + let before = spare.len().min(u32::MAX as usize) as u32; + s.set_output(spare.as_mut_ptr().cast(), before); // SAFETY: `s` was initialized by `deflateInit2_`; next_in/ // avail_in borrow `remaining`, next_out/avail_out borrow // the Vec's spare capacity for this one call. - let rc = unsafe { zlib::deflate(&raw mut **s, flush) }; - let written = (before - s.avail_out) as usize; + let rc = unsafe { zlib::deflate(s.as_ptr(), flush) }; + let written = (before - s.avail_out()) as usize; // SAFETY: deflate wrote exactly `written` bytes. unsafe { out.set_len(out.len() + written) }; - let consumed = take - s.avail_in as usize; + let consumed = take - s.avail_in() as usize; remaining = &remaining[consumed..]; match rc { zlib::ReturnCode::Ok | zlib::ReturnCode::BufError => {} @@ -413,7 +411,7 @@ impl CompressionStreamCoder { zlib::ReturnCode::MemError => return Err(CodecError::OutOfMemory), _ => return Err(CodecError::Message("deflate failed")), } - if s.avail_out != 0 && remaining.is_empty() { + if s.avail_out() != 0 && remaining.is_empty() { return Ok(Progress::Done); } } @@ -429,7 +427,7 @@ impl CompressionStreamCoder { return Err(CodecError::TrailingJunk); } // SAFETY: `s` is an initialized inflate stream. - if unsafe { zlib::inflateReset(&raw mut **s) } != zlib::ReturnCode::Ok { + if unsafe { zlib::inflateReset(s.as_ptr()) } != zlib::ReturnCode::Ok { return Err(CodecError::Message("inflate failed")); } self.ended = false; @@ -452,19 +450,17 @@ impl CompressionStreamCoder { } else { zlib::FlushValue::NoFlush }; - s.next_in = remaining.as_ptr(); - s.avail_in = take as u32; + s.set_input(remaining.as_ptr(), take as u32); let spare = spare(out, cap)?; - s.next_out = spare.as_mut_ptr().cast(); - s.avail_out = spare.len().min(u32::MAX as usize) as u32; - let before = s.avail_out; + let before = spare.len().min(u32::MAX as usize) as u32; + s.set_output(spare.as_mut_ptr().cast(), before); // SAFETY: `s` was initialized by `inflateInit2_`; buffers // as in the deflate arm. - let rc = unsafe { zlib::inflate(&raw mut **s, flush) }; - let written = (before - s.avail_out) as usize; + let rc = unsafe { zlib::inflate(s.as_ptr(), flush) }; + let written = (before - s.avail_out()) as usize; // SAFETY: inflate wrote exactly `written` bytes. unsafe { out.set_len(out.len() + written) }; - let consumed = take - s.avail_in as usize; + let consumed = take - s.avail_in() as usize; remaining = &remaining[consumed..]; match rc { zlib::ReturnCode::Ok => {} @@ -482,7 +478,7 @@ impl CompressionStreamCoder { return Err(CodecError::TrailingJunk); } // SAFETY: `s` is an initialized inflate stream. - if unsafe { zlib::inflateReset(&raw mut **s) } != zlib::ReturnCode::Ok { + if unsafe { zlib::inflateReset(s.as_ptr()) } != zlib::ReturnCode::Ok { return Err(CodecError::Message("inflate failed")); } self.ended = false; @@ -494,7 +490,7 @@ impl CompressionStreamCoder { zlib::ReturnCode::MemError => return Err(CodecError::OutOfMemory), _ => return Err(CodecError::Message("inflate failed")), } - if s.avail_out != 0 && remaining.is_empty() { + if s.avail_out() != 0 && remaining.is_empty() { if finish && !self.ended { return Err(CodecError::Message("unexpected end of file")); } diff --git a/src/zlib/stream.rs b/src/zlib/stream.rs index cb4bce7b2f69..deffce5877e0 100644 --- a/src/zlib/stream.rs +++ b/src/zlib/stream.rs @@ -137,7 +137,7 @@ impl ZStream { impl Drop for ZStream { fn drop(&mut self) { - // SAFETY: `ptr` came from `heap::alloc` in `new` and is freed once, + // SAFETY: `ptr` came from `heap::alloc_nn` in `new` and is freed once, // here. The owner has already ended the zlib state, so zlib holds no // live pointer into it. unsafe { bun_core::heap::destroy(self.ptr.as_ptr()) }; From 479c35bb402d5c533d8a6673cacc043a7d2052e8 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:04:05 +0000 Subject: [PATCH 3/5] zlib: init through ZStream and stop re-exporting the raw z_stream ZStream::deflate_init and ZStream::inflate_init carry the zlib version and struct size, so no owner names zStream_struct any more. bun_zlib no longer re-exports z_stream. Shorter comments in stream.rs. --- src/bun_alloc/c_thunks.rs | 3 +- src/runtime/node/zlib/NativeZlib.rs | 31 ++----- src/runtime/webcore/CompressionStreamCoder.rs | 35 +++----- src/zlib/lib.rs | 77 +++++------------ src/zlib/stream.rs | 85 ++++++++++++------- 5 files changed, 92 insertions(+), 139 deletions(-) diff --git a/src/bun_alloc/c_thunks.rs b/src/bun_alloc/c_thunks.rs index c787f7b3b9aa..a08c7c9d2bcc 100644 --- a/src/bun_alloc/c_thunks.rs +++ b/src/bun_alloc/c_thunks.rs @@ -6,8 +6,7 @@ //! | brotli `brotli_alloc/free_func` | `(opaque, size: usize)` | `(opaque, ptr)` | //! | JSC `JSTypedArrayBytesDeallocator` | — | `(bytes, ctx)` | //! -//! The plain (non-zone-tagged) JSC variant is a free function below; the -//! zone-tagged zlib and brotli variants are minted per-label by +//! The zone-tagged zlib and brotli variants are minted per-label by //! [`c_thunks_for_zone!`]. use core::ffi::c_void; diff --git a/src/runtime/node/zlib/NativeZlib.rs b/src/runtime/node/zlib/NativeZlib.rs index 2479d45c553b..68650ba4e5c3 100644 --- a/src/runtime/node/zlib/NativeZlib.rs +++ b/src/runtime/node/zlib/NativeZlib.rs @@ -1,5 +1,4 @@ use core::ffi::{c_char, c_int}; -use core::mem; use bun_zlib as c; // bun.zlib — C zlib FFI (NodeMode, z_stream, ReturnCode, FlushValue, deflate*, inflate*) @@ -272,28 +271,14 @@ impl Context { match self.mode { NONE => unreachable!(), - // SAFETY: FFI — `state` is a valid #[repr(C)] z_stream; zlibVersion() returns a static C string. - DEFLATE | GZIP | DEFLATERAW => unsafe { - self.err = c::deflateInit2_( - self.state.as_ptr(), - level, - 8, - window_bits_actual, - mem_level, - strategy, - c::zlibVersion().cast(), - c_int::try_from(mem::size_of::()).expect("int cast"), - ); - }, - // SAFETY: FFI — `state` is a valid #[repr(C)] z_stream; zlibVersion() returns a static C string. - INFLATE | GUNZIP | UNZIP | INFLATERAW => unsafe { - self.err = c::inflateInit2_( - self.state.as_ptr(), - window_bits_actual, - c::zlibVersion().cast(), - c_int::try_from(mem::size_of::()).expect("int cast"), - ); - }, + DEFLATE | GZIP | DEFLATERAW => { + self.err = + self.state + .deflate_init(level, 8, window_bits_actual, mem_level, strategy); + } + INFLATE | GUNZIP | UNZIP | INFLATERAW => { + self.err = self.state.inflate_init(window_bits_actual); + } BROTLI_DECODE | BROTLI_ENCODE => unreachable!(), ZSTD_COMPRESS | ZSTD_DECOMPRESS => unreachable!(), } diff --git a/src/runtime/webcore/CompressionStreamCoder.rs b/src/runtime/webcore/CompressionStreamCoder.rs index 24fb65645268..bf37f9c1d6d4 100644 --- a/src/runtime/webcore/CompressionStreamCoder.rs +++ b/src/runtime/webcore/CompressionStreamCoder.rs @@ -197,38 +197,23 @@ impl CompressionStreamCoder { ) -> Result, CodecError> { let backend = match (format, decompress) { (Format::Deflate | Format::DeflateRaw | Format::Gzip, false) => { - let s = zlib::ZStream::new(); + let mut s = zlib::ZStream::new(); // Spec: "default compression level". Z_DEFAULT_COMPRESSION = -1. - // SAFETY: `s` is a zeroed, #[repr(C)] z_stream; zlibVersion() is - // a static C string. - let rc = unsafe { - zlib::deflateInit2_( - s.as_ptr(), - level.unwrap_or(-1), - 8, // Z_DEFLATED - format.window_bits(), - 8, // default mem_level - 0, // Z_DEFAULT_STRATEGY - zlib::zlibVersion().cast(), - core::mem::size_of::() as c_int, - ) - }; + let rc = s.deflate_init( + level.unwrap_or(-1), + 8, // Z_DEFLATED + format.window_bits(), + 8, // default mem_level + 0, // Z_DEFAULT_STRATEGY + ); if rc != zlib::ReturnCode::Ok { return Err(CodecError::Message("failed to initialize deflate")); } Backend::Deflate(s) } (Format::Deflate | Format::DeflateRaw | Format::Gzip, true) => { - let s = zlib::ZStream::new(); - // SAFETY: as above. - let rc = unsafe { - zlib::inflateInit2_( - s.as_ptr(), - format.window_bits(), - zlib::zlibVersion().cast(), - core::mem::size_of::() as c_int, - ) - }; + let mut s = zlib::ZStream::new(); + let rc = s.inflate_init(format.window_bits()); if rc != zlib::ReturnCode::Ok { return Err(CodecError::Message("failed to initialize inflate")); } diff --git a/src/zlib/lib.rs b/src/zlib/lib.rs index 9debd13b464c..b9aee44c945b 100644 --- a/src/zlib/lib.rs +++ b/src/zlib/lib.rs @@ -1,7 +1,6 @@ // @link "deps/zlib/libz.a" use core::ffi::{c_char, c_int, c_uint}; -use core::mem::size_of; use bun_collections::VecExt as _; @@ -27,7 +26,7 @@ pub use bun_zlib_sys::shared::{Bytef, uInt, uLong, uLongf}; // typedef voidpf (*alloc_func) OF((voidpf opaque, uInt items, uInt size)); // typedef void (*free_func) OF((voidpf opaque, voidpf address)); -pub use bun_zlib_sys::shared::{z_stream, z_streamp}; +use bun_zlib_sys::shared::z_streamp; // typedef struct z_stream_s { // z_const Bytef *next_in; /* next input byte */ @@ -204,15 +203,7 @@ impl<'a> ZlibReaderArrayList<'a> { .zlib .set_output(zlib_reader.list_ptr.as_mut_ptr(), list_len as uInt); - // SAFETY: zlib_reader.zlib is fully initialized; version/size match the linked zlib. - match unsafe { - inflateInit2_( - zlib_reader.zlib.as_ptr(), - options.window_bits, - zlibVersion().cast::(), - size_of::() as c_int, - ) - } { + match zlib_reader.zlib.inflate_init(options.window_bits) { ReturnCode::Ok => Ok(zlib_reader), ReturnCode::MemError => Err(ZlibError::OutOfMemory), ReturnCode::StreamError | ReturnCode::VersionError => Err(ZlibError::InvalidArgument), @@ -632,29 +623,21 @@ impl<'a> ZlibCompressorArrayList<'a> { state: ZlibCompressorArrayListState::Uninitialized, }; - // `read_all` points the output at the list's spare capacity before - // each `deflate` call. zlib_reader .zlib .set_input(input.as_ptr(), input.len() as uInt); - // SAFETY: zlib_reader.zlib is fully initialized; version/size match the linked zlib. - match unsafe { - deflateInit2_( - zlib_reader.zlib.as_ptr(), - options.level, - options.method, - if !options.gzip { - -options.window_bits - } else { - options.window_bits + 16 - }, - options.mem_level, - options.strategy, - zlibVersion().cast::(), - size_of::() as c_int, - ) - } { + match zlib_reader.zlib.deflate_init( + options.level, + options.method, + if !options.gzip { + -options.window_bits + } else { + options.window_bits + 16 + }, + options.mem_level, + options.strategy, + ) { ReturnCode::Ok => { // SAFETY: zlib initialized; deflateBound returns upper bound on output. let bound = unsafe { @@ -761,23 +744,14 @@ impl DeflateEncoder { mem_level: c_int, strategy: c_int, ) -> Result { - let this = Self { + let mut this = Self { strm: ZStream::new(), }; - // SAFETY: strm is fully initialized; version/size match the linked zlib. - let rc = unsafe { - deflateInit2_( - this.strm.as_ptr(), - level, - 8, // Z_DEFLATED - window_bits, - mem_level, - strategy, - zlibVersion().cast::(), - size_of::() as c_int, - ) - }; - match rc { + // 8 = Z_DEFLATED + match this + .strm + .deflate_init(level, 8, window_bits, mem_level, strategy) + { ReturnCode::Ok => Ok(this), ReturnCode::MemError => Err(ZlibError::OutOfMemory), _ => Err(ZlibError::InvalidArgument), @@ -845,23 +819,14 @@ pub struct InflateDecoder { impl InflateDecoder { pub fn new(window_bits: c_int) -> Result { - let this = Self { + let mut this = Self { strm: ZStream::new(), state: State::Uninitialized, max_output_size: usize::MAX, // `MAX_WBITS | 16` selects gzip-only decode (zlib manual). multi_member: (16..32).contains(&window_bits), }; - // SAFETY: strm is fully initialized; version/size match the linked zlib. - let rc = unsafe { - inflateInit2_( - this.strm.as_ptr(), - window_bits, - zlibVersion().cast::(), - size_of::() as c_int, - ) - }; - match rc { + match this.strm.inflate_init(window_bits) { ReturnCode::Ok => Ok(this), ReturnCode::MemError => Err(ZlibError::OutOfMemory), _ => Err(ZlibError::InvalidArgument), diff --git a/src/zlib/stream.rs b/src/zlib/stream.rs index deffce5877e0..2bdae2594b5c 100644 --- a/src/zlib/stream.rs +++ b/src/zlib/stream.rs @@ -1,23 +1,15 @@ //! Owned `z_stream` that Rust reaches only through a raw pointer. //! -//! zlib-ng keeps a back-pointer to the `z_stream` in its internal state from -//! init on (`deflate.c`: `s->strm = strm`, `inflate.c`: `state->strm = strm`) -//! and reads and writes `avail_in`, `next_out`, `avail_out`, `total_out` -//! through it inside every later call. A `&mut zStream_struct` (or a `&mut` -//! to a struct that holds one inline) that is alive across such a call makes -//! that access a foreign access to a protected borrow, which is undefined -//! behaviour under both Stacked Borrows and Tree Borrows, and lets LLVM treat -//! the parameter as `noalias` under cross-language LTO. rust-lang/flate2-rs#392 -//! is the same bug against the same library. -//! -//! [`ZStream`] is the one place every stream owner goes through. It never -//! hands out a reference to the stream: field access is a read or write -//! through the stored pointer, and the C calls take [`ZStream::as_ptr`]. +//! zlib-ng stores a pointer to the `z_stream` at init and accesses the stream +//! through it in every later call. A `&mut` to the stream (or to a struct that +//! holds it inline) held across that call is undefined behaviour (aliasing). +//! `ZStream` never hands out a reference to the stream. use core::ffi::{c_char, c_int}; +use core::mem::size_of; use core::ptr::NonNull; -use bun_zlib_sys::shared::{DataType, uInt, uLong, zStream_struct}; +use bun_zlib_sys::shared::{DataType, ReturnCode, uInt, uLong, zStream_struct}; #[allow(non_snake_case)] mod ZlibAllocator { @@ -25,9 +17,7 @@ mod ZlibAllocator { pub(crate) use malloc_items as alloc; } -/// Heap-pinned `z_stream`. The owner calls `deflateEnd`/`inflateEnd` on -/// [`as_ptr`](Self::as_ptr) before this drops. `Drop` frees only the -/// `z_stream` allocation itself. +/// Heap-pinned `z_stream`. The owner ends the zlib state before this drops. pub struct ZStream { ptr: NonNull, } @@ -39,8 +29,7 @@ impl Default for ZStream { } impl ZStream { - /// A zeroed stream with Bun's zlib allocator installed. Ready for - /// `deflateInit2_`/`inflateInit2_`. + /// A zeroed stream with Bun's zlib allocator installed. pub fn new() -> Self { let ptr = bun_core::heap::alloc_nn(zStream_struct { next_in: core::ptr::null(), @@ -61,19 +50,53 @@ impl ZStream { Self { ptr } } - /// The pointer to hand to zlib. Every call into zlib for this stream - /// uses this same pointer, so the back-pointer zlib stores at init stays - /// valid for the life of the stream. + /// The pointer to hand to zlib. #[inline] pub fn as_ptr(&self) -> *mut zStream_struct { self.ptr.as_ptr() } + /// `deflateInit2_` against the linked zlib's version and struct size. + pub fn deflate_init( + &mut self, + level: c_int, + method: c_int, + window_bits: c_int, + mem_level: c_int, + strategy: c_int, + ) -> ReturnCode { + // SAFETY: `ptr` is a live z_stream; version/size match the linked zlib. + unsafe { + crate::deflateInit2_( + self.as_ptr(), + level, + method, + window_bits, + mem_level, + strategy, + crate::zlibVersion().cast::(), + size_of::() as c_int, + ) + } + } + + /// `inflateInit2_` against the linked zlib's version and struct size. + pub fn inflate_init(&mut self, window_bits: c_int) -> ReturnCode { + // SAFETY: `ptr` is a live z_stream; version/size match the linked zlib. + unsafe { + crate::inflateInit2_( + self.as_ptr(), + window_bits, + crate::zlibVersion().cast::(), + size_of::() as c_int, + ) + } + } + #[inline] pub fn next_in(&self) -> *const u8 { - // SAFETY: `ptr` is a live allocation owned by `self`. No reference to - // the stream exists, so a read through the raw pointer is the only - // Rust-side access. + // SAFETY: `ptr` is a live allocation owned by `self`, read through + // the raw pointer only. unsafe { (*self.as_ptr()).next_in } } @@ -101,8 +124,7 @@ impl ZStream { unsafe { (*self.as_ptr()).err_msg } } - /// `msg` as bytes, if zlib set one. zlib only ever points `msg` at a - /// static string. + /// `msg` as bytes, if zlib set one. pub fn error_message(&self) -> Option<&[u8]> { let msg = self.err_msg(); if msg.is_null() { @@ -112,12 +134,10 @@ impl ZStream { Some(unsafe { bun_core::ffi::cstr(msg) }.to_bytes()) } - /// Point `next_in`/`avail_in` at the next input chunk. `avail_in` is - /// 32 bits; the caller clamps and loops. + /// Point `next_in`/`avail_in` at the next input chunk. #[inline] pub fn set_input(&mut self, next_in: *const u8, avail_in: uInt) { - // SAFETY: see `next_in`. The write goes through the raw pointer, so - // it does not retag the stream. + // SAFETY: see `next_in`. unsafe { (*self.as_ptr()).next_in = next_in; (*self.as_ptr()).avail_in = avail_in; @@ -138,8 +158,7 @@ impl ZStream { impl Drop for ZStream { fn drop(&mut self) { // SAFETY: `ptr` came from `heap::alloc_nn` in `new` and is freed once, - // here. The owner has already ended the zlib state, so zlib holds no - // live pointer into it. + // here, after the owner ended the zlib state. unsafe { bun_core::heap::destroy(self.ptr.as_ptr()) }; } } From fe9967937d62b7ab79892a1a2b54522f2edfac35 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:05:15 +0000 Subject: [PATCH 4/5] zlib: shorter module doc on stream.rs --- src/zlib/stream.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/zlib/stream.rs b/src/zlib/stream.rs index 2bdae2594b5c..e95753574f67 100644 --- a/src/zlib/stream.rs +++ b/src/zlib/stream.rs @@ -1,9 +1,6 @@ -//! Owned `z_stream` that Rust reaches only through a raw pointer. -//! -//! zlib-ng stores a pointer to the `z_stream` at init and accesses the stream -//! through it in every later call. A `&mut` to the stream (or to a struct that -//! holds it inline) held across that call is undefined behaviour (aliasing). -//! `ZStream` never hands out a reference to the stream. +//! Owned `z_stream` reached only through a raw pointer: zlib-ng keeps its own +//! pointer to the stream from init on, so a `&mut` to it across a zlib call +//! is an aliasing violation (oven-sh/bun#46598). use core::ffi::{c_char, c_int}; use core::mem::size_of; From 4c243d981509d168c95493feb49e013bca884528 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:16:27 +0000 Subject: [PATCH 5/5] bun_alloc: give the zone thunks one malloc zone for alloc and free c_thunks_for_zone! expanded get_zone! once in malloc_size and once in free. Each expansion is its own static, so on a macOS debug build without ASAN the free thunk used a different zone than the alloc thunk. --- src/bun_alloc/c_thunks.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/bun_alloc/c_thunks.rs b/src/bun_alloc/c_thunks.rs index a08c7c9d2bcc..b64aca8e32db 100644 --- a/src/bun_alloc/c_thunks.rs +++ b/src/bun_alloc/c_thunks.rs @@ -43,12 +43,18 @@ pub unsafe extern "C" fn mi_free_bytes(bytes: *mut c_void, _ctx: *mut c_void) { #[macro_export] macro_rules! c_thunks_for_zone { ($name:literal) => { + // One zone for the alloc and free thunks. `get_zone!` expands to its + // own `static` per use, so it must appear once. + fn zone() -> &'static $crate::heap_breakdown::Zone { + $crate::get_zone!($name) + } + pub extern "C" fn malloc_size( _: *mut ::core::ffi::c_void, len: usize, ) -> *mut ::core::ffi::c_void { if $crate::heap_breakdown::ENABLED { - return $crate::get_zone!($name) + return zone() .malloc_zone_malloc(len) .unwrap_or(::core::ptr::null_mut()); } @@ -67,7 +73,7 @@ macro_rules! c_thunks_for_zone { if $crate::heap_breakdown::ENABLED { // SAFETY: `data` was allocated by this zone in one of the // alloc thunks above. - unsafe { $crate::get_zone!($name).malloc_zone_free(data) }; + unsafe { zone().malloc_zone_free(data) }; return; } // SAFETY: data was allocated by the default allocator (or is null).