Skip to content

fix: free Rust-owned FFI strings to stop native memory leak - #1141

Merged
sauraww merged 1 commit into
mainfrom
fix/native-resolver-ffi-string-leak
Aug 31, 2026
Merged

fix: free Rust-owned FFI strings to stop native memory leak#1141
sauraww merged 1 commit into
mainfrom
fix/native-resolver-ffi-string-leak

Conversation

@gyash1512

@gyash1512 gyash1512 commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Problem

The JavaScript client's native resolver leaks native (Rust-owned) memory on every FFI call that returns a string. The five string-returning functions were declared with a char* return type, so koffi auto-decodes the return into a JS string (a copy) and discards the original pointer. That makes typeof result === "string" always true, which means the core_free_string(result) calls are dead code and the Rust CString (CString::into_raw) is never freed.

Every evaluation leaks the full returned string (for core_get_resolved_config, that's the entire resolved config, ~KBs). In a long-running, high-throughput consumer this grows the process's native heap linearly until it OOMs. Confirmed with jemalloc heap profiling (jeprof diff): alloc::ffi::c_str::CString::_from_vec_unchecked, reached via koffi → libsuperposition_core, accounted for 90%+ of native heap growth. Present in every published version through the latest.

Solution

Use koffi's disposable type — the documented idiom for freeing heap-allocated strings returned from C. Register a named type OwnedStr derived from str with core_free_string as its free callback, then declare the five owned-string functions to return OwnedStr:

this.lib.core_free_string = this.lib.func("void core_free_string(char*)");
if (!ownedStrTypeRegistered) {
    koffi.disposable("OwnedStr", "str", this.lib.core_free_string);
    ownedStrTypeRegistered = true;
}
this.lib.core_get_resolved_config = this.lib.func("OwnedStr core_get_resolved_config(...)");

koffi converts the returned char* to a JS string and then automatically calls core_free_string on the original Rust pointer, so the native allocation is freed instead of leaked.

Changes in clients/javascript/bindings/native-resolver.ts:

  • Declare core_get_resolved_config, core_get_applicable_variants, core_parse_toml_config, core_parse_json_config, core_provider_cache_eval_config to return OwnedStr (was char*).
  • Register the OwnedStr disposable type once via a module-level ownedStrTypeRegistered guard (koffi type names are process-global, so registering per-instance throws Duplicate type name).
  • Remove the now-unnecessary manual decode/free branches in the function bodies — the return value is a plain string.
  • core_free_string's own char* argument type is unchanged (it's an input pointer, correct as-is).

No behavior change to return values — callers still receive the same string/parsed object; the pointer is now freed instead of leaked.

Environment variable changes

None.

Pre-deployment activity

  • Rebuild the JS client workspaces so dist/ reflects the source change: npm run build -w bindings -w sdk -w open-feature-provider.
  • Publish a new superposition-provider version (this ships in dist/index.esm.js / dist/index.js).
  • Consumers must bump to the new version (or apply it via pnpm patch until they can upgrade).

Post-deployment activity

  • On a consumer running the native path, watch RSS/native heap over a few hours — it should stay flat instead of climbing to the memory limit; no OOM/restarts.
  • (Optional) Verify with a jemalloc jeprof diff that CString::_from_vec_unchecked no longer dominates growth.

API changes

None — no public API, request, or response shape changes. (FFI return types are internal to the binding.)

Endpoint Method Request body Response Body
N/A N/A N/A N/A

Possible Issues in the future

  • koffi dependency: the fix relies on koffi's disposable type calling the free callback on the original pointer after C→JS conversion. Pin/verify koffi behavior on major upgrades.
  • Ownership contract: the disposable calls core_free_string on every returned pointer, which assumes the Rust side always returns an owned CString::into_raw pointer. If a function is ever changed to return a borrowed/static pointer, this would become an invalid free — keep the OwnedStr return paired with into_raw-style ownership.
  • Debug logging (pre-existing, not addressed here): these methods still console.log per call (including result/param dumps). Not a leak, but noisy/costly at high volume — worth removing in a follow-up.

Copilot AI lite review requested due to automatic review settings August 31, 2026 06:30
@gyash1512
gyash1512 requested a review from a team as a code owner August 31, 2026 06:30
@semanticdiff-com

semanticdiff-com Bot commented Aug 31, 2026

Copy link
Copy Markdown

Review changes with  SemanticDiff

Changed Files
File Status
  clients/javascript/bindings/native-resolver.ts  34% smaller

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 0c905d71-dbde-4cd0-8fd9-bbd580280508

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 29f1aa07-6fa2-4159-9900-eb7ebb751de9

📥 Commits

Reviewing files that changed from the base of the PR and between 3824be2 and 604911f.

📒 Files selected for processing (1)
  • clients/javascript/bindings/native-resolver.ts

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.


Walkthrough

The JavaScript native resolver now declares five FFI results as void*. All five non-string result paths decode through koffi.decode.

Changes

Native resolver FFI updates

Layer / File(s) Summary
FFI return types and result decoding
clients/javascript/bindings/native-resolver.ts
Five FFI declarations now use void* return types. The corresponding result paths use koffi.decode for string decoding.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 60491

This localized binding change preserves the JavaScript client’s returned values while freeing Rust-owned native strings to prevent memory growth. No actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: ayushjain17, avi892nash

Poem

A rabbit checked the pointer trail

And watched five native calls set sail
With void* in their gentle flow
koffi.decode makes strings show
The resolver hops with fewer snags

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 1…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: freeing Rust-owned FFI strings to prevent a native memory leak.
Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 1 files.

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/native-resolver-ffi-string-leak

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@ayushjain17

ayushjain17 commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

@gyash1512 tests are failing with segmentation fault

> superposition-bindings@0.0.1 build
> tsc


======================================================================
  SUPERPOSITION TOML PARSING - JAVASCRIPT/TYPESCRIPT BINDING TESTS
======================================================================

======================================================================
  TEST 1: Parse TOML Configuration
======================================================================
Using native library from package: /home/runner/_work/superposition/superposition/clients/javascript/bindings/dist/native-lib/libsuperposition_core-x86_64-unknown-linux-gnu.so
bash: line 1: 43035 Segmentation fault      (core dumped) node dist/test-toml.js
make[1]: *** [makefile:435: bindings-test] Error 139
make[1]: Leaving directory '/home/runner/_work/superposition/superposition'
make: *** [makefile:242: test] Error 2

@gyash1512
gyash1512 force-pushed the fix/native-resolver-ffi-string-leak branch from 700e6c9 to 975e949 Compare August 31, 2026 11:45
@ayushjain17

Copy link
Copy Markdown
Collaborator

can you please update the description with the latest changes

@sauraww

sauraww commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Possible to resolve the conflicts ?

@gyash1512
gyash1512 force-pushed the fix/native-resolver-ffi-string-leak branch from 975e949 to 405d78f Compare August 31, 2026 20:50
@gyash1512

Copy link
Copy Markdown
Collaborator Author

Possible to resolve the conflicts ?

Resolved @sauraww

@sauraww
sauraww added this pull request to the merge queue Aug 31, 2026
Merged via the queue into main with commit 240a732 Aug 31, 2026
25 of 39 checks passed
@sauraww
sauraww deleted the fix/native-resolver-ffi-string-leak branch August 31, 2026 21:37
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