From 870d6a2e40a791810bca7bc6d391331f6a1f6b2d Mon Sep 17 00:00:00 2001 From: ddidderr Date: Mon, 20 Jul 2026 09:07:11 +0200 Subject: [PATCH] refactor(decompress): move stack-context dispatch policy to Rust Keep the heapmode guard, stack-owned ZSTD_DCtx, static initialization, and private context layout in C. Once the context is initialized, Rust now owns the one-shot stack-context policy: it clears the projected static-size marker and dispatches the existing ZSTD_decompressDCtx implementation through the opaque context pointer. Add matching C/Rust view-layout assertions and focused callback tests covering static-size clearing, argument forwarding, and null-context rejection. Test Plan: - `ulimit -v 41943040; cargo +nightly fmt --manifest-path rust/Cargo.toml --all -- --check` - `ulimit -v 41943040; CARGO_BUILD_JOBS=1 cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings` - `ulimit -v 41943040; make -j1` - `ulimit -v 41943040; make -j1 -C tests test` (all upstream tests completed successfully) - Standalone Rust unit linking remains unavailable without the C-owned bridge symbols; the integrated build and upstream C/Rust oracle supplied the runtime/link coverage. --- lib/decompress/zstd_decompress.c | 16 ++-- rust/src/zstd_decompress.rs | 134 ++++++++++++++++++++++++++++++- 2 files changed, 144 insertions(+), 6 deletions(-) diff --git a/lib/decompress/zstd_decompress.c b/lib/decompress/zstd_decompress.c index 0d911279e..c2bf54ad8 100644 --- a/lib/decompress/zstd_decompress.c +++ b/lib/decompress/zstd_decompress.c @@ -106,6 +106,12 @@ typedef struct { size_t dctx_size; } ZSTD_rustDctxView; +typedef char ZSTD_rust_dctx_view_layout[ + (offsetof(ZSTD_rustDctxView, dctx) == 0 + && offsetof(ZSTD_rustDctxView, static_size) == 29 * sizeof(void*) + && offsetof(ZSTD_rustDctxView, dctx_size) == 67 * sizeof(void*) + && sizeof(ZSTD_rustDctxView) == 68 * sizeof(void*)) ? 1 : -1]; + /* Rust exposes this as u64. Use the trace API's exact unsigned-long-long * representation instead of U64, whose typedef may be a different C type on * some targets even when it has the same width. */ @@ -133,6 +139,9 @@ int ZSTD_rust_no_forward_progress_max(void); int ZSTD_rust_heapmode(void); size_t ZSTD_rust_decompress_stack(void* dst, size_t dstCapacity, const void* src, size_t srcSize); +size_t ZSTD_rust_decompress_stack_context(ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize); ZSTD_DDict* ZSTD_rust_create_ddict(const void* dict, size_t dictSize, ZSTD_dictLoadMethod_e dictLoadMethod, ZSTD_dictContentType_e dictContentType, @@ -277,11 +286,8 @@ size_t ZSTD_rust_decompress_stack(void* dst, size_t dstCapacity, ZSTD_DCtx dctx; ZSTD_DCtx* const initialized = ZSTD_initStaticDCtx(&dctx, sizeof(dctx)); if (initialized == NULL) return ERROR(memory_allocation); - /* This is a stack DCtx, not a user-provided static workspace. Keep the - * original heapmode=0 semantics so legacy decoding is permitted and no - * static-context allocation restrictions leak into the one-shot API. */ - initialized->staticSize = 0; - return ZSTD_decompressDCtx(initialized, dst, dstCapacity, src, srcSize); + return ZSTD_rust_decompress_stack_context(initialized, dst, dstCapacity, + src, srcSize); #endif } diff --git a/rust/src/zstd_decompress.rs b/rust/src/zstd_decompress.rs index f4415fef7..ea18d8f0a 100644 --- a/rust/src/zstd_decompress.rs +++ b/rust/src/zstd_decompress.rs @@ -26,7 +26,7 @@ use crate::zstd_ddict::{ }; use std::cmp::{max, min}; use std::ffi::c_void; -use std::mem::{size_of, MaybeUninit}; +use std::mem::{align_of, offset_of, size_of, MaybeUninit}; use std::os::raw::{c_int, c_uint}; use std::ptr; @@ -299,6 +299,14 @@ struct ZSTD_rustDctxView { dctx_size: usize, } +const _: () = { + assert!(offset_of!(ZSTD_rustDctxView, dctx) == 0); + assert!(offset_of!(ZSTD_rustDctxView, static_size) == 29 * size_of::()); + assert!(offset_of!(ZSTD_rustDctxView, dctx_size) == 67 * size_of::()); + assert!(size_of::() == 68 * size_of::()); + assert!(align_of::() == align_of::()); +}; + type ZSTD_rustTraceBeginFn = unsafe extern "C" fn(*const c_void) -> u64; type ZSTD_rustTraceEndFn = unsafe extern "C" fn(u64, *const c_void); @@ -375,6 +383,9 @@ unsafe extern "C" { fn ZSTD_checkContinuity(dctx: *mut ZSTD_DCtx, dst: *const c_void, dst_size: usize); } +type ZstdDecompressDCtxFn = + unsafe extern "C" fn(*mut ZSTD_DCtx, *mut c_void, usize, *const c_void, usize) -> usize; + #[inline] unsafe fn field(slot: *mut c_void) -> T { unsafe { slot.cast::().read() } @@ -2867,6 +2878,88 @@ mod dctx_trace_tests { } } +#[cfg(test)] +mod stack_context_tests { + use super::*; + + #[repr(C)] + struct StackDispatchProbe { + static_size: usize, + calls: usize, + dctx: usize, + dst: usize, + dst_capacity: usize, + src: usize, + src_size: usize, + result: usize, + } + + unsafe extern "C" fn dispatch_probe( + dctx: *mut ZSTD_DCtx, + dst: *mut c_void, + dst_capacity: usize, + src: *const c_void, + src_size: usize, + ) -> usize { + let probe = unsafe { &mut *dctx.cast::() }; + probe.calls += 1; + probe.dctx = dctx as usize; + probe.dst = dst as usize; + probe.dst_capacity = dst_capacity; + probe.src = src as usize; + probe.src_size = src_size; + probe.result + } + + #[test] + fn stack_context_callback_clears_static_size_before_dispatch() { + let mut probe = StackDispatchProbe { + static_size: usize::MAX, + calls: 0, + dctx: 0, + dst: 0, + dst_capacity: 0, + src: 0, + src_size: 0, + result: 0x1234, + }; + let mut view = unsafe { MaybeUninit::::zeroed().assume_init() }; + view.dctx = (&mut probe as *mut StackDispatchProbe).cast(); + view.static_size = std::ptr::addr_of_mut!(probe.static_size).cast(); + + let mut dst = [0u8; 8]; + let src = [1u8, 2, 3]; + let result = unsafe { + decompress_stack_context( + &view, + dst.as_mut_ptr().cast(), + dst.len(), + src.as_ptr().cast(), + src.len(), + dispatch_probe, + ) + }; + + assert_eq!(result, probe.result); + assert_eq!(probe.calls, 1); + assert_eq!(probe.static_size, 0); + assert_eq!(probe.dctx, view.dctx as usize); + assert_eq!(probe.dst, dst.as_mut_ptr() as usize); + assert_eq!(probe.dst_capacity, dst.len()); + assert_eq!(probe.src, src.as_ptr() as usize); + assert_eq!(probe.src_size, src.len()); + } + + #[test] + fn stack_context_callback_rejects_null_context() { + let result = unsafe { + ZSTD_rust_decompress_stack_context(ptr::null_mut(), ptr::null_mut(), 0, ptr::null(), 0) + }; + + assert_eq!(result, ERROR(ZstdErrorCode::Generic)); + } +} + unsafe fn select_frame_ddict(view: &ZSTD_rustDctxView) { let set: *mut DDictHashSet = unsafe { field(view.ddict_set) }; if set.is_null() || unsafe { dctx_ddict(view) }.is_null() { @@ -3483,6 +3576,45 @@ pub unsafe extern "C" fn ZSTD_decompressDCtx( unsafe { ZSTD_decompress_usingDDict(dctx, dst, dst_capacity, src, src_size, ddict) } } +unsafe fn decompress_stack_context( + view: &ZSTD_rustDctxView, + dst: *mut c_void, + dst_capacity: usize, + src: *const c_void, + src_size: usize, + dispatch: ZstdDecompressDCtxFn, +) -> usize { + if view.dctx.is_null() || view.static_size.is_null() { + return ERROR(ZstdErrorCode::Generic); + } + /* A stack DCtx is not a user-provided static workspace. Clear the + * projected marker before dispatch so heapmode=0 retains the historical + * one-shot behavior, including legacy decoding and allocations. */ + unsafe { set_field(view.static_size, 0usize) }; + unsafe { dispatch(view.dctx.cast(), dst, dst_capacity, src, src_size) } +} + +/// Rust policy/dispatch for the C-owned stack context used by heapmode=0. +/// +/// The context remains opaque here; the C projection supplies only the field +/// addresses Rust needs and keeps `ZSTD_DCtx_s` layout private to C. +#[no_mangle] +pub unsafe extern "C" fn ZSTD_rust_decompress_stack_context( + dctx: *mut ZSTD_DCtx, + dst: *mut c_void, + dst_capacity: usize, + src: *const c_void, + src_size: usize, +) -> usize { + if dctx.is_null() { + return ERROR(ZstdErrorCode::Generic); + } + let view = unsafe { dctx_view(dctx) }; + unsafe { + decompress_stack_context(&view, dst, dst_capacity, src, src_size, ZSTD_decompressDCtx) + } +} + #[no_mangle] pub unsafe extern "C" fn ZSTD_decompress( dst: *mut c_void,