Skip to content

Make dispose idempotent and reject use after dispose - #38

Merged
dstebila merged 3 commits into
masterfrom
ds-fix-double-dispose
Nov 3, 2026
Merged

dstebila merged 3 commits 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 IllegalStateException("Signature has been disposed") or IllegalStateException("KeyEncapsulation has been disposed") once the object has been disposed:

    • Signature: generate_keypair, both overloads of sign and verify, export_public_key, export_secret_key, print_signature and print_details.
    • KeyEncapsulation: both overloads of generate_keypair, encap_secret, decap_secret, get_keypair_seed_length, export_public_key, export_secret_key, print_KeyEncapsulation and print_details.

    The export, print and seed-length methods don't use the native object, but they are included so that a disposed object can't be used at all. Otherwise export_secret_key would return the zeroed key. Code that called any of them after dispose will now get an exception.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Switched to IllegalStateException. export_public_key, export_secret_key, print_signature and print_details now throw after dispose too, so a disposed Signature can't be used at all. export_secret_key was the one that mattered most, since it was quietly returning the zeroed key.

The only public method without the check is create_sig_new, because it's public native. Calling it after dispose creates a new native object, so it brings the Signature back to life rather than touching freed memory.

*/
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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same changes here, plus get_keypair_seed_length, which also only reads cached state. create_KEM_new is the same exception as create_sig_new.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done. The use-after-dispose tests in SigTest and KEMTest now check that each of these methods throws IllegalStateException after dispose.

Use IllegalStateException rather than RuntimeException for use after
dispose. Also check in the methods that don't use the native object
(export_public_key, export_secret_key, the print methods and
get_keypair_seed_length), so that a disposed object can't be used at
all. Before this, export_secret_key returned the zeroed secret key.

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

dstebila commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Thanks @jimouris! I've pushed some revisions addressing your comments. If they look good to you, we can then merge this.

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

jimouris commented Nov 3, 2026

Copy link
Copy Markdown
Member

LGTM! Feel free to merge it - I didn't in case you're still making edits as I saw you pushed one more commit after your comment.

@dstebila
dstebila merged commit d46d0ff into master Nov 3, 2026
10 checks 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.

2 participants