#!/usr/bin/env python3 """Real write, flush, readback, cancellation and worker-crash failure boundaries.""" import io import json from pathlib import Path import shlex import subprocess import sys import tarfile import tempfile import time project=Path(__file__).resolve().parents[2] sys.path.insert(0,str(project/'tools')) from vm_test import VM from image_formats import gpt, digest, LINUX_FILESYSTEM work=Path(tempfile.mkdtemp(prefix='m11-faults.',dir=project/'out')) inputs=[Path(__file__),project/'tools/vm_test.py',project/'out/rootfs-cli.tar', project/'out/kernel/boot/kernel_2712.img',project/'out/fds-initramfs.img'] (work/'inputs.sha256').write_text(''.join(f'{digest(path)} {path.relative_to(project)}\n' for path in inputs)) (work/'qemu-version.txt').write_bytes(subprocess.check_output([str(project/'tools/in-void'),'qemu-system-aarch64','--version'])) controller='qemu-xhci,id=xhci,addr=05.0' image_runs=sorted((project/'out').glob('m9-images.*'),key=lambda p:p.stat().st_mtime,reverse=True) program=next(p/'root/m9-test/program.img' for p in image_runs if (p/'program.json').is_file()) # A valid padded PROGRAM filesystem makes the transfer long enough to observe # cancellation after real writes. Its contents are still independently checked. layout=json.loads(subprocess.check_output(['sfdisk','--json',str(program)]))['partitiontable']['partitions'][0] large_fs=work/'large.erofs' with program.open('rb') as src,large_fs.open('xb') as dst: src.seek(layout['start']*512);dst.write(src.read(layout['size']*512));dst.truncate(256*1024*1024) large=work/'large.img';gpt(large,[('FDS_PROGRAM',LINUX_FILESYSTEM,large_fs)]) subprocess.run([str(project/'tools/in-image-tools'),'fsck.erofs',str(large_fs)],check=True) def query(vm,args):return json.loads(vm.capture('s6-setuidgid fds fds --json '+shlex.join(args))) def wait(vm,read,predicate,timeout=90): deadline=time.monotonic()+timeout while True: value=read() if predicate(value):return value assert time.monotonic()/tmp/m11-enumeration.err; true'),lambda s:s.startswith('{') and json.loads(s)['bytes']==disk.stat().st_size) def job_record(vm):return json.loads(vm.capture('cat /run/fds/burn/02.json'))['job'] def failed(vm,job): observed=wait(vm,lambda:job_record(vm),lambda j:j['phase']=='failed') assert observed['id']==job['id'] and observed['error'],observed assert query(vm,['bay','2'])['bays'][0]['state']!='safe' wait(vm,lambda:vm.capture('pgrep -x fds-burn || test "$?" = 1'),lambda s:not s) vm.capture('s6-svc -d /run/service/cartridged && s6-svwait -d -t 15000 /run/service/cartridged && s6-svc -u /run/service/cartridged && s6-svwait -U -t 15000 /run/service/cartridged') assert job_record(vm)['phase']=='failed' assert query(vm,['bay','2'])['bays'][0]['state']=='failed' vm.capture('test ! -e /run/fds/ejected/02') (work/f'{vm.name}-result.json').write_text(json.dumps(observed,indent=2)+'\n') (work/f'{vm.name}-kernel.log').write_text(vm.capture('dmesg')+'\n') return observed # Errors affect sectors beyond the GPT checks, so preview remains non-destructive. for name,rule in [('write-error',{'iotype':'write','sector':2056}), ('flush-error',{'iotype':'flush'}), ('readback-error',{'iotype':'read','sector':2056})]: disk=work/f'{name}.img' with disk.open('xb') as f:f.truncate(256*1024*1024) before=digest(disk) with VM(work,name,system,extra=['-device',controller]) as vm: vm.expect(rb'FDS# ');attach(vm,disk,rule) job=query(vm,['burn','program','/usr/share/fds/m11-small.img','BAY02']) assert job['phase']=='awaiting_confirmation' and digest(disk)==before error=vm.capture('s6-setuidgid fds fds burn confirm '+shlex.join([job['id'],job['confirmation']]),ok=False) result=failed(vm,job) assert 'os error 5' in result['error'],(name,error,result) assert digest(disk)!=before,(name,'No write was observed') print(f'PASS: {name}: actual EIO after confirmation; failed across restart, never SAFE',flush=True) for name in ['cancel-writing','kill-worker','kill-daemon']: disk=work/f'{name}.img' with disk.open('xb') as f:f.truncate(384*1024*1024) before=digest(disk) with VM(work,name,system,extra=['-device',controller]) as vm: vm.expect(rb'FDS# ');attach(vm,disk,throttle=True) job=query(vm,['burn','program','/usr/share/fds/m11-large.img','BAY02']) vm.capture('(s6-setuidgid fds fds burn confirm '+shlex.join([job['id'],job['confirmation']])+' >/tmp/m11-confirm.out 2>&1 & )') progress=wait(vm,lambda:job_record(vm),lambda j:j['phase']=='writing' and j['progress_bytes']>=64*1024*1024,timeout=180) assert progress['progress_bytes']=64*1024*1024 for r in rows),timeout=180) progress=job_record(vm) assert progress['phase']=='writing' and progress['progress_bytes']