Skip to content

fix: wire NativePointerWithRefCount into ref'd compress/decompress dictionaries - #119

Merged
dfa1 merged 1 commit into
mainfrom
fix/refdictionary-use-after-free
Sep 11, 2026
Merged

dfa1 merged 1 commit into
mainfrom
fix/refdictionary-use-after-free

Conversation

@dfa1

@dfa1 dfa1 commented Sep 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • ZstdCompressContext#refDictionary / ZstdDecompressContext#refDictionary attached a raw native pointer to a borrowed ZstdCompressDictionary/ZstdDecompressDictionary. Closing that dictionary while a context still referenced it freed the pointer out from under the context — a use-after-free on every subsequent compress/decompress call.
  • Both dictionary classes now extend NativePointerWithRefCount (added in a prior commit but not yet wired in) instead of NativeObject.
  • Both contexts retain() the dictionary on refDictionary(...), release() the previous one on replace/clear, release on a parameter reset(...), and release in tryClose() — so the native dictionary is only freed once every borrower, including the dictionary's own constructor reference, has let go.
  • Follow-up commit from code review: fixed two more gaps in that same bookkeeping — (1) refDictionary() on an already-closed context leaked a retain instead of failing cleanly, (2) loadDictionary()/refPrefix() natively supersede a previously ref'd CDict/DDict (per zstd.h) but weren't releasing the Java-side retain, delaying when memory could be freed. Also fixed stale docs in docs/how-to.md.

Test plan

  • New RefDictionaryTest: closing a still-referenced compress/decompress dictionary, replacing a referenced dictionary, clearing with null, reset dropping the reference, loadDictionary/refPrefix superseding the reference, refDictionary-on-a-closed-context, and the chaining contract.
  • Verified the core regression test actually catches the bug: ran the identical RefDictionaryTest.java against the pre-fix commit (047958c) in a scratch worktree — it crashes the JVM (Abort trap: 6, exit 134, native heap-corruption abort from the use-after-free). Ran the follow-up tests against the first fix commit (43c0364) — 6 of them fail as expected (assertion failures, not crashes, since that gap was a delayed-free, not a dangling pointer). All pass on the final commit.
  • ./mvnw -pl zstd validate (checkstyle) — clean
  • ./mvnw -pl zstd javadoc:javadoc — clean
  • ./mvnw -pl zstd test — full suite passes

🤖 Generated with Claude Code

…ctionaries

ZstdCompressContext#refDictionary and ZstdDecompressContext#refDictionary
attached a raw pointer to a borrowed dictionary; closing that dictionary
while a context still referenced it freed the pointer out from under the
context, corrupting subsequent compress/decompress calls. Both dictionary
classes now extend NativePointerWithRefCount, and both contexts
retain/release across ref/replace/clear/reset/close so the native
dictionary is only freed once every borrower has let go.

This also closes two narrower gaps in that same bookkeeping: refDictionary()
on an already-closed context used to leak a retain instead of failing
cleanly, and loadDictionary()/refPrefix() natively supersede a previously
ref'd CDict/DDict (per zstd.h) but weren't releasing the Java-side retain,
delaying when memory could be freed. docs/how-to.md is updated to match —
it previously described refDictionary as a plain borrow requiring the
dictionary to outlive the context.

New RefDictionaryTest covers closing a still-referenced dictionary,
replacing one, clearing with null, reset/loadDictionary/refPrefix dropping
the reference, refDictionary on a closed context, and the chaining
contract. Verified against the pre-fix code in a scratch worktree: the
core scenario crashes the JVM (Abort trap: 6, native heap-corruption abort
from the use-after-free); the two narrower-gap tests fail with plain
assertion errors (delayed-free, not a dangling pointer). All pass here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dfa1
dfa1 force-pushed the fix/refdictionary-use-after-free branch from 44b68f3 to 8c8a875 Compare September 11, 2026 20:05
@dfa1
dfa1 merged commit 74b163d into main Sep 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant