Skip to content

fix(runtime): drop an unmapped page's record as the runtime's own work - #245

Merged
SizzleUnrlsd merged 5 commits into
mainfrom
fix/munmap-runtime-scope
Nov 5, 2026
Merged

SizzleUnrlsd merged 5 commits into
mainfrom
fix/munmap-runtime-scope

Conversation

@SizzleUnrlsd

@SizzleUnrlsd SizzleUnrlsd commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Refs #243

Summary

On Windows, __ct_munmap dropped the freed record of a page with the allocation table's lock (ct_alloc_lock) held, outside a runtime scope. Erasing the record frees a node of the table. When that release reaches the runtime's entry points, as through the copies of the library's inline functions that an instrumented module compiles (#197), the entry point opens a scope and takes ct_alloc_lock again, from the thread that holds it: SRW locks are not recursive, and the unmap never returns.

__ct_munmap now runs in a runtime scope (CtRuntimeScope), as ct_release_tracked_pointer and ct_autofree_impl already do around the same releases. The entry point that the node's release reaches sees the runtime's own work and passes it on without taking the lock. The diff to the runtime is the scope and its comment, in src/runtime/windows/ct_runtime_alloc.cpp.

The POSIX runtime does not take this path: its table marks a dropped record as a tombstone in place and frees nothing.

Evidence

The tests ran in the four Windows jobs of LLVM versions (LLVM 19, 20, 22, 23), on main and on this branch.

Run Branch, head Sources Windows jobs
42100512865 probe/243-red, 5cc839e main (c11f8a5) with the test DLL's new function and the two tests below, without the fix 246/247 in each job: ADllWhoseOperatorDeleteFreesThroughItsCopyUnmapsAPage fails with exit status 7, after 20.01 to 20.02 seconds; the control passes.
42100593457 this branch, fef03fd this branch 247/247 in each job; the 15 jobs of LLVM versions pass, and so do Build, clang-format, license-compliance and the commit conventions.

#243's first probe (probe/munmap-forget-lock, 381e187, run 41341737799) gave the same result on a head of #242.

Tests

The allocating test DLL gains ct_allocating_library_map_and_unmap, which maps a page through its copy of the runtime (__ct_mmap), which records it, and unmaps it (__ct_munmap), which drops the page's record. Two death tests load a DLL in a child process that starts its watchdog first (startWatchdog(), exit status 7 after 20 seconds) and exit with 0 once the unmap returned:

Test DLL What it checks
ADllThatMapsAPageThroughItsCopyUnmapsIt ct_allocating_library, with the C++ library's operators The control: the page is mapped, recorded, unmapped and its record dropped.
ADllWhoseOperatorDeleteFreesThroughItsCopyUnmapsAPage ct_new_delete_through_runtime_library, whose operators new and delete go through the copy, as a model of #197 The node that dropping the record frees comes back to the copy's entry points with the table's lock held, and the unmap still returns.

Limits

The DLL maps a page through its copy of the runtime, which records it,
and unmaps it, which drops the page's record. Built alone and with the
operators new and delete that go through the copy, it lets a test
check that the copy drops the record without waiting on its own lock.

Refs #243
On Windows, __ct_munmap dropped the freed record of a page with the
allocation table's lock held, outside a runtime scope. Erasing the
record frees a node of the table, and that release may reach the
runtime's entry points through the copies of the library's inline
functions that an instrumented module compiles (#197). The entry point
then took the table's lock again from the thread that held it, and the
unmap never returned.

__ct_munmap now runs in a runtime scope, as the other entry points that
release a record under that lock do: the entry point that the node's
release reaches sees the runtime's own work and passes it on without
taking the lock.

Refs #243
A child process loads the allocating test DLL, which maps a page
through its copy of the runtime and unmaps it, and exits with 0 once
the unmap returned. A watchdog, running before the DLL loads, ends the
child with 7 after 20 seconds.

Refs #243
The same child process loads the DLL whose operators new and delete go
through its copy of the runtime. Dropping the page's record frees a
node of the copy's table with the table's lock held, and the node comes
back to the copy's entry points through the DLL's operator delete, as
through an instrumented DLL. The test fails with 7 when the unmap waits
on that lock.

Refs #243
@SizzleUnrlsd SizzleUnrlsd added the bug Something isn't working label Nov 5, 2026
@SizzleUnrlsd SizzleUnrlsd self-assigned this Nov 5, 2026
@SizzleUnrlsd
SizzleUnrlsd marked this pull request as ready for review November 5, 2026 22:19
@SizzleUnrlsd
SizzleUnrlsd merged commit d586c3f into main Nov 5, 2026
25 checks passed
@SizzleUnrlsd
SizzleUnrlsd deleted the fix/munmap-runtime-scope branch November 6, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant