Skip to content

Free native string returns after copying them into Vale - #645

Open
nuttyartist wants to merge 2 commits into
ValeLang:masterfrom
nuttyartist:codex/fix-extern-string-return-leak
Open

nuttyartist wants to merge 2 commits into
ValeLang:masterfrom
nuttyartist:codex/fix-extern-string-return-leak

Conversation

@nuttyartist

@nuttyartist nuttyartist commented Sep 13, 2026 •

Copy link
Copy Markdown

AI assisted (GPT-6 Astra Extra High)

Each str + str currently leaks the native ValeStr returned by __vale_addStr. A loop of 1,000 concatenations exits successfully but leaves 1,000 unreachable allocations. Free the native string after receiveHostObjectIntoVale has copied it into Vale, using the existing Linear::dealias helper.

The compiler change is six lines and applies to externs returning str, including conversions, substring, and user-defined externs. It does not change aggregate-return or exported-argument ownership.

Reproduction

Base: 6fb98324c0b919a55812376f4d74cfcbfc7f2db7 (master). Save this as repro.vale:

import stdlib.*;

exported func main() int {
  i = 0;
  total = 0;
  while (i < 1000) {
    s = "Vale " + "ARM64";
    set total = total + len(s);
    set i = i + 1;
  }
  if (total != 10000) { return 1; }
  return 0;
}

With a working compiler package from that revision, set VALEC to its absolute valec path:

"$VALEC" build repro=repro.vale --output_dir out --census false
./out/main
/usr/bin/leaks --atExit -- ./out/main

On macOS ARM64, the program returns 0 but leaks returns 1:

Process ...: 1000 leaks for 32000 total leaked bytes.
STACK OF 1000 INSTANCES OF 'ROOT LEAK: <malloc in ValeStrNew>':
    main
    vale_abi___vale_addStr
    __vale_addStr
    ValeStrNew
    _malloc_zone_malloc_instrumented_or_legacy

Repeat with --census true: same leak, and Vale's own census check still passes. Census counts Vale-managed allocations, not the C allocation made by ValeStrNew. The requested allocation is 15 bytes (4 + 10 + 1); the macOS leak-checking allocator reports 32 bytes per allocation.

Cause and fix

Backend/builtins/strings.c allocates the returned string and frees both incoming string copies. The extern return path in Backend/src/function/expressions/externs.cpp then calls receiveHostObjectIntoVale, which copies the returned bytes into the RC region. Nothing releases the original native buffer; the code even has a dont we have to free here too comment at that point.

The fix releases that buffer after the copy. Optimized LLVM IR now contains free of the exact pointer returned by vale_abi___vale_addStr, after strncpy into the new RC string.

Verification

Measured before/after using the same frontend, stdlib, builtins, driver, and link command; only the backend is replaced:

Program Before: leaked allocations / bytes After
0 concatenations 0 / 0 0 / 0
100 concatenations 100 / 3,200 0 / 0
1,000 concatenations, census off 1,000 / 32,000 0 / 0
1,000 concatenations, census on 1,000 / 32,000 0 / 0
1,000 literal strings, no concatenation 0 / 0 0 / 0
1,000 iterations of eight string-return checks, census off 8,000 / 208,000 0 / 0
Same eight checks, census on 8,000 / 208,000 0 / 0

The eight additional checks cover a C/Vale string-return round trip, i32/i64/float conversions, substring, ASCII conversion, and empty/UTF-8 concatenation. They check exact contents, also exercise int/bool/float/void extern returns, and return 0 both before and after. Normal runs also passed with MallocScribble=1.

All concatenation/control programs returned 0 with the expected checksum. The added regression also checks the resulting string's contents, and checks the native heap with census both off and on:

# Run from the repository root. LLVM 16 is required.
cmake -S Backend -B Backend/build -G Ninja \
  -DLLVM_DIR=/opt/homebrew/opt/llvm@16/lib/cmake/llvm \
  -DCMAKE_POLICY_VERSION_MINIMUM=3.5
cmake --build Backend/build -j8
bash Backend/test/extern_string_return.sh "$VALEC" \
  --backend_path_override "$PWD/Backend/build/backend"

CMAKE_POLICY_VERSION_MINIMUM is needed for CMake 4; no CMake build type was set. No unrelated build-source changes are included. The regression fails on the original backend and passes on the patched backend. Its Linux branch uses Valgrind; Linux execution and the full compiler test suite have not been run here.

Environment: macOS 26.6.2 (25G83), Apple ARM64; Apple clang 21.0.0; LLVM 16.0.6; CMake 4.0.1; Java 17.0.1; frontend built from the base revision with sbt 1.6.2 / Scala 2.12.19 (git lfs pull --include="Frontend/lib/*", then sbt -batch 'set test in assembly := {}' clean assembly in Frontend). The Coordinator was bootstrapped using Vale-Mac-0.2.0.27 and runs as x86_64 under Rosetta; it launches the rebuilt ARM64 backend and clang. All tested program binaries are ARM64. Existing compiler packaging/build instructions apply; no application or library-mode harness is needed.

This branch has not been deployed

No deployments
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