Skip to content

Windows runtime: __ct_munmap drops a freed record under the table's lock outside a runtime scope #243

Open

Description

@SizzleUnrlsd

Summary

On Windows, __ct_munmap drops the freed record of a page with the allocation table's lock (ct_alloc_lock) held, outside a runtime scope (CtRuntimeScope). Erasing the record frees a node of the table. When that release reaches the runtime's entry points, as it may through the copies of the library's inline functions that an instrumented module compiles (#197), the entry point takes the same lock again, from the thread that holds it: the call never returns.

The path

In src/runtime/windows/ct_runtime_alloc.cpp, on main and on #242, which does not change these functions:

  1. __ct_munmap takes the lock without a scope and marks the page's record as freed (ct_remove_for_release), then releases the lock.
  2. When the page was recorded, it calls ct_forget_returned_block, which takes the lock again, still without a scope, and calls ct_table_forget_freed.
  3. ct_table_forget_freed erases the record: std::unordered_map::erase frees the record's node with operator delete.
  4. Through a copy of the library's deallocation that calls __ct_free or __ct_delete, the release enters ct_release_tracked_pointer outside a scope: it opens one and takes ct_alloc_lock, which its own thread holds. SRW locks are not recursive.

The other releases of a record under that lock run in a scope: ct_release_tracked_pointer, ct_autofree_impl (which calls ct_forget_returned_block from its scope), and the quarantine's pushes. #242 puts the table's build in a scope of its own, which covers the lookups of __ct_check_bounds and __ct_realloc that take the lock without a scope; those lookups free nothing.

Reproduction

probe/munmap-forget-lock (381e187), on ad71cea, a head of #242, which has a test DLL whose operators new and delete go through its copy of the runtime (__ct_malloc, __ct_free), as a model of #197. The DLL maps a page through its copy (__ct_mmap) and unmaps it (__ct_munmap), in a child process that a watchdog ends with 7 after 20 seconds. Run 41341737799, Windows jobs of LLVM versions:

Job ProbeADllThatAllocatesThroughItsCopyUnmapsAPage (control: the library's own operators) ProbeADllWhoseOperatorDeleteFreesThroughItsCopyUnmapsAPage
Windows / LLVM 19 passes, 0.01 s fails: exit status 7, after 20.02 s
Windows / LLVM 20 passes, 0.01 s fails: exit status 7, after 20.02 s
Windows / LLVM 22 passes, 0.01 s fails: exit status 7, after 20.02 s
Windows / LLVM 23 passes, 0.01 s fails: exit status 7, after 20.02 s

The 236 other tests pass in each job.

Not established

  • Whether an instrumented program reaches this path: it needs the linker to keep, for the runtime, the instrumented copy of the library's deallocation, and a program that unmaps a mapping the runtime recorded. The probe forces both.
  • The POSIX runtime was not checked.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions