File size: 12,069 Bytes
3f69d68 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 | """Native neural self-data candidates; no generated host payload is executed."""
from pathlib import Path
import json
import os
import random
import shutil
import struct
import subprocess
import pytest
import torch
from lightnest import rv32
from lightnest.learned_patch import encode, load_predictor, make_example
from lightnest.pack_patch import build, export_weights
ROOT = Path(__file__).resolve().parents[1]
CHECKPOINT = Path(os.environ.get('LIGHTNEST_PATCH_CHECKPOINT', ROOT / 'work/learned-patch.pt'))
def run(exe, *args):
return subprocess.run([str(exe), *map(str, args)], capture_output=True, timeout=20)
def seed_bytes():
return b''.join(w.to_bytes(4, 'little') for w in [rv32.lw('x1', 0, 'x0'), rv32.lw('x2', 4, 'x0'), rv32.add('x3', 'x1', 'x2'), rv32.addi('x10', 'x3', 0), rv32.EBREAK])
@pytest.fixture(scope='module')
def native(tmp_path_factory):
if not CHECKPOINT.exists():
pytest.skip('run the bounded learned_patch training experiment first')
if not shutil.which('gcc'):
pytest.skip('gcc is required')
directory = tmp_path_factory.mktemp('native-patch')
executable = build(CHECKPOINT, directory / 'patch-nest')
return executable, load_predictor(CHECKPOINT)
def seeded(native, tmp_path, binary):
raw = tmp_path / 'seed.rv32'
raw.write_bytes(binary)
exe = tmp_path / 'seeded'
result = run(native[0], '--init', raw, '-o', exe)
assert result.returncode == 0, result.stderr
return exe
@torch.inference_mode()
def reference(predictor, command, binary):
c,b=encode(command,binary)
logits,bits=predictor.model(torch.tensor([c]),torch.tensor([b]))
probs=logits.softmax(-1)[0]
pos=int(probs.argmax());confidence=float(probs[pos]);byte=0
if pos==256:
return pos,byte,confidence,False,'none'
byte=sum(int(bits[0,pos,k]>=0)<<k for k in range(8))
if confidence<predictor.threshold:
return pos,byte,confidence,False,'uncertain'
bitp=bits[0,pos].sigmoid()
confidence=min(confidence,float(torch.maximum(bitp,1-bitp).min()))
if confidence<predictor.threshold:
return pos,byte,confidence,False,'uncertain_byte'
if byte==binary[pos]:
return pos,byte,confidence,False,'no_change'
return pos,byte,confidence,True,'unvalidated_candidate'
def test_native_python_parity_fixed_examples(native,tmp_path):
# Frozen seeds, no checkpoint selection and no repair from expected labels.
rng=random.Random(984176)
seen=set()
for split in ('iid','register','offset','language','combined'):
for index in range(6):
e=make_example(rng,split)
directory=tmp_path/f'{split}-{index}';directory.mkdir()
exe=seeded(native,directory,e.binary)
result=run(exe,'--inspect',e.command)
assert result.returncode==0,result.stderr
actual=json.loads(result.stdout)
p,b,c,proposed,status=reference(native[1],e.command,e.binary)
assert (actual['position'],actual['replacement'],actual['proposed'],actual['status'])==(p,b,proposed,status)
assert actual['confidence']==pytest.approx(c,abs=2e-5)
patch=native[1].propose(e.command,e.binary)
assert actual['proposed']==(patch is not None)
seen.add(status)
assert 'none' in seen
assert 'unvalidated_candidate' in seen
def test_native_child_grandchild_independent_semantics(native,tmp_path):
original=seed_bytes();parent=seeded(native,tmp_path,original)
before=parent.read_bytes();child=tmp_path/'child';grandchild=tmp_path/'grandchild'
assert run(parent,'--propose','change add to sub','-o',child).returncode==0
assert parent.read_bytes()==before
candidate=run(child,'--dump-self').stdout
# Candidate host executable differs at precisely one data byte.
assert len(child.read_bytes())==len(before)
assert sum(a!=b for a,b in zip(before,child.read_bytes()))==1
assert sum(a!=b for a,b in zip(original,candidate))==1
cases=[(0,0),(3,5),(-12,8),(2147483647,-1),(-2147483648,1),(12345,-6789)]
signed=lambda n:(n+2**31)%2**32-2**31
assert all(rv32.simulate(candidate,list(pair))==signed(pair[0]-pair[1]) for pair in cases)
assert run(child,'--propose','change sub to add','-o',grandchild).returncode==0
assert grandchild.read_bytes()==before
restored=run(grandchild,'--dump-self').stdout
assert restored==original
assert all(rv32.simulate(restored,list(pair))==signed(pair[0]+pair[1]) for pair in cases)
# No-op model decision must create no output.
absent=tmp_path/'absent'
result=run(child,'--propose','change add to sub','-o',absent)
assert result.returncode==3
assert not absent.exists()
def test_input_output_bounds_and_immutability(native,tmp_path):
exe=seeded(native,tmp_path,seed_bytes());before=exe.read_bytes()
for request in ('','a'*97):
assert run(exe,'--inspect',request).returncode==2
assert run(exe,'--propose','change add to sub','-o',exe).returncode==2
alias=tmp_path/'alias';os.link(exe,alias)
assert run(exe,'--propose','change add to sub','-o',alias).returncode==2
assert exe.read_bytes()==before
raw=tmp_path/'huge';raw.write_bytes(b'X'*257)
assert run(native[0],'--init',raw,'-o',tmp_path/'bad').returncode==2
assert not (tmp_path/'bad').exists()
assert run(native[0],'--inspect','change add to sub').returncode==2
def test_corrupt_embedded_weights_fail_closed(native,tmp_path):
encoded=tmp_path/'weights';count=export_weights(CHECKPOINT,encoded)
assert count==107994
blob=encoded.read_bytes();image=native[0].read_bytes()
location=image.find(blob)
assert location>=0 and image.find(blob,location+1)<0
for name,offset,value in [('version',8,struct.pack('<I',2)),('count',20,struct.pack('<I',0)),('threshold',24,struct.pack('<f',0)),('nan',32,struct.pack('<f',float('nan')))]:
corrupt=bytearray(image);corrupt[location+offset:location+offset+4]=value
exe=tmp_path/name;exe.write_bytes(corrupt);exe.chmod(0o700)
result=run(exe,'--dump-self')
assert result.returncode==2
assert b'weight' in result.stderr or b'confidence' in result.stderr
def test_untrained_weights_abstain_without_fallback(tmp_path):
# A different (untrained) policy changes behavior; there is no keyword/opcode
# fallback silently supplying the learned task's known answer.
from lightnest.learned_patch import PatchNetwork, Predictor
torch.manual_seed(991)
model=PatchNetwork().eval()
checkpoint=tmp_path/'untrained.pt'
torch.save({'state_dict':model.state_dict(),'threshold':.55},checkpoint)
exe=build(checkpoint,tmp_path/'untrained')
native=(exe,Predictor(model))
parent=seeded(native,tmp_path,seed_bytes())
checkpoint.unlink() # All runtime weights are really embedded.
expected=reference(native[1],'change add to sub',seed_bytes())
target=tmp_path/'must-not-exist'
result=run(parent,'--propose','change add to sub','-o',target)
actual=json.loads(result.stdout)
assert result.returncode==3 and not target.exists()
assert actual['status'] in ('uncertain','uncertain_byte','none')
assert (actual['position'],actual['replacement'],actual['proposed'],actual['status'])==(expected[0],expected[1],expected[3],expected[4])
assert actual['confidence']==pytest.approx(expected[2],abs=2e-5)
def test_combined_holdout_native_generation_chain(native,tmp_path):
rng=random.Random(20261016)
while True:
e=make_example(rng,'combined')
if e.present:
break
assert e.command=='please replace sub with add'
assert e.pointer==115 and e.replacement==1
parent=seeded(native,tmp_path,e.binary);before=parent.read_bytes()
child=tmp_path/'ood-child';grandchild=tmp_path/'ood-grandchild'
first=run(parent,'--propose',e.command,'-o',child)
assert first.returncode==0,first.stderr
assert json.loads(first.stdout)['position']==115
second=run(child,'--propose','change add to sub','-o',grandchild)
assert second.returncode==0,second.stderr
candidate=run(child,'--dump-self').stdout
restored=run(grandchild,'--dump-self').stdout
inputs=random.Random(2026101602)
signed=lambda n:(n+2**31)%2**32-2**31
for _ in range(32):
a,b=[inputs.randint(-2**31,2**31-1) for _ in range(2)]
assert rv32.simulate(candidate,[a,b])==signed(a+b)
assert rv32.simulate(restored,[a,b])==signed(a-b)
assert parent.read_bytes()==before==grandchild.read_bytes()
assert sum(a!=b for a,b in zip(before,child.read_bytes()))==1
def test_running_inode_used_after_path_replacement(native, tmp_path):
original = seed_bytes()
parent = seeded(native, tmp_path, original)
before = parent.read_bytes()
fd = os.open(parent, os.O_RDONLY)
try:
parent.unlink()
parent.write_bytes(b'concurrently replaced pathname')
result = subprocess.run(
[f'/proc/self/fd/{fd}', '--propose', 'change add to sub', '-o', str(tmp_path / 'child')],
pass_fds=(fd,), capture_output=True, timeout=20,
)
finally:
os.close(fd)
assert result.returncode == 0, result.stderr
child = tmp_path / 'child'
assert len(child.read_bytes()) == len(before)
assert sum(a != b for a, b in zip(before, child.read_bytes())) == 1
assert parent.read_bytes() == b'concurrently replaced pathname'
assert rv32.simulate(run(child, '--dump-self').stdout, [3, 5]) == -2
def test_fifo_seed_rejected_without_blocking(native, tmp_path):
fifo = tmp_path / 'seed-fifo'
os.mkfifo(fifo)
result = subprocess.run(
[str(native[0]), '--init', str(fifo), '-o', str(tmp_path / 'child')],
capture_output=True, timeout=2,
)
assert result.returncode == 2
assert not (tmp_path / 'child').exists()
def test_packer_failure_leaves_no_output(native, tmp_path, monkeypatch):
from lightnest import pack_patch
output = tmp_path / 'final'
def failed_compiler(command, **kwargs):
staged = Path(command[command.index('-o') + 1])
assert staged != output
assert staged.parent.parent == output.parent
staged.write_bytes(b'partial compiler output')
raise subprocess.CalledProcessError(1, command)
monkeypatch.setattr(pack_patch.subprocess, 'run', failed_compiler)
with pytest.raises(subprocess.CalledProcessError):
build(CHECKPOINT, output)
assert not output.exists()
assert list(tmp_path.iterdir()) == []
def test_packer_publish_race_preserves_existing_output(native, tmp_path, monkeypatch):
from lightnest import pack_patch
output = tmp_path / 'final'
def racing_compiler(command, **kwargs):
staged = Path(command[command.index('-o') + 1])
assert staged != output
staged.write_bytes(b'compiled candidate')
output.write_bytes(b'concurrently created destination')
return subprocess.CompletedProcess(command, 0)
monkeypatch.setattr(pack_patch.subprocess, 'run', racing_compiler)
with pytest.raises(FileExistsError):
build(CHECKPOINT, output)
assert output.read_bytes() == b'concurrently created destination'
assert list(tmp_path.iterdir()) == [output]
def test_packer_does_not_follow_destination_symlinks(native, tmp_path):
target = tmp_path / 'absent-target'
output = tmp_path / 'output'
output.symlink_to(target)
with pytest.raises(FileExistsError):
build(CHECKPOINT, output)
assert output.is_symlink()
assert not target.exists()
@pytest.mark.parametrize('threshold', [float('nan'), float('inf'), -float('inf')])
def test_export_rejects_nonfinite_threshold(native, tmp_path, threshold):
saved = torch.load(CHECKPOINT, map_location='cpu', weights_only=True)
saved['threshold'] = threshold
checkpoint = tmp_path / 'invalid.pt'
torch.save(saved, checkpoint)
output = tmp_path / 'weights.bin'
with pytest.raises(ValueError, match='confidence'):
export_weights(checkpoint, output)
assert not output.exists()
|