Skip to content

Make dispose idempotent and reject use after dispose - #38

Open
dstebila wants to merge 1 commit into
masterfrom
ds-fix-double-dispose
Open

dstebila wants to merge 1 commit into
masterfrom
ds-fix-double-dispose

Conversation

@dstebila

@dstebila dstebila commented Nov 2, 2026

Copy link
Copy Markdown
Member

dispose_sig() and dispose_KEM() free the native liboqs object but leave its pointer in the Java object. Calling dispose twice frees the object twice, which crashes the process. Calling any other method after dispose uses freed memory. Both happen only through the calling application's own sequence of calls.

Changes

  • The native free functions clear the handle after freeing and do nothing if it is already clear, so a second dispose is harmless.
  • These methods throw RuntimeException("Signature has been disposed") or RuntimeException("KeyEncapsulation has been disposed") once the object has been disposed:
    • Signature: generate_keypair, and both overloads of sign and verify.
    • KeyEncapsulation: both overloads of generate_keypair, plus encap_secret and decap_secret.

This does not make it safe to dispose an object in one thread while another thread is using it.

Testing

There are 4 new tests in SigTest and KEMTest. Before the fix, the double-dispose tests crashed the JVM, and the use-after-dispose tests found that no exception was thrown.

Tested locally with mvn package on macOS (arm64, JDK 27) against liboqs main (8c41c85), where all the new tests pass. The fix does not depend on how liboqs handles a NULL object.

Until #36 is merged, three existing testAllSigs cases fail on both this branch and master, for ML-DSA-44-extmu, ML-DSA-65-extmu and ML-DSA-87-extmu. liboqs signs with these only when the message is the 64-byte μ, and the test signs a 27-byte message. Merged with #36, mvn package passes with no errors.

This PR is independent of #37, and the two merge cleanly together.

🤖 Generated with Claude Code

dispose_sig() and dispose_KEM() freed the native liboqs object but left
its pointer in the Java object. Calling dispose twice freed it twice,
and calling any other method afterwards used freed memory.

Clear the handle when freeing, skip the free if it is already clear,
and throw a RuntimeException from methods that need the native object
once it has been disposed.

Signed-off-by: Douglas Stebila <dstebila@uwaterloo.ca>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@jimouris jimouris left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Left some nits from an AI pass.

Comment thread src/main/c/Signature.c
if (sig == NULL) return;
OQS_SIG_free(sig);
// Clear the handle so that the freed object cannot be used again
setHandle(env, obj, NULL, "native_sig_handle_");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😬 we had missed this, good catch.

*/
private void check_not_disposed() throws RuntimeException {
if (native_sig_handle_ == 0) {
throw new RuntimeException("Signature has been disposed");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(🤖 pass) Nit: IllegalStateException fits use-after-dispose better than a plain RuntimeException.

If the contract is that a disposed object is fully unusable, print_details(), print_signature(), and the export-key methods should call this too. They do not SIGSEGV after a normal dispose (alg_details_ is already cached; the secret is only wiped), but they still succeed while sign/verify throw.

*/
private void check_not_disposed() throws RuntimeException {
if (native_kem_handle_ == 0) {
throw new RuntimeException("KeyEncapsulation has been disposed");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as on Signature: consider IllegalStateException, and print_details() / export_* if you want dispose to apply to the whole object.

assertDisposed(() -> signer.verify(message, signature, context, signer_public_key));
}

private static void assertDisposed(Executable executable) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If print/export should also fail after dispose, assert that here. Now they still succeed on cached/wiped Java state.

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.

2 participants