fix(compress): keep Fast dictionary projection live

The Fast dictionary-table projection was populated before the Rust
orchestrator ran its window-update and match-state publication callbacks.
That left base and nextToUpdate pointing at the initial sentinel for MT
prefix jobs, which caused ZSTD_rust_fillHashTable to dereference an invalid
range. Project pointers to the live match-state fields so Rust reads the
same values the former C adapter would have read at the Fast branch.

Test Plan:
- git diff --check
- ulimit -v 41943040; CARGO_BUILD_JOBS=1 cargo check --manifest-path rust/Cargo.toml --tests
- ulimit -v 41943040; make -j1
- capped 4 MiB -T0 compress/decompress round trip with cmp
This commit is contained in:
2026-07-21 20:52:27 +02:00
parent 4a3d5b9d27
commit 846e18f6d9
2 changed files with 63 additions and 60 deletions
+18 -17
View File
@@ -2780,11 +2780,11 @@ typedef void (*ZSTD_rust_loadDictionaryContent_overflowCorrect_f)(
* fields needed by that leaf in the ABI projection; the complete * fields needed by that leaf in the ABI projection; the complete
* ZSTD_MatchState_t layout remains private to C. */ * ZSTD_MatchState_t layout remains private to C. */
typedef struct { typedef struct {
U32* hashTable; U32** hashTable;
const BYTE* base; const BYTE** base;
U32 nextToUpdate; const U32* nextToUpdate;
U32 hashLog; const U32* hashLog;
U32 minMatch; const U32* minMatch;
int fullTableLoad; int fullTableLoad;
int forCDict; int forCDict;
} ZSTD_rust_loadDictionaryContentFastTableState; } ZSTD_rust_loadDictionaryContentFastTableState;
@@ -2795,17 +2795,15 @@ typedef char ZSTD_rust_load_dictionary_fast_table_layout[
&& offsetof(ZSTD_rust_loadDictionaryContentFastTableState, nextToUpdate) && offsetof(ZSTD_rust_loadDictionaryContentFastTableState, nextToUpdate)
== 2 * sizeof(void*) == 2 * sizeof(void*)
&& offsetof(ZSTD_rust_loadDictionaryContentFastTableState, hashLog) && offsetof(ZSTD_rust_loadDictionaryContentFastTableState, hashLog)
== 2 * sizeof(void*) + sizeof(U32) == 3 * sizeof(void*)
&& offsetof(ZSTD_rust_loadDictionaryContentFastTableState, minMatch) && offsetof(ZSTD_rust_loadDictionaryContentFastTableState, minMatch)
== 2 * sizeof(void*) + 2 * sizeof(U32) == 4 * sizeof(void*)
&& offsetof(ZSTD_rust_loadDictionaryContentFastTableState, fullTableLoad) && offsetof(ZSTD_rust_loadDictionaryContentFastTableState, fullTableLoad)
== 2 * sizeof(void*) + 3 * sizeof(U32) == 5 * sizeof(void*)
&& offsetof(ZSTD_rust_loadDictionaryContentFastTableState, forCDict) && offsetof(ZSTD_rust_loadDictionaryContentFastTableState, forCDict)
== 2 * sizeof(void*) + 3 * sizeof(U32) + sizeof(int) == 5 * sizeof(void*) + sizeof(int)
&& sizeof(ZSTD_rust_loadDictionaryContentFastTableState) && sizeof(ZSTD_rust_loadDictionaryContentFastTableState)
== ((offsetof(ZSTD_rust_loadDictionaryContentFastTableState, forCDict) == 5 * sizeof(void*) + 2 * sizeof(int))
+ sizeof(int) + sizeof(void*) - 1)
/ sizeof(void*) * sizeof(void*)))
? 1 : -1]; ? 1 : -1];
typedef void (*ZSTD_rust_loadDictionaryContent_fillTable_f)( typedef void (*ZSTD_rust_loadDictionaryContent_fillTable_f)(
void* context, const void* iend, int dtlm, int tfp); void* context, const void* iend, int dtlm, int tfp);
@@ -7016,11 +7014,14 @@ static size_t ZSTD_loadDictionaryContent_callback(
assert((tfp == ZSTD_tfp_forCDict && dtlm == ZSTD_dtlm_full) assert((tfp == ZSTD_tfp_forCDict && dtlm == ZSTD_dtlm_full)
|| (tfp != ZSTD_tfp_forCDict && dtlm == ZSTD_dtlm_fast)); || (tfp != ZSTD_tfp_forCDict && dtlm == ZSTD_dtlm_fast));
fastTable.hashTable = ms->hashTable; /* These fields are mutated by the window/publish callbacks before the
fastTable.base = ms->window.base; * Fast branch runs. Keep pointers to the live match-state fields rather
fastTable.nextToUpdate = ms->nextToUpdate; * than a stale snapshot taken before the Rust orchestrator starts. */
fastTable.hashLog = ms->cParams.hashLog; fastTable.hashTable = &ms->hashTable;
fastTable.minMatch = ms->cParams.minMatch; fastTable.base = &ms->window.base;
fastTable.nextToUpdate = &ms->nextToUpdate;
fastTable.hashLog = &ms->cParams.hashLog;
fastTable.minMatch = &ms->cParams.minMatch;
fastTable.fullTableLoad = dtlm == ZSTD_dtlm_full; fastTable.fullTableLoad = dtlm == ZSTD_dtlm_full;
fastTable.forCDict = tfp == ZSTD_tfp_forCDict; fastTable.forCDict = tfp == ZSTD_tfp_forCDict;
+45 -43
View File
@@ -141,17 +141,17 @@ type LoadDictionaryContentOverflowCorrectFn =
unsafe extern "C" fn(context: *mut c_void, ip: *const c_void, iend: *const c_void); unsafe extern "C" fn(context: *mut c_void, ip: *const c_void, iend: *const c_void);
/// Direct projection for the Fast dictionary-table leaf. /// Direct projection for the Fast dictionary-table leaf.
/// ///
/// C copies `nextToUpdate` rather than exposing a pointer into the private /// C exposes pointers to the live scalar fields needed by the Fast leaf.
/// match state. `fullTableLoad` and `forCDict` are normalized from the C enum /// `fullTableLoad` and `forCDict` are normalized from the C enum inputs after
/// inputs after their original validity assertion has run. /// their original validity assertion has run.
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct ZSTD_rust_loadDictionaryContentFastTableState { struct ZSTD_rust_loadDictionaryContentFastTableState {
hash_table: *mut c_uint, hash_table: *const *mut c_uint,
base: *const u8, base: *const *const u8,
next_to_update: c_uint, next_to_update: *const c_uint,
hash_log: c_uint, hash_log: *const c_uint,
min_match: c_uint, min_match: *const c_uint,
full_table_load: c_int, full_table_load: c_int,
for_cdict: c_int, for_cdict: c_int,
} }
@@ -229,28 +229,25 @@ const _: () = {
); );
assert!( assert!(
offset_of!(ZSTD_rust_loadDictionaryContentFastTableState, hash_log) offset_of!(ZSTD_rust_loadDictionaryContentFastTableState, hash_log)
== 2 * size_of::<usize>() + size_of::<c_uint>() == 3 * size_of::<usize>()
); );
assert!( assert!(
offset_of!(ZSTD_rust_loadDictionaryContentFastTableState, min_match) offset_of!(ZSTD_rust_loadDictionaryContentFastTableState, min_match)
== 2 * size_of::<usize>() + 2 * size_of::<c_uint>() == 4 * size_of::<usize>()
); );
assert!( assert!(
offset_of!( offset_of!(
ZSTD_rust_loadDictionaryContentFastTableState, ZSTD_rust_loadDictionaryContentFastTableState,
full_table_load full_table_load
) == 2 * size_of::<usize>() + 3 * size_of::<c_uint>() ) == 5 * size_of::<usize>()
); );
assert!( assert!(
offset_of!(ZSTD_rust_loadDictionaryContentFastTableState, for_cdict) offset_of!(ZSTD_rust_loadDictionaryContentFastTableState, for_cdict)
== 2 * size_of::<usize>() + 3 * size_of::<c_uint>() + size_of::<c_int>() == 5 * size_of::<usize>() + size_of::<c_int>()
); );
assert!( assert!(
size_of::<ZSTD_rust_loadDictionaryContentFastTableState>() size_of::<ZSTD_rust_loadDictionaryContentFastTableState>()
== (offset_of!(ZSTD_rust_loadDictionaryContentFastTableState, for_cdict) == 5 * size_of::<usize>() + 2 * size_of::<c_int>()
+ size_of::<c_int>())
.div_ceil(size_of::<usize>())
* size_of::<usize>()
); );
assert!(size_of::<LoadDictionaryContentLoadMatchFn>() == size_of::<usize>()); assert!(size_of::<LoadDictionaryContentLoadMatchFn>() == size_of::<usize>());
assert!(size_of::<LoadDictionaryContentLoadTreeFn>() == size_of::<usize>()); assert!(size_of::<LoadDictionaryContentLoadTreeFn>() == size_of::<usize>());
@@ -369,6 +366,25 @@ fn dictionary_table_policy(
} }
} }
#[inline]
unsafe fn fill_fast_dictionary_table(
fast_table: &ZSTD_rust_loadDictionaryContentFastTableState,
end: *const c_void,
) {
unsafe {
ZSTD_rust_fillHashTable(
*fast_table.hash_table,
*fast_table.base,
*fast_table.next_to_update,
end,
*fast_table.hash_log,
*fast_table.min_match,
fast_table.full_table_load,
fast_table.for_cdict,
)
}
}
unsafe fn load_dictionary_content( unsafe fn load_dictionary_content(
state: &ZSTD_rust_loadDictionaryContentState, state: &ZSTD_rust_loadDictionaryContentState,
src: *const c_void, src: *const c_void,
@@ -446,16 +462,7 @@ unsafe fn load_dictionary_content(
) { ) {
DictionaryTablePolicy::Fast => unsafe { DictionaryTablePolicy::Fast => unsafe {
let fast_table = &*state.fast_table; let fast_table = &*state.fast_table;
ZSTD_rust_fillHashTable( fill_fast_dictionary_table(fast_table, iend.cast())
fast_table.hash_table,
fast_table.base,
fast_table.next_to_update,
iend.cast(),
fast_table.hash_log,
fast_table.min_match,
fast_table.full_table_load,
fast_table.for_cdict,
)
}, },
DictionaryTablePolicy::DoubleFast => unsafe { DictionaryTablePolicy::DoubleFast => unsafe {
(state.fill_double_hash_table)( (state.fill_double_hash_table)(
@@ -6467,31 +6474,26 @@ mod tests {
for (index, byte) in input.iter_mut().enumerate() { for (index, byte) in input.iter_mut().enumerate() {
*byte = (index as u8).wrapping_mul(37).wrapping_add(11); *byte = (index as u8).wrapping_mul(37).wrapping_add(11);
} }
let hash_table_ptr = hash_table.as_mut_ptr();
let base_ptr = input.as_ptr();
let next_to_update = 17u32;
let min_match = 4u32;
let projection = ZSTD_rust_loadDictionaryContentFastTableState { let projection = ZSTD_rust_loadDictionaryContentFastTableState {
hash_table: hash_table.as_mut_ptr(), hash_table: &hash_table_ptr,
base: input.as_ptr(), base: &base_ptr,
next_to_update: 17, next_to_update: &next_to_update,
hash_log, hash_log: &hash_log,
min_match: 4, min_match: &min_match,
full_table_load: 1, full_table_load: 1,
for_cdict: 1, for_cdict: 1,
}; };
unsafe { unsafe {
ZSTD_rust_fillHashTable( fill_fast_dictionary_table(&projection, input.as_ptr().wrapping_add(input.len()).cast())
projection.hash_table, }
projection.base,
projection.next_to_update,
input.as_ptr().wrapping_add(input.len()).cast(),
projection.hash_log,
projection.min_match,
projection.full_table_load,
projection.for_cdict,
)
};
assert!(hash_table.iter().any(|&entry| entry != 0)); assert!(hash_table.iter().any(|&entry| entry != 0));
assert_eq!(projection.next_to_update, 17); assert_eq!(unsafe { *projection.next_to_update }, 17);
assert_eq!(projection.full_table_load, 1); assert_eq!(projection.full_table_load, 1);
assert_eq!(projection.for_cdict, 1); assert_eq!(projection.for_cdict, 1);
} }