aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments/007-page-size-compatibility/run_qemu_4k.py
blob: 2a5b36e1a433ff089947969393a911d8fc8a8d79 (plain)
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
#!/usr/bin/env python3
"""Run the selected page store and the real document CLI under a 4 KiB kernel."""
import argparse
import gzip
import hashlib
import json
import os
from pathlib import Path
import platform
import subprocess
import tempfile


ROOT = Path(__file__).resolve().parents[3]
CONNECTIONS = ROOT / 'prototypes/connections'
GUEST = Path(__file__).with_name('guest.c')


def sha256(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()


def run(command, *, cwd=None, data=None, timeout=120):
    result = subprocess.run(command, cwd=cwd, input=data, capture_output=True,
                            timeout=timeout)
    if result.returncode:
        raise RuntimeError(f'{command[0]} failed ({result.returncode}): '
                           f'{result.stdout[-1000:]!r} {result.stderr[-1000:]!r}')
    return result


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--kernel', type=Path, required=True,
                        help='gzip-compressed or raw AArch64 4 KiB Linux kernel')
    parser.add_argument('--config', type=Path, required=True,
                        help='configuration belonging to that kernel')
    parser.add_argument('--output', type=Path, required=True)
    parser.add_argument('--log', type=Path, required=True)
    parser.add_argument('--cc', default='cc')
    parser.add_argument('--qemu', default='qemu-system-aarch64')
    args = parser.parse_args()
    if args.log.suffix != '.gz':
        parser.error('--log must be a .gz path')
    if platform.system() != 'Linux' or platform.machine() not in ('aarch64', 'arm64'):
        parser.error('this runner compiles an AArch64 guest on an AArch64 Linux host')
    if 'CONFIG_ARM64_4K_PAGES=y' not in args.config.read_text().splitlines():
        parser.error('the supplied kernel configuration is not a 4 KiB AArch64 build')
    if args.output.exists() or args.log.exists():
        parser.error('evidence output already exists')

    with tempfile.TemporaryDirectory(prefix='pure-qemu-4k-') as temporary:
        work = Path(temporary)
        build = work / 'build'
        run(['make', '-C', str(CONNECTIONS), f'BUILD={build}', 'fixed-editor'])
        edit = work / 'edit.o'
        run([args.cc, '-O2', '-std=c17', '-Wall', '-Wextra', '-Wpedantic', '-Werror',
             '-DPURE_EMBEDDED_PROGRAM', '-Dmain=pure_edit_main', f'-I{build}',
             '-c', str(CONNECTIONS / 'edit.c'), '-o', str(edit)])

        rootfs = work / 'rootfs'
        rootfs.mkdir()
        for name in ('tmp', 'dev'):
            (rootfs / name).mkdir()
        guest_binary = rootfs / 'init'
        run([args.cc, '-O2', '-static', '-std=c17', '-Wall', '-Wextra',
             '-Wpedantic', '-Werror', '-DPURE_STORE_TEST', f'-I{CONNECTIONS}',
             str(GUEST), str(edit), str(CONNECTIONS / 'store-paged.c'),
             str(CONNECTIONS / 'store-port.c'), str(CONNECTIONS / 'evaluate.c'),
             str(CONNECTIONS / 'runtime.c'), '-o', str(guest_binary)])
        guest_sha = sha256(guest_binary)
        for path in (rootfs, rootfs / 'tmp', rootfs / 'dev', guest_binary):
            os.utime(path, (0, 0))
        archive = work / 'initrd.cpio'
        entries = b'.\0./tmp\0./dev\0./init\0'
        archive.write_bytes(run(['cpio', '--null', '-o', '-H', 'newc', '--owner=0:0',
                                 '--reproducible'],
                                cwd=rootfs, data=entries).stdout)
        kernel_image = work / 'Image'
        with args.kernel.open('rb') as source:
            compressed_kernel = source.read(2) == b'\x1f\x8b'
        if compressed_kernel:
            with gzip.open(args.kernel, 'rb') as source, kernel_image.open('wb') as target:
                while chunk := source.read(1024 * 1024):
                    target.write(chunk)
        else:
            kernel_image.write_bytes(args.kernel.read_bytes())

        command = [args.qemu, '-machine', 'virt', '-cpu', 'cortex-a72',
                   '-smp', '1', '-m', '256M', '-nographic', '-no-reboot',
                   '-nic', 'none', '-kernel', str(kernel_image), '-initrd', str(archive),
                   '-append', 'console=ttyAMA0 loglevel=4 rdinit=/init panic=-1']
        guest_run = subprocess.run(command, capture_output=True, timeout=90)
        log_bytes = guest_run.stdout + guest_run.stderr
        args.log.parent.mkdir(parents=True, exist_ok=True)
        with args.log.open('xb') as destination:
            with gzip.GzipFile(fileobj=destination, mode='wb', filename='',
                               mtime=0, compresslevel=9) as compressed:
                compressed.write(log_bytes)
        log = log_bytes.decode(errors='replace').replace('\r\n', '\n')
        required = ('PURE_GUEST_OS_PAGE=4096', 'PURE_4K_STORE_RESULT=0',
                    'PURE_4K_APP_RESULT=0', '"mixed_edits":128',
                    '{"revision":"1","reply_bytes":1048576}',
                    '{"revision":"2","reply_bytes":1}',
                    '{"error":"conflict","revision":"2"}',
                    'reboot: Power down')
        missing = [marker for marker in required if marker not in log]
        if guest_run.returncode or missing or 'Kernel panic' in log:
            raise AssertionError(f'guest exit {guest_run.returncode}, missing {missing}; log: {args.log}')

        evidence = {
            'passed': True, 'os_page_bytes': 4096,
            'checks': ['128 mixed and fault-injected page-store edits',
                       'real prepared CLI creates and rereads every byte of a 1 MiB document',
                       'one-byte last-position revision edit and stale-revision rejection'],
            'guest_shutdown': 'power down', 'qemu_exit_code': guest_run.returncode,
            'host': {'system': platform.system(), 'machine': platform.machine(),
                     'page_bytes': os.sysconf('SC_PAGESIZE')},
            'kernel_sha256': sha256(args.kernel), 'config_sha256': sha256(args.config),
            'guest_binary_sha256': guest_sha, 'initramfs_sha256': sha256(archive),
            'log_sha256': hashlib.sha256(log_bytes).hexdigest(),
            'compressed_log_sha256': sha256(args.log),
            'qemu_version': run([args.qemu, '--version']).stdout.decode().splitlines()[0],
        }
        args.output.parent.mkdir(parents=True, exist_ok=True)
        with args.output.open('x') as destination:
            json.dump(evidence, destination, indent=2)
            destination.write('\n')
        print(json.dumps(evidence, indent=2))


if __name__ == '__main__':
    main()