The fastest way to stop a decompression bomb is to never allocate for it in the first place.
Today we released OxiCode 0.2.5 — a hardening-focused release. A coordinated internal audit went through decoding, streaming, compression, checksums, the derive macros, and serde integration, and fixed a wide range of denial-of-service, panic, integer-overflow, and silent-corruption issues. No serialized wire-format bytes changed for any input that was already valid — every behavior change is either a new rejection of previously-invalid or malicious input, or a bookkeeping/allocation-timing fix on the error path.
No C. No Fortran. No unaudited allocation path.
OxiCode stays a pure-Rust binary codec that compiles to a single static binary (or WASM) — now with every collection and stream decoder accounting for its allocation before it makes it.
Why OxiCode 0.2.5 is a game changer
Binary deserializers have a well-known soft spot: a length prefix is just a number, and if you allocate based on it before validating it, an attacker who controls four bytes controls your memory usage. OxiCode 0.2.5 closes that gap everywhere it was still open:
- Container decoding —
HashMap,HashSet,BinaryHeap,VecDeque,BTreeMap,BTreeSet,LinkedList, andPathBufnow callclaim_container_readagainst the configured decode-size limit before allocating, matching whatVecalready did. A forged huge length prefix is rejected up front instead of driving an unbounded allocation. - Streaming decode —
StreamingDecoder/AsyncStreamingDecoder/BufferStreamingDecodernow reject any chunk header whose declared payload length exceeds the configured limit before allocating a buffer for it. A forged0xFFFFFFFFchunk header no longer forces a ~4 GiB allocation attempt. - Zstd/LZ4 decompression —
compression::decompressnow pre-scans the frame header and block table, rejecting frames that omitFrame_Content_Size, declare a size above the configured cap, or whose block table doesn’t add up — bounding peak memory to roughlycontent_size + 128 KiBregardless of what the blocks actually decode to. - Recursion depth —
Box,Rc,Arc, and the standard collection decoders now enforce a configurable recursion-depth guard (default 128), so adversarially deep nesting returns an error instead of exhausting the stack.
Alongside the DoS-mitigation work, the audit also caught a checksum length-overflow that could panic on attacker-controlled input, an [T; N] decode path that leaked already-initialized elements on error, an OsStr::encode that silently lossy-converted non-UTF-8 bytes instead of erroring, and — separate from the security items — a SIMD array codec that quietly fell back to scalar code for some inputs while still claiming a fixed “2-4x speedup” it never measured. 0.2.5 fixes all of it, and replaces the SIMD fast path with real hardware kernels.
Technical Deep Dive: what changed under the hood
-
Claim-before-allocate, everywhere
Theclaim_container_readaccounting that already guardedVecdecode is now applied uniformly across every standard-library collection type and every derive-generatedVec/seq_lenfield, plus the streaming chunk-header path. One rule, enforced consistently, instead ofVecbeing hardened while its siblings were not. -
Compression-bomb pre-scan
Zstd decompression now inspects the frame header and per-block regenerated-size upper bounds before committing to decompress, rejecting frames that would blow past the configured cap. LZ4 frames that clear theContent_Sizeflag (or use a non-standard magic) are rejected before the decoder would otherwise make an unbounded up-front reservation. Cross-feature codec mismatches (LZ4 payload decompressed with onlycompression-zstdenabled) now return a clear “codec not enabled” error instead of misdispatching. -
Overflow and leak fixes on the error path
verify_checksum’sHEADER_SIZE + stored_lenaddition is nowchecked_addinstead of an unchecked add that could wrap or panic on a forged length nearusize::MAX.[T; N]decode now uses a drop-guard tracking the initialized-so-far count, so an error partway through no longer leaks the elements already decoded. Every remainingu64::decode(..)? as usizelength read acrossimpl_std.rs,impl_alloc.rs, and derive-generated code now uses a checkedusize::try_from. -
Real SIMD, and zero-copy serde borrowing
oxicode::simdnow always routes through genuine AVX2/SSE2 (x86_64) or NEON (aarch64) hardware kernels on little-endian targets, with runtime CPU-capability detection understd— the fabricated “2-4x” claim is gone; the docs now measure their own speedup at run time. Separately, the serde integration gained real zero-copy borrowed deserialization:&'de str/&'de [u8]/#[serde(borrow)]fields now actually borrow from the input buffer instead of erroring at runtime.
Getting Started
cargo add oxicode
Cap decompression memory explicitly — the headline 0.2.5 capability, though decompress() already defaults to a 256 MiB cap:
use oxicode::compression::{compress, decompress_with_limit, Compression};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let payload = vec![0u8; 4096];
let encoded = oxicode::encode_to_vec(&payload)?;
let compressed = compress(&encoded, Compression::Zstd)?;
// Reject anything whose declared/regenerated size exceeds 16 MiB,
// checked against the frame header before decompression starts.
let decompressed = decompress_with_limit(&compressed, 16 * 1024 * 1024)?;
let (decoded, _): (Vec<u8>, _) = oxicode::decode_from_slice(&decompressed)?;
assert_eq!(payload, decoded);
Ok(())
}
What’s New in 0.2.5
- Security: claim-before-allocate accounting extended to every standard collection and derive-generated
seq_lenfield; streaming chunk-header size checked before allocation; streaming truncation now returnsError::UnexpectedEndinstead of looking like a clean stream; streaming decoders latch a poisoned state after any load/truncation/limit error; async streaming cancellation now uses a persisted, resumable fill-cursor so a dropped future can’t lose or duplicate bytes - Security: Zstd/LZ4 decompression-bomb pre-scan, checksum length-overflow fixed with
checked_add,[T; N]decode leak fixed with a drop-guard,OsStr::encodeno longer silently lossy-converts non-UTF-8 input, checkedusize/isizeconversions throughout, configurable decode recursion-depth guard (default 128) - Security: derive now rejects mutually-exclusive attribute combinations (
bytes+skip/default/seq_len/with) and unrepresentable skipped-variant encodes at compile time instead of generating broken code - Added:
compression::decompress_with_limitandDEFAULT_MAX_DECOMPRESSED_SIZE;new_with_config/new_with_configsconstructors across the streaming encoder/decoder family; zero-copy borrowed deserialization for serde (&'de str/&'de [u8]/#[serde(borrow)]); genericBorrowDecodeforRc<T>/Arc<T>;u8-specialized bulk-copy fast paths;#[oxicode(tag_type = "u64")]for enum discriminants wider thanu32; ~40 newhardening_*regression tests - Fixed: real AVX2/SSE2/NEON kernels now back
oxicode::simdunconditionally (previously a silent scalar fallback for some inputs); serde-path errors now preserve the real underlyingoxicode::Errorinstead of collapsing to a generic string - Changed: default features now include
validationandversioning(previously mislabeled as test-only opt-ins);oxiarc-lz4/oxiarc-zstdbumped 0.3.2 → 0.4.0;tokiobumped to 1.53 - Documentation: rewrote the README’s Advanced Features examples to match the real API; added a “Known compatibility caveats” section documenting the standard-library types that diverge from bincode 2.0.1 regardless of config
Tips
- Set your own decompression cap for untrusted input.
decompress()defaults to 256 MiB; for tighter environments calldecompress_with_limit(data, max_output)explicitly, as shown above — the check happens against the frame header before any decompression work starts. - Lower the recursion limit for adversarial input. The default decode recursion depth is 128 (
oxicode::de::decoder::DEFAULT_RECURSION_LIMIT); construct your decoder withDecoderImpl::new(..)and callset_recursion_limit(n)to tighten it further for deeply-nested untrusted payloads. - Borrowed serde fields are now actually zero-copy. If your
#[derive(Deserialize)]type has#[serde(borrow)] field: &'de str(or&'de [u8]), it now borrows from the input buffer instead of silently erroring — no code changes needed on your end. - A streaming decoder that errors stays errored. Once a
StreamingDecoderhits a load, truncation, or limit error, every subsequent read deterministically returnsError::InvalidDatarather than risking a misread of stale bytes — don’t retry a poisoned decoder, construct a new one. - Re-check any code that used to silently accept OsStr with invalid UTF-8.
OsStr::encodenow returnsError::InvalidDatafor non-UTF-8 input instead of lossy-converting it; valid-UTF-8 output is byte-identical to before. validationandversioningship by default now. If you were pulling either in explicitly, the feature flags still work — they’re just no longer opt-in-only.
This is the foundation
OxiCode is the serialization layer beneath the COOLJAPAN data stack — checkpoints for SciRS2 and NumRS2, tensor buffers for ToRSh, UI state serialization for OxiUI, spatial data for OxiGeo, quantum circuit payloads for QuantRS2, and archive-embedded records via OxiARC. A hardening pass this thorough lands underneath every one of those consumers automatically, without any of them having to change a line of their own code.
Repository: https://github.com/cool-japan/oxicode
Star the repo if you want a binary codec that treats every length prefix as hostile until proven otherwise.
The era of “trust the length byte” is over. Pure Rust binary serialization is here — fast, compatible, and sovereign.
— KitaSan at COOLJAPAN OÜ July 30, 2026