Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d4dfedc
zstd: stop zero-filling the output buffers of Bun.zstdCompress/zstdDe…
robobun Aug 17, 2026
64f15e9
zstd: make the output buffer allocations fallible
robobun Aug 17, 2026
1f061f2
zstd: shorten comments, drain the child's stderr in the allocation fa…
robobun Aug 17, 2026
649f884
zlib, brotli: let the codecs see a failed allocation of their own state
robobun Aug 17, 2026
ff5139f
zlib: make the output buffer allocations fallible
robobun Aug 17, 2026
b70e8f5
libdeflate: make the output buffer allocations fallible
robobun Aug 17, 2026
25ed577
brotli, fetch compression, CompressionStream: fallible output buffers…
robobun Aug 17, 2026
e0e731b
zlib: allocate the inflate and encoder state without zeroing it
robobun Aug 17, 2026
c23ab9b
image: decode, resize, rotate and flip into uninitialized buffers
robobun Aug 17, 2026
e7467f4
encoding, TextDecoder, Blob: build transcoded buffers without zero-fi…
robobun Aug 17, 2026
09b8ffe
fs, archive, quic: stop zeroing the 16-64 KiB read scratch buffers on…
robobun Aug 17, 2026
704da0c
http, h2, websocket, tls wrapper: drop dead and redundant buffer zeroing
robobun Aug 17, 2026
8f7d42f
randomFill, compile cache, XML, bytea, bundler chunks, JSON tape, sou…
robobun Aug 17, 2026
2b6ea64
paths: join into an uninitialized scratch buffer
robobun Aug 17, 2026
23f1506
base64: encode and decode into spare capacity
robobun Aug 17, 2026
b1274e3
Shorten comments
robobun Aug 17, 2026
898e269
Share one UninitBuf between bun_sys, the runtime and the TLS wrapper
robobun Aug 17, 2026
3e53755
zstd: report the decoder's own allocation failures as OutOfMemory
robobun Aug 17, 2026
abd6949
UninitBuf::filled keeps the bounds check the sliced arrays had
robobun Aug 17, 2026
4677064
DecompressionStream: report brotli's allocation failures as OutOfMemory
robobun Aug 17, 2026
139f31d
Bun.zstd*: hand results to JS as boxed slices
robobun Aug 17, 2026
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
23 changes: 8 additions & 15 deletions src/ast/e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -991,7 +991,6 @@ pub struct JsonTape {
prop_value_locs: Vec<crate::Loc, TapeAlloc>,
item_locs: Vec<crate::Loc, TapeAlloc>,
str_chunks: Vec<Vec<u8, TapeAlloc>, TapeAlloc>,
str_used: usize,
pub encoding: StrEncoding,
}

Expand All @@ -1014,7 +1013,6 @@ impl JsonTape {
prop_value_locs: Vec::new_in(alloc),
item_locs: Vec::new_in(alloc),
str_chunks: Vec::new_in(alloc),
str_used: 0,
encoding: StrEncoding::Utf8,
}
}
Expand Down Expand Up @@ -1061,7 +1059,7 @@ impl JsonTape {
(first, rows.len() as u32)
}

/// Copy decoded string bytes into the tape; chunks never move once handed out.
/// Copy decoded string bytes into the tape; chunks grow only within capacity and so never move.
pub fn alloc_str(&mut self, bytes: &[u8]) -> Str {
self.alloc_str_join(bytes, b"")
}
Expand All @@ -1072,26 +1070,21 @@ impl JsonTape {
let fits = self
.str_chunks
.last()
.is_some_and(|c| c.len() - self.str_used >= len);
.is_some_and(|c| c.capacity() - c.len() >= len);
if !fits {
let cap = len.max(Self::STR_CHUNK);
let mut chunk: Vec<u8, TapeAlloc> = Vec::with_capacity_in(cap, self.alloc());
chunk.resize(cap, 0);
let chunk: Vec<u8, TapeAlloc> = Vec::with_capacity_in(cap, self.alloc());
self.str_chunks.push(chunk);
self.str_used = 0;
}
let chunk = self.str_chunks.last_mut().expect("chunk pushed above");
let out = &mut chunk[self.str_used..self.str_used + len];
let start = chunk.len();
if len <= 32 {
for (o, &c) in out.iter_mut().zip(a.iter().chain(b)) {
*o = c;
}
chunk.extend(a.iter().chain(b));
} else {
out[..a.len()].copy_from_slice(a);
out[a.len()..].copy_from_slice(b);
chunk.extend_from_slice(a);
chunk.extend_from_slice(b);
}
self.str_used += len;
Str::new(out)
Str::new(&chunk[start..])
}

/// The row buffers, for a reader that resolves spans itself.
Expand Down
43 changes: 29 additions & 14 deletions src/base64/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,22 +106,41 @@ pub enum DecodeAllocError {
}

pub fn decode_alloc(input: &[u8]) -> Result<Vec<u8>, DecodeAllocError> {
let mut dest = vec![0u8; decode_len(input)];
let result = decode(&mut dest, input);
let len = decode_len(input);
let mut dest: Vec<u8> = Vec::with_capacity(len);
// SAFETY: both decoders behind `decode` only write the destination, never read it.
let destination = unsafe { bun_core::vec::spare_bytes_mut(&mut dest) };
let result = decode(&mut destination[..len], input);
if !result.is_successful() {
return Err(DecodeAllocError::DecodingFailed);
}
dest.truncate(result.count);
// SAFETY: on success the decoder wrote the first `result.count` (<= `len`) bytes of the spare.
unsafe { bun_core::vec::commit_spare(&mut dest, result.count) };
Ok(dest)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

pub use bun_core::base64::encode;

/// [`encode`] appended to `out` (reserving the room itself); returns the number of bytes appended.
pub fn encode_append(out: &mut Vec<u8>, source: &[u8]) -> usize {
encode_append_impl(out, source, false)
}

fn encode_append_impl(out: &mut Vec<u8>, source: &[u8], is_urlsafe: bool) -> usize {
let len = simdutf::base64::encode_len(source.len(), is_urlsafe);
// SAFETY: `encode_raw` writes exactly `len` bytes into the `len` spare bytes reserved here.
unsafe {
bun_core::vec::fill_spare(out, len, |spare| {
let written = simdutf::base64::encode_raw(source, spare.as_mut_ptr(), is_urlsafe);
debug_assert_eq!(written, len);
(written, written)
})
}
}

pub fn encode_alloc(source: &[u8]) -> Vec<u8> {
let len = encode_len(source);
let mut destination = vec![0u8; len];
let encoded_len = encode(&mut destination, source);
destination.truncate(encoded_len);
let mut destination = Vec::new();
encode_append(&mut destination, source);
destination
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand All @@ -139,14 +158,10 @@ pub fn encode_url_safe(dest: &mut [u8], source: &[u8]) -> usize {
simdutf::base64::encode(source, dest, true)
}

/// `encode_url_safe` into a freshly-allocated `Vec<u8>` sized exactly via
/// `simdutf_encode_len_url_safe` (simdutf computes the exact no-padding length, so
/// the trailing `truncate` is a no-op kept for symmetry with `encode_alloc`).
/// [`encode_url_safe`] into a freshly-allocated `Vec<u8>`.
pub fn simdutf_encode_url_safe_alloc(source: &[u8]) -> Vec<u8> {
let len = simdutf_encode_len_url_safe(source.len());
let mut destination = vec![0u8; len];
let encoded_len = encode_url_safe(&mut destination, source);
destination.truncate(encoded_len);
let mut destination = Vec::new();
encode_append_impl(&mut destination, source, true);
destination
}

Expand Down
4 changes: 4 additions & 0 deletions src/brotli/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ pub enum Error {
BrotliFailedToCreateInstance,
#[error("BrotliDecompressionError")]
BrotliDecompressionError,
/// The output, or the decoder state the stream's window size dictates, could not be allocated.
#[error("OutOfMemory")]
OutOfMemory,
#[error("ShortRead")]
ShortRead,
}
Expand All @@ -17,6 +20,7 @@ impl Error {
Self::BrotliFailedToLoad => "BrotliFailedToLoad",
Self::BrotliFailedToCreateInstance => "BrotliFailedToCreateInstance",
Self::BrotliDecompressionError => "BrotliDecompressionError",
Self::OutOfMemory => "OutOfMemory",
Self::ShortRead => "ShortRead",
}
}
Expand Down
45 changes: 43 additions & 2 deletions src/brotli/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,10 @@ impl StreamingDecoder {
self.state,
ReaderState::Uninitialized | ReaderState::Inflating
) {
out.reserve(4096);
if out.try_reserve(4096).is_err() {
self.state = ReaderState::Error;
return Err(crate::Error::OutOfMemory);
}
let spare = out.spare_capacity_mut();
let out_len = spare.len();
let mut next_out: *mut u8 = spare.as_mut_ptr().cast::<u8>();
Expand Down Expand Up @@ -160,7 +163,13 @@ impl StreamingDecoder {
}
c::BrotliDecoderResult::err => {
self.state = ReaderState::Error;
return Err(crate::Error::BrotliDecompressionError);
return Err(
if c::BrotliDecoderGetErrorCode(self.brotli_mut()).is_alloc_failure() {
crate::Error::OutOfMemory
} else {
crate::Error::BrotliDecompressionError
},
);
}
c::BrotliDecoderResult::needs_more_input => {
self.state = ReaderState::Inflating;
Expand Down Expand Up @@ -220,3 +229,35 @@ pub fn encode(
};
(ok != 0).then_some(out_len)
}

/// [`encode`] into `out`'s spare capacity (callers reserve the bound first), advancing `out.len()`.
pub fn encode_append(
quality: core::ffi::c_int,
lgwin: core::ffi::c_int,
mode: c::BrotliEncoderMode,
input: &[u8],
out: &mut Vec<u8>,
) -> Option<usize> {
let spare = out.spare_capacity_mut();
let mut out_len = spare.len();
// SAFETY: input/spare are valid for their lengths; BrotliEncoderCompress
// only reads `input` and writes at most `out_len` bytes into spare,
// updating `out_len` to the number written.
let ok = unsafe {
c::BrotliEncoderCompress(
quality,
lgwin,
mode,
input.len(),
input.as_ptr(),
&raw mut out_len,
spare.as_mut_ptr().cast::<u8>(),
)
};
if ok == 0 {
return None;
}
// SAFETY: brotli initialized the first `out_len` bytes of spare.
unsafe { bun_core::vec::commit_spare(out, out_len) };
Some(out_len)
}
15 changes: 15 additions & 0 deletions src/brotli_sys/brotli_c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,21 @@ pub enum BrotliDecoderErrorCode2 {
ERROR_UNREACHABLE = -31,
}

impl BrotliDecoderErrorCode2 {
/// The decoder's allocator (its `alloc_func`) returned null.
pub fn is_alloc_failure(self) -> bool {
matches!(
self,
Self::ERROR_ALLOC_CONTEXT_MODES
| Self::ERROR_ALLOC_TREE_GROUPS
| Self::ERROR_ALLOC_CONTEXT_MAP
| Self::ERROR_ALLOC_RING_BUFFER_1
| Self::ERROR_ALLOC_RING_BUFFER_2
| Self::ERROR_ALLOC_BLOCK_TYPE_TREES
)
}
}

#[repr(u32)]
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum BrotliDecoderParameter {
Expand Down
39 changes: 9 additions & 30 deletions src/bun_alloc/c_thunks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,9 @@ use crate::default_alloc as raw;
// ──────────────────────────────────────────────────────────────────────────

/// 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 {
let p = raw::malloc((items * size) as usize);
if p.is_null() {
unreachable!();
}
p
raw::malloc(items as usize * size as usize)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/// `(opaque, ptr)` → default allocator `free(ptr)`; opaque cookie ignored.
Expand Down Expand Up @@ -52,8 +49,7 @@ pub unsafe extern "C" fn mi_free_bytes(bytes: *mut c_void, _ctx: *mut c_void) {
/// Generated items:
/// - `malloc_size(_, len: usize) -> *mut c_void` — brotli-shape, non-zeroing.
/// Safe `extern "C" fn` (opaque cookie ignored; body is all-safe).
/// - `calloc_items(_, items: c_uint, len: c_uint) -> *mut c_void` — zlib-shape,
/// zeroing. Safe `extern "C" fn` (same rationale).
/// - `malloc_items(_, items: c_uint, len: c_uint) -> *mut c_void` — zlib-shape, non-zeroing. Both return null on failure, which zlib and brotli report as their own OOM errors.
/// - `free(_, ptr: *mut c_void)` — paired with either alloc. `unsafe`
/// (precondition: `ptr` was allocated by this zone / the default allocator).
///
Expand All @@ -67,36 +63,19 @@ macro_rules! c_thunks_for_zone {
len: usize,
) -> *mut ::core::ffi::c_void {
if $crate::heap_breakdown::ENABLED {
return match $crate::get_zone!($name).malloc_zone_malloc(len) {
Some(p) => p,
None => $crate::out_of_memory(),
};
return $crate::get_zone!($name)
.malloc_zone_malloc(len)
.unwrap_or(::core::ptr::null_mut());
}
let p = $crate::default_alloc::malloc(len);
if p.is_null() {
$crate::out_of_memory();
}
p
$crate::default_alloc::malloc(len)
}

pub extern "C" fn calloc_items(
pub extern "C" fn malloc_items(
_: *mut ::core::ffi::c_void,
items: ::core::ffi::c_uint,
len: ::core::ffi::c_uint,
) -> *mut ::core::ffi::c_void {
if $crate::heap_breakdown::ENABLED {
return match $crate::get_zone!($name)
.malloc_zone_calloc(items as usize, len as usize)
{
Some(p) => p,
None => $crate::out_of_memory(),
};
}
let p = $crate::default_alloc::calloc(items as usize, len as usize);
if p.is_null() {
$crate::out_of_memory();
}
p
malloc_size(::core::ptr::null_mut(), items as usize * len as usize)
}

pub unsafe extern "C" fn free(_: *mut ::core::ffi::c_void, data: *mut ::core::ffi::c_void) {
Expand Down
32 changes: 32 additions & 0 deletions src/bun_core/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,38 @@ pub mod vec {
r
}
}

/// The stack-array form of [`spare_bytes_mut`]: `N` uninitialized bytes for a producer that reports how many it wrote.
pub struct UninitBuf<const N: usize>(core::mem::MaybeUninit<[u8; N]>);

impl<const N: usize> UninitBuf<N> {
#[inline(always)]
pub const fn uninit() -> Self {
Self(core::mem::MaybeUninit::uninit())
}

#[inline(always)]
pub fn as_mut_ptr(&mut self) -> *mut u8 {
self.0.as_mut_ptr().cast::<u8>()
}

/// # Safety
/// Write-only view, same contract as [`spare_bytes_mut`]: only a producer may store into it, and only the prefix it reports may be read back.
Comment thread
robobun marked this conversation as resolved.
#[inline(always)]
pub unsafe fn as_bytes_mut(&mut self) -> &mut [u8] {
// SAFETY: `MaybeUninit<[u8; N]>` has the layout of `[u8; N]`; the caller upholds the write-only contract.
unsafe { core::slice::from_raw_parts_mut(self.as_mut_ptr(), N) }
}

/// # Safety
/// A producer must have written every byte of `[0..len]` (`len <= N`).
Comment thread
robobun marked this conversation as resolved.
#[inline(always)]
pub unsafe fn filled(&self, len: usize) -> &[u8] {
assert!(len <= N);
// SAFETY: `[0..len]` is inside the array (asserted above) and initialized (caller contract).
unsafe { core::slice::from_raw_parts(self.0.as_ptr().cast::<u8>(), len) }
}
}
}

#[path = "Progress.rs"]
Expand Down
6 changes: 2 additions & 4 deletions src/bun_core/string/MutableString.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ impl MutableString {

#[inline]
pub fn grow_if_needed(&mut self, amount: usize) -> Result<(), AllocError> {
self.list.reserve(amount);
Ok(())
self.list.try_reserve(amount).map_err(|_| AllocError)
}

pub(crate) fn writable_n_bytes_assume_capacity(&mut self, amount: usize) -> &mut [u8] {
Expand Down Expand Up @@ -226,8 +225,7 @@ impl MutableString {

#[inline]
pub fn grow_by(&mut self, amount: usize) -> Result<(), AllocError> {
self.list.reserve(amount);
Ok(())
self.list.try_reserve(amount).map_err(|_| AllocError)
}

#[inline]
Expand Down
Loading
Loading