AtlatestRepositorycourier
1
#!/usr/bin/env python32
"""Leader-courier multi-relay burst repro.4
Reproduces the GC heap-corruption SIGSEGV tracked in task5
`fix-courier-leader-multi-relay-crash`: connects N worker sockets6
simultaneously via a threading.Barrier, each sends K messages then7
closes, and we close the leader's stdin. The buggy pre-fix leader8
segfaults inside Sigil's GC mark phase (shadow_stack parent=PAIR with9
garbage pointer) 3-15 seconds after stdin close.11
Usage:12
COURIER_BIN=/path/to/courier python3 test-leader-multi-relay-stress.py14
Exit 0 = pass (leader shut down cleanly); exit 1 = crash reproduced.16
Regression contract: a fixed leader must pass this test 20 runs in a row17
with zero crashes. SIGIL_GC_CRASH_DEBUG=1 on the binary prints root/18
parent info to stderr on the bad mark.19
"""20
import json, os, socket, subprocess, sys, tempfile, threading, time, shutil, select22
COURIER = os.environ.get("COURIER_BIN",23
"/home/daviwil/Projects/Code/sigil/courier/build/release/bin/courier")24
N_RELAYS = int(os.environ.get("N_RELAYS", "5"))25
N_MSGS = int(os.environ.get("N_MSGS", "50"))26
RUNS = int(os.environ.get("RUNS", "1"))27
SHUTDOWN_WAIT = float(os.environ.get("SHUTDOWN_WAIT", "20"))29
def send(p, msg):30
try: p.stdin.write((json.dumps(msg) + "\n").encode()); p.stdin.flush(); return True31
except (BrokenPipeError, OSError): return False33
def read_line(p, timeout=3.0):34
end = time.time() + timeout35
while time.time() < end:36
r, _, _ = select.select([p.stdout], [], [], max(0.01, end - time.time()))37
if r:38
line = p.stdout.readline()39
return line if line else None40
return None42
def one_run(idx):43
relay_dir = tempfile.mkdtemp(prefix=f"courier-stress-{idx}-")44
core_dir = tempfile.mkdtemp(prefix=f"courier-core-{idx}-")45
env = os.environ.copy()46
env["COURIER_RELAY_DIR"] = relay_dir47
wrapper = f"cd {core_dir}; ulimit -c unlimited; exec {COURIER}"48
p = subprocess.Popen(["bash", "-c", wrapper],49
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,50
env=env)52
send(p, {"jsonrpc":"2.0","id":1,"method":"initialize",53
"params":{"protocolVersion":"2024-11-05","capabilities":{},54
"clientInfo":{"name":"stress","version":"1"}}})55
if not read_line(p, 5):56
p.kill(); p.wait(); return "no-init"57
send(p, {"jsonrpc":"2.0","method":"notifications/initialized"})59
names = [f"stress-{idx}-{i}" for i in range(N_RELAYS)]60
for i, name in enumerate(names):61
send(p, {"jsonrpc":"2.0","id":10+i,"method":"tools/call",62
"params":{"name":"create-relay","arguments":{"name":name}}})63
read_line(p, 3)65
barrier = threading.Barrier(N_RELAYS)66
def wfn(name):67
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)68
try: s.connect(os.path.join(relay_dir, name + ".sock"))69
except FileNotFoundError: return70
barrier.wait()71
for k in range(N_MSGS):72
try:73
s.sendall((json.dumps({"type":"message","sender":"worker",74
"text":f"burst {name} {k}"}) + "\n").encode())75
except: break76
time.sleep(0.2)77
s.close()79
ts = [threading.Thread(target=wfn, args=(n,)) for n in names]80
for t in ts: t.start()81
for t in ts: t.join(10)82
time.sleep(1.0)84
try: p.stdin.close()85
except: pass86
try: p.wait(timeout=SHUTDOWN_WAIT)87
except subprocess.TimeoutExpired:88
p.kill(); p.wait()89
result = "hang"90
else:91
result = "crash" if p.returncode < 0 else "ok"93
cores = []94
try: cores = [f for f in os.listdir(core_dir) if f.startswith("core")]95
except: pass96
print(f"[run {idx}] result={result} exit={p.returncode} cores={len(cores)}",97
flush=True)98
if not cores: shutil.rmtree(core_dir, ignore_errors=True)99
shutil.rmtree(relay_dir, ignore_errors=True)100
return result102
if __name__ == "__main__":103
crashes = 0104
for i in range(RUNS):105
if one_run(i) == "crash":106
crashes += 1107
print(f"=== {crashes}/{RUNS} crashes ===")108
sys.exit(1 if crashes > 0 else 0)