aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-24 17:50:28 +0100
committereunhhu <[email protected]>2026-09-24 17:50:28 +0100
commitc63d70d535be7cf4b8c0a5739a7e623e51d2ebf0 (patch)
tree226611da0028e3bcd6f17f8615504ec32cb4a303 /research/experiments
parent4525124c01cc4c4ee335b0a430354a235a6539e0 (diff)
downloadpure-c63d70d535be7cf4b8c0a5739a7e623e51d2ebf0.tar.gz
pure-c63d70d535be7cf4b8c0a5739a7e623e51d2ebf0.zip
Verify paged document app under 4 KiB AArch64 kernel
Diffstat (limited to 'research/experiments')
-rw-r--r--research/experiments/007-page-size-compatibility/guest.c63
-rw-r--r--research/experiments/007-page-size-compatibility/run_qemu_4k.py133
2 files changed, 196 insertions, 0 deletions
diff --git a/research/experiments/007-page-size-compatibility/guest.c b/research/experiments/007-page-size-compatibility/guest.c
new file mode 100644
index 0000000..0599bc1
--- /dev/null
+++ b/research/experiments/007-page-size-compatibility/guest.c
@@ -0,0 +1,63 @@
+/* PID 1 in the 4 KiB AArch64 test initramfs. Keep the storage assertions and
+ * the real prepared document CLI in the same guest with no host file mapping. */
+#define main paged_store_test_main
+#include "../../../prototypes/connections/test-paged-store.c"
+#undef main
+#include <sys/reboot.h>
+
+int pure_edit_main(int argc, char **argv);
+
+static void check_native_app(void) {
+ const char *store = "/tmp/pure-4k-app";
+ const char *input = "/tmp/pure-4k-input";
+ const char *replacement = "/tmp/pure-4k-replacement";
+ FILE *file = fopen(input, "wb");
+ assert(file);
+ unsigned char block[4096];
+ memset(block, 'A', sizeof(block));
+ for (unsigned i = 0; i < 256; ++i)
+ assert(fwrite(block, 1, sizeof(block), file) == sizeof(block));
+ assert(fclose(file) == 0);
+
+ char *first[] = {"pure-edit-fixed", "--store", (char *)store, "edit",
+ "note", "0", "0", "0", (char *)input};
+ assert(pure_edit_main(9, first) == 0);
+ PureStoredView view;
+ assert(pure_store_get(store, "note", 1048576, &view) == PURE_OK);
+ assert(view.revision == 1 && view.body.size == 1048576);
+ for (size_t i = 0; i < view.body.size; ++i) assert(view.body.data[i] == 'A');
+ pure_store_release(&view);
+
+ file = fopen(replacement, "wb");
+ assert(file && fputc('Z', file) == 'Z' && fclose(file) == 0);
+ char *second[] = {"pure-edit-fixed", "--store", (char *)store, "edit",
+ "note", "1", "1048575", "1", (char *)replacement};
+ assert(pure_edit_main(9, second) == 0);
+ assert(pure_store_get(store, "note", 1048576, &view) == PURE_OK);
+ assert(view.revision == 2 && view.body.size == 1048576);
+ for (size_t i = 0; i < view.body.size - 1; ++i) assert(view.body.data[i] == 'A');
+ assert(view.body.data[view.body.size - 1] == 'Z');
+ pure_store_release(&view);
+
+ assert(pure_edit_main(9, second) == 3);
+ assert(pure_store_get(store, "note", 1048576, &view) == PURE_OK);
+ assert(view.revision == 2 && view.body.data[view.body.size - 1] == 'Z');
+ pure_store_release(&view);
+}
+
+int main(void) {
+ long os_page = sysconf(_SC_PAGESIZE);
+ printf("PURE_GUEST_OS_PAGE=%ld\n", os_page);
+ fflush(stdout);
+ if (os_page != 4096) return 2;
+ int result = paged_store_test_main();
+ printf("PURE_4K_STORE_RESULT=%d\n", result);
+ fflush(stdout);
+ if (!result) {
+ check_native_app();
+ puts("PURE_4K_APP_RESULT=0");
+ fflush(stdout);
+ reboot(RB_POWER_OFF);
+ }
+ return result;
+}
diff --git a/research/experiments/007-page-size-compatibility/run_qemu_4k.py b/research/experiments/007-page-size-compatibility/run_qemu_4k.py
new file mode 100644
index 0000000..2a5b36e
--- /dev/null
+++ b/research/experiments/007-page-size-compatibility/run_qemu_4k.py
@@ -0,0 +1,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()