Skip to content

Fix several memory-safety issues in the C API surface - #219

Open
djw8605 wants to merge 1 commit into
scitokens:masterfrom
djw8605:fix/capi-memory-safety
Open

Fix several memory-safety issues in the C API surface#219
djw8605 wants to merge 1 commit into
scitokens:masterfrom
djw8605:fix/capi-memory-safety

Conversation

@djw8605

@djw8605 djw8605 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

A bundle of small, related memory-safety fixes found while auditing the C API:

Out-of-bounds / uninitialized memory

  • scitoken_free_string_list: the do/while read value[idx] after the terminating NULL — for an empty (terminator-only) list this reads one element past the allocation. Rewritten as a bounded loop; NULL input is now a no-op.
  • convert_acls: malloc'd array was filled left-to-right with the tail (including the terminator) uninitialized; on strdup failure the cleanup handed uninitialized pointers to enforcer_acl_free (wild frees). Now uses checked calloc and checks each strdup before proceeding.
  • scitoken_get_claim_string_list: unchecked malloc → checked calloc (partially-built lists always terminated).

Null-pointer UB

  • scitoken_get_claim_string dereferenced a NULL token handle — the only getter without the standard NULL check. Also rejects null key/value.
  • scitoken_create(NULL) bound a C++ reference through a null pointer (UB; the test suite itself calls this). Now constructs with an empty key, matching the header's documented "NULL for unsigned token".
  • sqlite3_column_text results were passed to std::string(const char*) without a null check in get_public_keys_from_db and get_jwks_metadata (the third call site already checks).

Dangling reference

  • SciToken stored its signing key as SciTokenKey&. scitoken_deserialize passes a stack-local key, so the returned token held a dangling reference — scitoken_serialize after deserialize read dead stack memory. Same for callers destroying their key handle before serializing. The key is now owned by value (four small strings; keys have no mutators, so no behavior change).
  • SciToken::get_claim* used operator[], silently inserting empty claims into the token on every lookup miss; now uses find().

Iterator UB

  • Enforcer::scope_validator walked its iterator past scope.end() on trailing whitespace, and then emitted a bogus ACL with an empty authorization string into generate_acls output.

Testing

  • New regression tests: EnforcerScopeTrailingSpaceTest (exactly one ACL from "read:/foo ") and EmptyStringListTest (empty list round-trip + free).
  • ctest unit, env_config, and monitoring suites pass.

🤖 Generated with Claude Code

- scitoken_free_string_list read value[idx] after freeing value[idx-1],
  running one element past the allocation for an empty (terminator-only)
  list; rewrite as a bounded loop and make null input a no-op.
- convert_acls left the ACL array tail (including the terminator)
  uninitialized while filling it; on a strdup failure the cleanup path
  handed uninitialized pointers to enforcer_acl_free.  Use calloc, check
  the allocation, and check each strdup before moving on.
- scitoken_get_claim_string_list: check the list allocation and use
  calloc so partially-built lists are always terminated.
- scitoken_get_claim_string dereferenced a NULL token handle (every
  sibling function rejects NULL); also reject null key/value pointers.
- scitoken_create(NULL) bound a C++ reference through a null pointer
  (undefined behavior); construct with an empty key instead, matching
  the documented 'NULL for unsigned token' usage.
- SciToken stored its signing key as a reference; the internal key in
  scitoken_deserialize lives on the stack, leaving a dangling reference
  in the returned token (serialize() after deserialize read freed
  stack memory).  Own the key by value.
- SciToken::get_claim*/get_claim_list used operator[], silently
  inserting empty claims into the token on lookup misses; use find().
- sqlite3_column_text results were passed to std::string's const char*
  constructor without a null check in get_public_keys_from_db and
  get_jwks_metadata (undefined behavior on OOM/NULL).
- Enforcer::scope_validator walked the iterator past scope.end() on
  trailing whitespace and then emitted a bogus ACL with an empty
  authorization string.

Adds regression tests for the trailing-whitespace scope and the
empty string list round-trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@djw8605 djw8605 added the ai-gen label Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant