~james/remy

test(native): isolate generated code execution in child processes

commit
5fcf9d2c41a83fa9834748fbfbcaf54837c9c7b3 browse tree
parent
14346e112835db44d5eba27da0ca97089f621a91
author
james <j@cpl0.zip>
signature
Verified ~james signed with a key registered by ~james
date
2026-10-28 12:00 UTC

1 files changed, +51 −6

tests/native.py modified

@@ -1,5 +1,7 @@
11 import ctypes
22 import mmap
3+import multiprocessing
4+import resource
35 from pathlib import Path
46 import platform
57 import random
@@ -48,6 +50,47 @@ def native(code):
4850 return function, image, thunk
4951
5052
53+def worker(connection, code, vectors):
54+ resource.setrlimit(resource.RLIMIT_CORE,(0,0))
55+ try:
56+ function, image, thunk = native(code)
57+ results = [function(*vector) for vector in vectors]
58+ del function
59+ image.close()
60+ thunk.close()
61+ connection.send((True,results))
62+ except BaseException as error:
63+ connection.send((False,repr(error)))
64+ finally:
65+ connection.close()
66+
67+
68+def execute(code, vectors):
69+ context = multiprocessing.get_context('fork')
70+ reader, writer = context.Pipe(duplex=False)
71+ process = context.Process(target=worker,args=(writer,code,vectors))
72+ process.start()
73+ writer.close()
74+ try:
75+ if not reader.poll(30):
76+ raise RuntimeError('native child exceeded 30 seconds')
77+ try:
78+ ok, result = reader.recv()
79+ except EOFError:
80+ process.join(1)
81+ raise RuntimeError(f'native child failed with status {process.exitcode}') from None
82+ process.join(1)
83+ if not ok or process.exitcode != 0:
84+ raise RuntimeError(f'native child failed: {result}, status {process.exitcode}')
85+ return result
86+ finally:
87+ reader.close()
88+ if process.is_alive():
89+ process.terminate()
90+ process.join()
91+ process.close()
92+
93+
5194 def generate(rng, depth, names):
5295 if depth == 0 or rng.randrange(5) == 0:
5396 return rng.choice([0, 1, 63, 64, 65, MASK, 1 << 63, *names])
@@ -63,6 +106,12 @@ def generate(rng, depth, names):
63106 def main():
64107 if platform.system() != "Linux" or platform.machine() != "x86_64":
65108 raise RuntimeError("native differential tests require an x86-64 Linux host")
109+ try:
110+ execute(bytes.fromhex('0f 0b'),[(0,0,0)])
111+ except RuntimeError as error:
112+ assert 'status -4' in str(error), error
113+ else:
114+ raise AssertionError('invalid opcode did not kill the isolated child')
66115 rng = random.Random(0x52454D59)
67116 cases = [(op, "x", "y") for op in OPERATORS]
68117 cases += [(op, "x", 0) for op in ("+", "-", "or", "xor", "shl", "shr")]
@@ -82,20 +131,16 @@ def main():
82131 assert result.returncode == 0, (case, result.stderr)
83132 checked = subprocess.run([ROOT / ".build/remy", "check", binary],capture_output=True,text=True,timeout=30)
84133 assert checked.returncode == 0, (case,checked.stderr)
85- function, image, thunk = native(binary.read_bytes())
86134 vectors = [(a, b, a ^ b) for a in (0, 1, MASK, 1 << 63) for b in (0, 1, 63, 64, 65, MASK)]
87135 vectors += [tuple(rng.getrandbits(64) for _ in range(3)) for _ in range(24)]
88- for x, y, t in vectors:
136+ results = execute(binary.read_bytes(),vectors)
137+ for (x, y, t), actual in zip(vectors,results,strict=True):
89138 expected = evaluate(case, {"x": x, "y": y, "t": t})
90- actual = function(x, y, t)
91139 assert actual == expected, (source(case), x, y, t, expected, actual)
92140 comparisons += 1
93141 result = subprocess.run([ROOT / ".build/remy", "eval", path, str(x), str(y), str(t)],
94142 capture_output=True, text=True, timeout=30)
95143 assert result.returncode == 0 and int(result.stdout) == expected, (case, result.stdout, result.stderr)
96- del function
97- image.close()
98- thunk.close()
99144 for bad in ("", "(+ x)", "(+ x y) 1", "(let a 3 b)", "(if 1 2)", "(let if 3 4)",
100145 "18446744073709551616", "(+ x z)", "(div x y)", "é", " " * 65537):
101146 path.write_text(bad)