Publish unwind plans without a plain write to their pc

A new plan was copied into its slot as a whole struct, which wrote the
slot's pc with a plain store while other threads' lookups read it
atomically: a data race. The insert now copies the fields after pc and
publishes pc only with the release store.

The unwinder test also runs its threaded cases under ThreadSanitizer,
which reports the race without this change.
This commit is contained in:
Chris Tate
2026-10-28 00:36:18 -05:00
parent d5ac31f812
commit d3e8ab41ef
2 changed files with 14 additions and 5 deletions
+4 -2
View File
@@ -566,8 +566,10 @@ static __attribute__((noinline)) const ScrUnwindPlan *scr_unwind_insert(uint64_t
scr_unwind_plan_count++;
ScrUnwindPlan built;
scr_unwind_build(&built, ra);
built.pc = 0;
*slot = built;
/* Lookups read pc atomically, so the plan's other fields are written
* first and pc only by the release store that publishes them. */
memcpy((char *)slot + sizeof slot->pc, (const char *)&built + sizeof built.pc,
sizeof *slot - sizeof slot->pc);
__atomic_store_n(&slot->pc, ra, __ATOMIC_RELEASE);
result = slot;
}
+10 -3
View File
@@ -18,7 +18,7 @@ test.runIf(process.platform === "linux" && process.arch === "x64")(
// .eh_frame_hdr.
const library = join(buildDir, "libtest_unwind_frames.so");
await execFileAsync("clang", ["-O1", "-shared", "-fPIC", "-o", library, ...frames]);
for (const build of ["plain", "asan", "shared"] as const) {
for (const build of ["plain", "asan", "tsan", "shared"] as const) {
const bin = join(buildDir, `test_unwind_${build}`);
await execFileAsync("clang", [
"-std=c11",
@@ -28,6 +28,7 @@ test.runIf(process.platform === "linux" && process.arch === "x64")(
"-D_GNU_SOURCE",
"-DSCR_UNWIND_TESTING",
...(build === "asan" ? ["-fsanitize=address"] : []),
...(build === "tsan" ? ["-fsanitize=thread"] : []),
"-pthread",
"-o",
bin,
@@ -37,8 +38,14 @@ test.runIf(process.platform === "linux" && process.arch === "x64")(
// The library's landing pads and the program share libgcc_s's unwinder.
...(build === "shared" ? ["-shared-libgcc"] : ["-lgcc_eh"]),
]);
const env = { ...process.env, ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1" };
for (const args of [[], ["threads"]]) {
const env = {
...process.env,
ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1",
TSAN_OPTIONS: "halt_on_error=1",
};
// ThreadSanitizer checks the threads' concurrent plan inserts and
// lookups; the libgcc fallback cases run in the other builds.
for (const args of build === "tsan" ? [["threads"]] : [[], ["threads"]]) {
const run = await execFileAsync(bin, args, { env });
expect(run.stdout).toBe("unwind checks passed\n");
}