Skip to content

Keep object handles unique when object ids run out - #255

Merged
Frauschi merged 5 commits into
wolfSSL:masterfrom
LinuxJedi:fix-object-handle-wrap
Oct 19, 2026
Merged

Frauschi merged 5 commits into
wolfSSL:masterfrom
LinuxJedi:fix-object-handle-wrap

Conversation

@LinuxJedi

@LinuxJedi LinuxJedi commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

An object handle is the object id with the onToken flag in bit 28 (OBJ_HANDLE(on, i)), but nothing bounded the id. Session and token objects shared one counter, token->nextObjId, which is stored with the token and never goes back. Once it passed 0x0FFFFFFF the id spilled into the flag. Reproduced on master 427a22c with a debug + ASAN build:

  • A session object got the handle of an existing token object (OBJ_HANDLE(0, 0x10000001) == OBJ_HANDLE(1, 1)), so C_GetAttributeValue on it read the token object.
  • C_DestroyObject on that handle deleted the token object and its store files.
  • A session object whose handle did not collide returned CKR_OBJECT_HANDLE_INVALID.
  • C_CloseSession, C_CloseAllSessions and C_Finalize hit a heap-use-after-free. WP11_Session_RemoveObject picks the list from the handle (since Match the handle when removing an object by handle #252), so it missed the session object and left it linked, and wp11_Session_Final freed it and looped back onto it.
  • At 2^31, nextObjId++ was signed overflow.

This takes about 268M objects. Because every session object used up a stored id for good, a long-running server creating keys per connection could get there.

Commits

Cap object ids so handles stay unique

  • Adds OBJ_ID_MAX (0x0FFFFFFF). WP11_Session_AddObject fails with OBJ_COUNT_E (CKR_FUNCTION_FAILED) once the counter is past it, for both token and session objects.
  • wp11_Token_Load rejects a stored counter that is negative or above OBJ_ID_MAX + 1 (replaced by the third commit). OBJ_ID_MAX + 1 itself is accepted, because that is what a token that has used every id stores.

Give session objects their own id counter

  • Session handles have no onToken flag, so they cannot clash with token handles and do not need ids from the stored counter. Session objects now take ids from slot->nextSessObjId, next to the slot's session list. It is guarded by the token lock and starts at 1 in wp11_Slot_Init, so it starts again on each C_Initialize, when no session objects exist.
  • The stored counter now only moves for token objects, each of which is a token store. Running out of session ids in one process no longer locks the token out of creating objects until it is reinitialised.

Recover the token object counter from the loaded objects

  • Rejecting an out of range counter broke storage-upgrade-test-tpm. v1.3.0 stored neither the token flags nor the counter. With file storage the read runs out of data and falls back, but a TPM NV index is fixed size, so the read returned whatever followed the token data and C_Initialize failed.
  • That counter was never usable anyway. v1.3.0 gave token objects the ids 1, 2, ... and stored them in the objects, while master fell back to 1 (file storage) or took the bytes that followed (TPM storage), so after an upgrade new token objects could get the handles of existing ones.
  • A stored counter outside 1..OBJ_ID_MAX + 1 is now treated like a missing one, and on load the counter is raised above the id of every token object.

Give the test hook declaration C linkage

  • wolfpkcs11/internal.h declares the hook inside extern "C". The test's own declaration did not, so a CC=g++ --enable-debug build would fail to link (Copilot review).

Behaviour changes

  • Once ids run out, object creation returns CKR_FUNCTION_FAILED. Token ids only come back through C_InitToken. Session ids come back on the next C_Initialize.
  • On load, the token object counter is never below the highest stored token object id + 1, whatever was stored. A token upgraded from v1.3.0 no longer reuses the handles of its existing objects.
  • Session object handles start at 1 after each C_Initialize instead of continuing from the token's stored counter.

Tests

Reaching the limit through the API takes hundreds of millions of objects, so this adds a DEBUG_WOLFPKCS11-only hook, WP11_Test_SetNextObjId(slotId, onToken, nextObjId), and tests/object_handle_limit_test.c. The test:

  • fills the token ids and checks the last handle, that the next token object fails, and that session objects can still be created;
  • fills the session ids and checks the handles have no token flag, and that the next session object fails;
  • checks that an EC key pair whose private key does not get an id leaves no public key behind (rollback at the limit);
  • reloads the token with every token id used and checks that its objects are there, token creation still fails and session creation works again;
  • stores an out of range counter, reloads, and checks that the next token object gets the id after the remaining object's, then resets the token so nothing is left in storage.

It runs in --enable-debug builds (the unit-test.yml debug and TPM debug jobs) and skips otherwise.

Testing:

  • With the first commit's version of the test, removing the cap fails 8 checks and ASAN reports the same use-after-free in wp11_Session_Final. Putting session objects back on the shared counter fails 9 checks. Removing the counter recovery on load fails 2 checks: the new token object gets the remaining object's handle and shadows it.
  • All 49 checks pass under ASAN. The non-ECC branch also passes (47 checks). The test also compiles as C and C++ with WOLFPKCS11_NO_STORE, WOLFPKCS11_TPM_STORE and WOLFPKCS11_NO_ENV.
  • Pre-commit hook make test passes on every commit (debug + ASAN and default configs). The TPM store path of the test and the v1.3.0 TPM upgrade have not been run locally. CI covers them.

An object handle is the object id with the onToken flag in bit 28, but
the id counter was never bounded. Once it passed 0x0FFFFFFF the id
spilled into the flag, so a session object could get the handle of a
token object. Reading the session object then read the token object,
C_DestroyObject on it deleted the token object and its store files, and
C_CloseSession, C_CloseAllSessions and C_Finalize hit a
heap-use-after-free: WP11_Session_RemoveObject chose the list from the
handle, left the session object linked, and wp11_Session_Final freed it
and looped back onto it. At 2^31 the counter also overflowed.

WP11_Session_AddObject now fails with OBJ_COUNT_E (CKR_FUNCTION_FAILED)
once the counter is past OBJ_ID_MAX. wp11_Token_Load rejects a stored
counter that is negative or above OBJ_ID_MAX + 1. OBJ_ID_MAX + 1 is
still accepted, because that is what a token that has used every id
stores.

Reaching the limit through the public API takes hundreds of millions
of objects, so add a DEBUG_WOLFPKCS11-only hook, WP11_Test_SetNextObjId,
and object_handle_limit_test. The test fills the ids, checks that
handles stay distinct and that creation fails past the limit, that a
key pair rolled back at the limit leaves no object behind, that a token
which has used every id reloads, and, with file storage, that a stored
counter past the limit is rejected.
Session and token objects took their ids from the one token counter,
which is stored with the token and never goes back. Every session
object a process created used up an id for good, so a busy server
could run the token out of ids, after which no object of either kind
could be created until the token was reinitialised, losing its keys.

Session handles have no onToken flag, so they cannot clash with token
handles and do not need ids from the same counter. Session objects now
take ids from a counter in the slot, next to its session list. It
starts at 1 in wp11_Slot_Init, so it starts again on each
C_Initialize, when no session objects exist. The token counter now
only moves for token objects.

WP11_Test_SetNextObjId takes an onToken flag to pick the counter, and
object_handle_limit_test now checks that running out of token ids does
not stop session objects from being created and that session ids are
available again after C_Initialize.
Copilot AI balanced review requested due to automatic review settings October 16, 2026 10:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The regression test has a C++ linkage defect and can leave corrupted token storage behind in no-environment builds.

2 open findings
What changed in this PR

Prevents object-ID exhaustion from producing invalid or colliding PKCS#11 handles.

Changes:

  • Bounds object IDs and validates persisted counters.
  • Separates session IDs from persistent token IDs.
  • Adds debug-only regression coverage for exhaustion, rollback, and reload behavior.
File Description
wolfpkcs11/​internal.h Declares the debug counter-setting hook.
tests/​object_handle_limit_test.c Tests handle limits and persistence behavior.
tests/​include.am Registers and links the regression test.
src/​internal.c Enforces ID limits and separates session allocation.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/object_handle_limit_test.c
Comment thread tests/object_handle_limit_test.c Outdated
Rejecting a stored counter outside 1..OBJ_ID_MAX + 1 broke tokens
written by v1.3.0. That version stored neither the token flags nor the
counter. With file storage the read runs out of data and falls back,
but a TPM NV index is fixed size, so the read returns whatever follows
the token data and C_Initialize failed (storage-upgrade-test-tpm).

Such a counter was never usable anyway. v1.3.0 gave token objects the
ids 1, 2, ... and stored them in the objects, while the counter fell
back to 1 (file storage) or took the bytes that followed (TPM storage),
so new token objects could get the handles of existing ones.

Treat a stored counter outside the range like a missing one, and raise
the counter above the id of every token object loaded. The test now
stores an out of range counter and checks that the reloaded token gives
its next object the id after the remaining object's, then resets the
token instead of leaving it corrupted in storage.
wolfpkcs11/internal.h declares WP11_Test_SetNextObjId inside an
extern "C" block, but object_handle_limit_test declared it on its own.
Built as C++ (CC=g++ with --enable-debug) that declaration asked for a
mangled symbol the library does not export, so the test failed to
link. Wrap it in the same extern "C" guard.

@Frauschi Frauschi 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.

The cap, the separate session id counter and the counter recovery on load all look right to me. Two small doc comment nits inline.

Comment thread src/internal.c
Comment thread src/internal.c Outdated
WP11_Session_AddObject also returns OBJ_COUNT_E when the slot's
session object ids run out, and WP11_Test_SetNextObjId describes the
load as rejecting an out of range counter, which it now replaces.
@Frauschi
Frauschi merged commit e2a6548 into wolfSSL:master Oct 19, 2026
96 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.

4 participants