Skip to content

Commit 5678a96

Browse files
ljmf00-wekaioclaude
andcommitted
druntime: add regression test for the fiber-switch GC scan hole
Covers issue #23576 on both affected architectures. The test parks the sole reference to a canary object (with a 4 MB payload) in the first callee-saved FP register (d8 on AArch64, fs0 on LoongArch64), XOR-obfuscating every other copy and scrubbing the stack and scratch registers, then yields the fiber, forces collections and heap churn from the main context, resumes, and asserts the canary survived with its payload intact. On a druntime without the fix the canary is collected while the fiber is suspended and the test fails its assertion. The register-parking helpers live in a separate assembly file rather than inline asm so the test does not depend on any compiler's inline-asm dialect; the file compiles to an empty object on other architectures and the test itself reduces to an empty main there. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent fbd433a commit 5678a96

3 files changed

Lines changed: 265 additions & 1 deletion

File tree

druntime/test/thread/Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,18 @@ TESTS := tlsgc_sections test_import tlsstack filterthrownglobal filterthrownmeth
66
ifeq ($(OS),windows)
77
TESTS += winfiber
88
else
9-
TESTS += fiber_guard_page external_threads
9+
TESTS += fiber_guard_page external_threads fiber_fpreg_gc_scan
1010
endif
1111

1212
include ../common.mak
1313

14+
# no-op on other architectures; the asm helpers live in a separate .S file
15+
$(ROOT)/fiber_fpreg_gc_scan$(DOTEXE): private extra_sources = $(ROOT)/fiber_gc_scan_asm$(DOTOBJ)
16+
$(ROOT)/fiber_fpreg_gc_scan$(DOTEXE): $(ROOT)/fiber_gc_scan_asm$(DOTOBJ)
17+
18+
$(ROOT)/fiber_gc_scan_asm$(DOTOBJ): $(SRC)/fiber_gc_scan_asm.S | $(OBJDIR)
19+
$(COMPILE.c) $(OUTPUT_OPTION) $<
20+
1421
# segfault || bus error (OSX)
1522
$(ROOT)/fiber_guard_page.done: $(ROOT)/%.done : $(ROOT)/%$(DOTEXE)
1623
@echo Testing $*
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Fiber-switch GC scan regression test for callee-saved FP registers.
2+
//
3+
// On AArch64 (d8-d15) and LoongArch64 (fs0-fs7), the callee-saved FP
4+
// registers may hold a live GC pointer across a suspension point
5+
// (e.g. fmov d8, x0). fiber_switchContext used to report sp + 9*8 as
6+
// the stack top, hiding the saved return address and FP registers
7+
// from the GC scan - a pointer whose only copy sat in a saved FP
8+
// register could be collected while the fiber was suspended.
9+
//
10+
// The test parks the sole reference to a canary object in the first
11+
// callee-saved FP register (all other copies XOR-obfuscated, via
12+
// helpers in fiber_gc_scan_asm.S), yields, forces collections plus
13+
// heap churn from the main context, then resumes and checks the
14+
// canary survived.
15+
16+
version (AArch64) version = TestFiberFpRegGcScan;
17+
else version (LoongArch64) version = TestFiberFpRegGcScan;
18+
19+
version (TestFiberFpRegGcScan)
20+
{
21+
import core.memory : GC;
22+
import core.thread : Fiber;
23+
24+
extern (C) nothrow @nogc
25+
{
26+
void park_fpreg(size_t obf, size_t key);
27+
size_t read_fpreg();
28+
void scrub_regs();
29+
void compiler_barrier(void* p);
30+
}
31+
32+
enum size_t MAGIC = 0xDEAD_BEEF_CAFE_F00D;
33+
enum size_t KEY = 0xA5A5_A5A5_A5A5_A5A5;
34+
enum PAYLOAD_WORDS = (4 * 1024 * 1024) / size_t.sizeof;
35+
36+
class Canary
37+
{
38+
__gshared bool collected;
39+
size_t[] payload;
40+
~this() { collected = true; }
41+
}
42+
43+
// Returns the canary address XOR-obfuscated so the raw pointer has
44+
// no GC-visible copy outside the fiber's saved FP register.
45+
size_t makeCanary()
46+
{
47+
pragma(inline, false);
48+
auto c = new Canary;
49+
c.payload = new size_t[PAYLOAD_WORDS];
50+
c.payload[] = MAGIC;
51+
return (cast(size_t) cast(void*) c) ^ KEY;
52+
}
53+
54+
// Zero the stack below the current frame to wipe stale spills of
55+
// the raw pointer left behind by makeCanary.
56+
void scrubStack()
57+
{
58+
pragma(inline, false);
59+
size_t[1024] z = 0;
60+
compiler_barrier(z.ptr);
61+
}
62+
63+
__gshared bool sawCollected;
64+
__gshared bool payloadIntact;
65+
66+
void fiberFunc()
67+
{
68+
size_t obf = makeCanary();
69+
70+
// Deobfuscate straight into the FP register; the raw pointer
71+
// exists nowhere else.
72+
park_fpreg(obf, KEY);
73+
obf = 0;
74+
scrubStack();
75+
76+
Fiber.yield();
77+
78+
auto c = cast(Canary) cast(void*) read_fpreg();
79+
sawCollected = Canary.collected;
80+
if (!sawCollected)
81+
payloadIntact = c.payload.length == PAYLOAD_WORDS
82+
&& c.payload[0] == MAGIC && c.payload[$ - 1] == MAGIC;
83+
}
84+
85+
void main()
86+
{
87+
auto fib = new Fiber(&fiberFunc);
88+
fib.call();
89+
90+
// The canary now lives only in the suspended fiber's saved FP
91+
// register.
92+
scrub_regs();
93+
GC.collect();
94+
foreach (i; 0 .. 8)
95+
{
96+
auto junk = new size_t[PAYLOAD_WORDS];
97+
junk[] = 0x0101_0101_0101_0101;
98+
compiler_barrier(junk.ptr);
99+
}
100+
GC.collect();
101+
102+
fib.call();
103+
assert(!sawCollected, "canary collected: saved FP register was hidden from the GC scan");
104+
assert(payloadIntact, "canary payload corrupted");
105+
}
106+
}
107+
else
108+
{
109+
void main() {} // only meaningful on AArch64 / LoongArch64
110+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/* Register-parking helpers for the fiber_fpreg_gc_scan test.
2+
*
3+
* Kept in a separate assembly file so the test does not depend on any
4+
* compiler's inline-asm dialect. Only meaningful on architectures
5+
* with callee-saved FP registers covered by the test (AArch64,
6+
* LoongArch64); compiles to an empty object elsewhere.
7+
*/
8+
9+
#if (__linux__ || __FreeBSD__ || __NetBSD__ || __OpenBSD__ || __DragonFly__) && __ELF__
10+
.section .note.GNU-stack,"",%progbits
11+
#endif
12+
13+
#ifdef __USER_LABEL_PREFIX__
14+
#define GLUE2(a, b) a ## b
15+
#define GLUE(a, b) GLUE2(a, b)
16+
#define CSYM(name) GLUE(__USER_LABEL_PREFIX__, name)
17+
#else
18+
#define CSYM(name) name
19+
#endif
20+
21+
#if defined(__aarch64__)
22+
23+
/* void park_fpreg(size_t obf, size_t key)
24+
*
25+
* Deliberately violates the ABI: leaves obf ^ key parked in the
26+
* callee-saved d8 and returns, emulating a compiler that spilled a GC
27+
* pointer into an FP register across a suspension point.
28+
*/
29+
.text
30+
.global CSYM(park_fpreg)
31+
.p2align 2
32+
#ifndef __APPLE__
33+
.type park_fpreg, %function
34+
#endif
35+
CSYM(park_fpreg):
36+
eor x9, x0, x1
37+
fmov d8, x9
38+
mov x9, xzr
39+
ret
40+
41+
/* size_t read_fpreg(void) */
42+
.text
43+
.global CSYM(read_fpreg)
44+
.p2align 2
45+
#ifndef __APPLE__
46+
.type read_fpreg, %function
47+
#endif
48+
CSYM(read_fpreg):
49+
fmov x0, d8
50+
ret
51+
52+
/* void scrub_regs(void)
53+
*
54+
* Clear scratch registers that might still hold a stale copy of the
55+
* canary pointer.
56+
*/
57+
.text
58+
.global CSYM(scrub_regs)
59+
.p2align 2
60+
#ifndef __APPLE__
61+
.type scrub_regs, %function
62+
#endif
63+
CSYM(scrub_regs):
64+
mov x9, xzr
65+
mov x10, xzr
66+
mov x11, xzr
67+
mov x12, xzr
68+
mov x13, xzr
69+
mov x14, xzr
70+
mov x15, xzr
71+
ret
72+
73+
/* void compiler_barrier(void *p)
74+
*
75+
* Opaque sink: keeps the pointed-to memory alive and ordered as far as
76+
* the calling compiler can tell.
77+
*/
78+
.text
79+
.global CSYM(compiler_barrier)
80+
.p2align 2
81+
#ifndef __APPLE__
82+
.type compiler_barrier, %function
83+
#endif
84+
CSYM(compiler_barrier):
85+
ret
86+
87+
#elif defined(__loongarch64)
88+
89+
/* void park_fpreg(size_t obf, size_t key)
90+
*
91+
* Deliberately violates the ABI: leaves obf ^ key parked in the
92+
* callee-saved fs0 and returns, emulating a compiler that spilled a
93+
* GC pointer into an FP register across a suspension point.
94+
*/
95+
.text
96+
.globl CSYM(park_fpreg)
97+
.p2align 2
98+
.type park_fpreg, %function
99+
CSYM(park_fpreg):
100+
xor $t0, $a0, $a1
101+
movgr2fr.d $fs0, $t0
102+
move $t0, $zero
103+
jr $ra
104+
105+
/* size_t read_fpreg(void) */
106+
.text
107+
.globl CSYM(read_fpreg)
108+
.p2align 2
109+
.type read_fpreg, %function
110+
CSYM(read_fpreg):
111+
movfr2gr.d $a0, $fs0
112+
jr $ra
113+
114+
/* void scrub_regs(void)
115+
*
116+
* Clear scratch registers that might still hold a stale copy of the
117+
* canary pointer.
118+
*/
119+
.text
120+
.globl CSYM(scrub_regs)
121+
.p2align 2
122+
.type scrub_regs, %function
123+
CSYM(scrub_regs):
124+
move $t0, $zero
125+
move $t1, $zero
126+
move $t2, $zero
127+
move $t3, $zero
128+
move $t4, $zero
129+
move $t5, $zero
130+
move $t6, $zero
131+
move $t7, $zero
132+
move $t8, $zero
133+
jr $ra
134+
135+
/* void compiler_barrier(void *p)
136+
*
137+
* Opaque sink: keeps the pointed-to memory alive and ordered as far as
138+
* the calling compiler can tell.
139+
*/
140+
.text
141+
.globl CSYM(compiler_barrier)
142+
.p2align 2
143+
.type compiler_barrier, %function
144+
CSYM(compiler_barrier):
145+
jr $ra
146+
147+
#endif /* __aarch64__ / __loongarch64 */

0 commit comments

Comments
 (0)