[
  {
    "id": "ldd",
    "title": "ldd: resolve trusted shared libraries",
    "notes": "",
    "steps": [
      {
        "command": "ldd ./tool_demo",
        "output": "\tlinux-vdso.so.1 (0x0000ffff9cf41000)\n\tlibdemo.so => /work/fixture/./libdemo.so (0x0000ffff9cec0000)\n\tlibstdc++.so.6 => /lib/aarch64-linux-gnu/libstdc++.so.6 (0x0000ffff9cc90000)\n\tlibgcc_s.so.1 => /lib/aarch64-linux-gnu/libgcc_s.so.1 (0x0000ffff9cc60000)\n\tlibc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffff9cab0000)\n\t/lib/ld-linux-aarch64.so.1 (0x0000ffff9cf00000)\n\tlibm.so.6 => /lib/aarch64-linux-gnu/libm.so.6 (0x0000ffff9ca10000)\n",
        "caption": "Resolve dependencies of our own trusted ELF",
        "duration_ms": 4500
      },
      {
        "command": "readelf -d ./tool_demo | grep -E 'NEEDED|RUNPATH'",
        "output": " 0x0000000000000001 (NEEDED)             Shared library: [libdemo.so]\n 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]\n 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]\n 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]\n 0x0000000000000001 (NEEDED)             Shared library: [ld-linux-aarch64.so.1]\n 0x000000000000001d (RUNPATH)            Library runpath: [$ORIGIN]\n",
        "caption": "RUNPATH uses the binary directory, not the shell cwd",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "local libdemo.so resolves and no dependency is missing",
      "ELF records libdemo.so and $ORIGIN RUNPATH"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1
    ]
  },
  {
    "id": "ps",
    "title": "ps: process, threads and wait channels",
    "notes": "",
    "steps": [
      {
        "command": "ps -p 32 -o pid,ppid,stat,nlwp,comm",
        "output": "  PID  PPID STAT NLWP COMMAND\n   32     1 Sl      3 tool_demo\n",
        "caption": "One process can contain multiple Linux threads",
        "duration_ms": 4500
      },
      {
        "command": "ps -L -p 32 -o pid,tid,stat,wchan:28,comm",
        "output": "  PID   TID STAT WCHAN                        COMMAND\n   32    32 Sl   do_sys_poll                  tool_demo\n   32    33 Sl   futex_wait_queue             tool_demo\n   32    34 Sl   futex_wait_queue             tool_demo\n",
        "caption": "Inspect the selected process, not every user process",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "toy process has exactly three live threads",
      "thread view returns main and both worker threads"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1
    ]
  },
  {
    "id": "lsof",
    "title": "lsof: deleted file and listening socket",
    "notes": "",
    "steps": [
      {
        "command": "lsof -nP -a -p 32 +L1",
        "output": "COMMAND   PID    USER   FD   TYPE DEVICE SIZE/OFF NLINK NODE NAME\ntool_demo  32 learner    3u   REG   0,77       18     0   12 /work/fixture/sample.log (deleted)\n",
        "caption": "Unlink removes a pathname, not an open file descriptor",
        "duration_ms": 4500
      },
      {
        "command": "lsof -nP -a -p 32 -iTCP -sTCP:LISTEN",
        "output": "COMMAND   PID    USER   FD   TYPE DEVICE SIZE/OFF NODE NAME\ntool_demo  32 learner    4u  IPv4  15159      0t0  TCP 127.0.0.1:46757 (LISTEN)\n",
        "caption": "AND process and socket filters with -a",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "unlinked sample.log remains held by the toy process",
      "toy TCP listener is loopback only"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1
    ]
  },
  {
    "id": "pstack",
    "title": "pstack unavailable: inspect stacks with GDB",
    "notes": "Ubuntu ARM64 has no pstack binary here. The following all-thread stack command is an explicit alternative.",
    "steps": [
      {
        "command": "if command -v pstack; then pstack --help; else printf 'pstack is not installed in this Ubuntu 22.04 image.\nUse the explicit GDB batch alternative below.\n'; fi",
        "output": "pstack is not installed in this Ubuntu 22.04 image.\nUse the explicit GDB batch alternative below.\n",
        "caption": "Check availability; do not pretend a missing tool ran",
        "duration_ms": 4500
      },
      {
        "command": "gdb -q -nx -batch -ex 'set pagination off' -ex 'thread apply all bt 5' -p 32",
        "output": "[New LWP 33]\n[New LWP 34]\n[Thread debugging using libthread_db enabled]\nUsing host libthread_db library \"/lib/aarch64-linux-gnu/libthread_db.so.1\".\n0x0000ffffa6b19c8c in pause () from /lib/aarch64-linux-gnu/libc.so.6\n\nThread 3 (Thread 0xffffa61af100 (LWP 34) \"tool_demo\"):\n#0  0x0000ffffa6adcba8 in ?? () from /lib/aarch64-linux-gnu/libc.so.6\n#1  0x0000ffffa6adf6c4 in pthread_cond_wait () from /lib/aarch64-linux-gnu/libc.so.6\n#2  0x0000aaaab0981984 in std::condition_variable::wait<wait_for_job()::<lambda()> >(std::unique_lock<std::mutex> &, struct {...}) (this=0xaaaab0995050 <queue_ready>, __lock=..., __p=...) at /usr/include/c++/11/condition_variable:103\n#3  0x0000aaaab09815cc in wait_for_job () at tool_demo.cpp:23\n#4  0x0000aaaab0983ad4 in std::__invoke_impl<void, void (*)()> (__f=@0xaaaad6b02008: 0xaaaab0981570 <wait_for_job()>) at /usr/include/c++/11/bits/invoke.h:61\n\nThread 2 (Thread 0xffffa69bf100 (LWP 33) \"tool_demo\"):\n#0  0x0000ffffa6adcba8 in ?? () from /lib/aarch64-linux-gnu/libc.so.6\n#1  0x0000ffffa6adf6c4 in pthread_cond_wait () from /lib/aarch64-linux-gnu/libc.so.6\n#2  0x0000aaaab0981984 in std::condition_variable::wait<wait_for_job()::<lambda()> >(std::unique_lock<std::mutex> &, struct {...}) (this=0xaaaab0995050 <queue_ready>, __lock=..., __p=...) at /usr/include/c++/11/condition_variable:103\n#3  0x0000aaaab09815cc in wait_for_job () at tool_demo.cpp:23\n#4  0x0000aaaab0983ad4 in std::__invoke_impl<void, void (*)()> (__f=@0xaaaad6b01eb8: 0xaaaab0981570 <wait_for_job()>) at /usr/include/c++/11/bits/invoke.h:61\n\nThread 1 (Thread 0xffffa6eb7fc0 (LWP 32) \"tool_demo\"):\n#0  0x0000ffffa6b19c8c in pause () from /lib/aarch64-linux-gnu/libc.so.6\n#1  0x0000aaaab0981808 in main (argc=2, argv=0xffffdbd4e858) at tool_demo.cpp:40\n[Inferior 1 (process 32) detached]\n",
        "caption": "A brief attach stops the toy threads; all stacks then detach",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "pstack is absent; the alternative is explicitly labelled",
      "batch alternative sees both wait_for_job stacks and detaches"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1
    ]
  },
  {
    "id": "strace",
    "title": "strace: file success, ENOENT, and a short sleep",
    "notes": "",
    "steps": [
      {
        "command": "strace -f -e trace=openat,read,close,clock_nanosleep,nanosleep -o trace.log ./tool_demo once",
        "output": "demo_add(2,3)=5 bytes=36 missing_errno=2\n",
        "caption": "Trace a child that we launch, with output kept in a file",
        "duration_ms": 4500
      },
      {
        "command": "grep -E 'sample.txt|missing.txt|clock_nanosleep|nanosleep' trace.log",
        "output": "56    openat(AT_FDCWD, \"sample.txt\", O_RDONLY) = 3\n56    openat(AT_FDCWD, \"missing.txt\", O_RDONLY) = -1 ENOENT (No such file or directory)\n56    clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=0, tv_nsec=20000000}, 0xffffff47bc38) = 0\n",
        "caption": "ENOENT is a failed lookup, not automatically the root cause",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "toy application computes correctly and sees ENOENT",
      "trace records successful input, missing file and sleep syscall"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1
    ]
  },
  {
    "id": "ipcs",
    "title": "ipcs: inspect only the shared memory we created",
    "notes": "",
    "steps": [
      {
        "command": "ipcmk -M 4096",
        "output": "Shared memory id: 0\n",
        "caption": "Create a private 4096-byte teaching IPC object",
        "duration_ms": 4500
      },
      {
        "command": "ipcs -m -i 0",
        "output": "\nShared memory Segment shmid=0\nuid=501\tgid=20\tcuid=501\tcgid=20\nmode=0644\taccess_perms=0644\nbytes=4096\tlpid=0\tcpid=58\tnattch=0\natt_time=Not set                   \ndet_time=Not set                   \nchange_time=Mon Sep  7 13:55:29 2026  \n\n",
        "caption": "Inspect its owner, size and attachment count",
        "duration_ms": 4500
      },
      {
        "command": "ipcrm -m 0 && ipcs -m",
        "output": "\n------ Shared Memory Segments --------\nkey        shmid      owner      perms      bytes      nattch     status      \n\n",
        "caption": "Remove that exact object, never a blanket IPC cleanup",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "ipcmk returns the created shared-memory ID",
      "the selected object is 4096 bytes and unattached"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1,
      2
    ]
  },
  {
    "id": "top",
    "title": "top: batch snapshots, then thread snapshots",
    "notes": "",
    "steps": [
      {
        "command": "top -b -n 2 -d 0.3 -p 32",
        "output": "top - 13:55:29 up 18 min,  0 users,  load average: 0.39, 0.40, 0.21\nTasks:   1 total,   0 running,   1 sleeping,   0 stopped,   0 zombie\n%Cpu(s):  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st\nMiB Mem :   7837.0 total,   5923.5 free,    506.4 used,   1407.2 buff/cache\nMiB Swap:   1024.0 total,   1024.0 free,      0.0 used.   7150.7 avail Mem \n\n  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND\n   32 learner   20   0   21804   2488   2268 S   0.0   0.0   0:00.00 tool_demo\n\ntop - 13:55:29 up 18 min,  0 users,  load average: 0.39, 0.40, 0.21\nTasks:   1 total,   0 running,   1 sleeping,   0 stopped,   0 zombie\n%Cpu(s):  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st\nMiB Mem :   7837.0 total,   5923.5 free,    506.4 used,   1407.2 buff/cache\nMiB Swap:   1024.0 total,   1024.0 free,      0.0 used.   7150.7 avail Mem \n\n  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND\n   32 learner   20   0   21804   2488   2268 S   0.0   0.0   0:00.00 tool_demo\n",
        "caption": "Two samples of only our toy process; headers may be VM-wide",
        "duration_ms": 4500
      },
      {
        "command": "top -H -b -n 1 -p 32",
        "output": "top - 13:55:29 up 18 min,  0 users,  load average: 0.39, 0.40, 0.21\nThreads:   3 total,   0 running,   3 sleeping,   0 stopped,   0 zombie\n%Cpu(s):  0.8 us,  0.0 sy,  0.0 ni, 99.2 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st\nMiB Mem :   7837.0 total,   5923.5 free,    506.4 used,   1407.2 buff/cache\nMiB Swap:   1024.0 total,   1024.0 free,      0.0 used.   7150.7 avail Mem \n\n  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND\n   32 learner   20   0   21804   2488   2268 S   0.0   0.0   0:00.00 tool_demo\n   33 learner   20   0   21804   2488   2268 S   0.0   0.0   0:00.00 tool_demo\n   34 learner   20   0   21804   2488   2268 S   0.0   0.0   0:00.00 tool_demo\n",
        "caption": "Switch from process rows to thread rows",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "two batch process samples are present",
      "thread mode lists all three toy threads"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1
    ]
  },
  {
    "id": "free",
    "title": "free: available memory is not the same as free",
    "notes": "",
    "steps": [
      {
        "command": "free -h",
        "output": "               total        used        free      shared  buff/cache   available\nMem:           7.7Gi       506Mi       5.8Gi       0.0Ki       1.4Gi       7.0Gi\nSwap:          1.0Gi          0B       1.0Gi\n",
        "caption": "Read the available column before declaring memory pressure",
        "duration_ms": 4500
      },
      {
        "command": "free -m -s 1 -c 2",
        "output": "               total        used        free      shared  buff/cache   available\nMem:            7837         506        5923           0        1407        7150\nSwap:           1023           0        1023\n\n               total        used        free      shared  buff/cache   available\nMem:            7837         506        5923           0        1407        7150\nSwap:           1023           0        1023\n",
        "caption": "Observe two samples; these may describe Docker VM memory",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "human-readable summary includes available memory",
      "interval mode records exactly two memory samples"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1
    ]
  },
  {
    "id": "vmstat",
    "title": "vmstat: boot averages versus interval activity",
    "notes": "",
    "steps": [
      {
        "command": "vmstat 1 2",
        "output": "procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----\n r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st\n 0  0      0 6065640  74668 1366292    0    0    37   175   81  117  0  0 99  0  0\n 0  0      0 6062664  74668 1366292    0    0     0     0 2862 4563  2  1 97  0  0\n",
        "caption": "First CPU/rate row since boot; gauges are current",
        "duration_ms": 4500
      },
      {
        "command": "vmstat -s",
        "output": "      8025128 K total memory\n       521504 K used memory\n       594480 K active memory\n      1081356 K inactive memory\n      6062664 K free memory\n        74668 K buffer memory\n      1366292 K swap cache\n      1048572 K total swap\n            0 K used swap\n      1048572 K free swap\n         3523 non-nice user cpu ticks\n            0 nice user cpu ticks\n         1199 system cpu ticks\n       890285 idle cpu ticks\n          537 IO-wait cpu ticks\n            0 IRQ cpu ticks\n          523 softirq cpu ticks\n            0 stolen cpu ticks\n       328201 pages paged in\n      1563872 pages paged out\n            0 pages swapped in\n            0 pages swapped out\n       729462 interrupts\n      1056146 CPU context switches\n   1788788210 boot time\n         4062 forks\n",
        "caption": "Review cumulative counters separately from per-second rates",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "two numeric vmstat rows are present",
      "summary provides cumulative memory and context-switch counters"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1
    ]
  },
  {
    "id": "iostat",
    "title": "iostat: CPU report and interval-only device report",
    "notes": "",
    "steps": [
      {
        "command": "iostat -c 1 2",
        "output": "Linux 6.12.54-linuxkit (ace52f81e5a7) \t09/07/26 \t_aarch64_\t(8 CPU)\n\navg-cpu:  %user   %nice %system %iowait  %steal   %idle\n           0.39    0.00    0.19    0.06    0.00   99.35\n\n\n\navg-cpu:  %user   %nice %system %iowait  %steal   %idle\n           0.00    0.00    0.00    0.00    0.00  100.00\n\n\n\n",
        "caption": "The initial CPU report is an average since boot",
        "duration_ms": 4500
      },
      {
        "command": "iostat -dx -y 1 1",
        "output": "Linux 6.12.54-linuxkit (ace52f81e5a7) \t09/07/26 \t_aarch64_\t(8 CPU)\n\n\nDevice            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s   wrqm/s  %wrqm w_await wareq-sz     d/s     dkB/s   drqm/s  %drqm d_await dareq-sz     f/s f_await  aqu-sz  %util\nvda              0.00      0.00     0.00   0.00    0.00     0.00    2.00     48.00    10.00  83.33    1.00    24.00    1.00      8.00     0.00   0.00    1.00     8.00    2.00    0.00    0.00   0.20\nvdb              0.00      0.00     0.00   0.00    0.00     0.00    0.00      0.00     0.00   0.00    0.00     0.00    0.00      0.00     0.00   0.00    0.00     0.00    0.00    0.00    0.00   0.00\n\n\n",
        "caption": "Use -y to omit the since-boot report; device scope may be VM-wide",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "CPU report contains iowait without asserting a bottleneck",
      "extended interval report has latency and utilization columns"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1
    ]
  },
  {
    "id": "sar",
    "title": "sar: capture now, then replay the binary sample",
    "notes": "",
    "steps": [
      {
        "command": "sar -u -o cpu.sar 1 2",
        "output": "Linux 6.12.54-linuxkit (ace52f81e5a7) \t09/07/26 \t_aarch64_\t(8 CPU)\n\n13:55:33        CPU     %user     %nice   %system   %iowait    %steal     %idle\n13:55:34        all      0.00      0.00      0.00      0.00      0.00    100.00\n13:55:35        all      0.00      0.00      0.25      0.00      0.00     99.75\nAverage:        all      0.00      0.00      0.12      0.00      0.00     99.88\n",
        "caption": "Collect two samples explicitly; no background collector required",
        "duration_ms": 4500
      },
      {
        "command": "sar -u -f cpu.sar",
        "output": "Linux 6.12.54-linuxkit (ace52f81e5a7) \t09/07/26 \t_aarch64_\t(8 CPU)\n\n13:55:33        CPU     %user     %nice   %system   %iowait    %steal     %idle\n13:55:34        all      0.00      0.00      0.00      0.00      0.00    100.00\n13:55:35        all      0.00      0.00      0.25      0.00      0.00     99.75\nAverage:        all      0.00      0.00      0.12      0.00      0.00     99.88\n",
        "caption": "Read the recorded interval instead of inventing historical data",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "live CPU samples and a nonempty binary recording exist",
      "sar replays the generated recording successfully"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1
    ]
  },
  {
    "id": "readelf",
    "title": "readelf: ELF identity and dynamic dependencies",
    "notes": "",
    "steps": [
      {
        "command": "readelf -h ./tool_demo",
        "output": "ELF Header:\n  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 \n  Class:                             ELF64\n  Data:                              2's complement, little endian\n  Version:                           1 (current)\n  OS/ABI:                            UNIX - System V\n  ABI Version:                       0\n  Type:                              DYN (Position-Independent Executable file)\n  Machine:                           AArch64\n  Version:                           0x1\n  Entry point address:               0x1440\n  Start of program headers:          64 (bytes into file)\n  Start of section headers:          111720 (bytes into file)\n  Flags:                             0x0\n  Size of this header:               64 (bytes)\n  Size of program headers:           56 (bytes)\n  Number of program headers:         9\n  Size of section headers:           64 (bytes)\n  Number of section headers:         37\n  Section header string table index: 36\n",
        "caption": "Read class, machine and ELF type before debugging ABI problems",
        "duration_ms": 4500
      },
      {
        "command": "readelf -d ./tool_demo | grep -E 'NEEDED|RUNPATH'",
        "output": " 0x0000000000000001 (NEEDED)             Shared library: [libdemo.so]\n 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]\n 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]\n 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]\n 0x0000000000000001 (NEEDED)             Shared library: [ld-linux-aarch64.so.1]\n 0x000000000000001d (RUNPATH)            Library runpath: [$ORIGIN]\n",
        "caption": "Dependencies and search paths are different ELF entries",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "header identifies a 64-bit ELF and its actual architecture",
      "dynamic section records the expected library and relative search path"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1
    ]
  },
  {
    "id": "objdump",
    "title": "objdump: object sections and assembly with relocations",
    "notes": "",
    "steps": [
      {
        "command": "objdump -h ./symbols.o",
        "output": "\n./symbols.o:     file format elf64-littleaarch64\n\nSections:\nIdx Name          Size      VMA               LMA               File off  Algn\n  0 .text         00000048  0000000000000000  0000000000000000  00000040  2**2\n                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE\n  1 .data         00000008  0000000000000000  0000000000000000  00000088  2**2\n                  CONTENTS, ALLOC, LOAD, DATA\n  2 .bss          00001000  0000000000000000  0000000000000000  00000090  2**3\n                  ALLOC\n  3 .rodata       0000000b  0000000000000000  0000000000000000  00000090  2**3\n                  CONTENTS, ALLOC, LOAD, READONLY, DATA\n  4 .debug_info   00000726  0000000000000000  0000000000000000  0000009b  2**0\n                  CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS\n  5 .debug_abbrev 000001e2  0000000000000000  0000000000000000  000007c1  2**0\n                  CONTENTS, READONLY, DEBUGGING, OCTETS\n  6 .debug_aranges 00000030  0000000000000000  0000000000000000  000009a3  2**0\n                  CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS\n  7 .debug_line   000000a3  0000000000000000  0000000000000000  000009d3  2**0\n                  CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS\n  8 .debug_str    00000411  0000000000000000  0000000000000000  00000a76  2**0\n                  CONTENTS, READONLY, DEBUGGING, OCTETS\n  9 .debug_line_str 0000015e  0000000000000000  0000000000000000  00000e87  2**0\n                  CONTENTS, READONLY, DEBUGGING, OCTETS\n 10 .comment      0000002e  0000000000000000  0000000000000000  00000fe5  2**0\n                  CONTENTS, READONLY\n 11 .note.GNU-stack 00000000  0000000000000000  0000000000000000  00001013  2**0\n                  CONTENTS, READONLY\n 12 .eh_frame     00000050  0000000000000000  0000000000000000  00001018  2**3\n                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA\n",
        "caption": "Section sizes belong to an ELF object, not live process RSS",
        "duration_ms": 4500
      },
      {
        "command": "objdump -drC ./symbols.o",
        "output": "\n./symbols.o:     file format elf64-littleaarch64\n\n\nDisassembly of section .text:\n\n0000000000000000 <demo::square(int)>:\n   0:\td10043ff \tsub\tsp, sp, #0x10\n   4:\tb9000fe0 \tstr\tw0, [sp, #12]\n   8:\tb9400fe0 \tldr\tw0, [sp, #12]\n   c:\t1b007c01 \tmul\tw1, w0, w0\n  10:\t90000000 \tadrp\tx0, 0 <demo::square(int)>\n\t\t\t10: R_AARCH64_ADR_PREL_PG_HI21\t.data+0x4\n  14:\t91000000 \tadd\tx0, x0, #0x0\n\t\t\t14: R_AARCH64_ADD_ABS_LO12_NC\t.data+0x4\n  18:\tb9400000 \tldr\tw0, [x0]\n  1c:\t0b000020 \tadd\tw0, w1, w0\n  20:\t910043ff \tadd\tsp, sp, #0x10\n  24:\td65f03c0 \tret\n\n0000000000000028 <announce()>:\n  28:\ta9bf7bfd \tstp\tx29, x30, [sp, #-16]!\n  2c:\t910003fd \tmov\tx29, sp\n  30:\t90000000 \tadrp\tx0, 0 <demo::square(int)>\n\t\t\t30: R_AARCH64_ADR_PREL_PG_HI21\t.rodata\n  34:\t91000000 \tadd\tx0, x0, #0x0\n\t\t\t34: R_AARCH64_ADD_ABS_LO12_NC\t.rodata\n  38:\t94000000 \tbl\t0 <puts>\n\t\t\t38: R_AARCH64_CALL26\tputs\n  3c:\td503201f \tnop\n  40:\ta8c17bfd \tldp\tx29, x30, [sp], #16\n  44:\td65f03c0 \tret\n",
        "caption": "Demangle C++ names and retain relocation records",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "object contains code and zero-initialized storage sections",
      "disassembly includes C++ function and unresolved puts relocation"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1
    ]
  },
  {
    "id": "nm",
    "title": "nm: defined data, zero-fill, and undefined symbols",
    "notes": "",
    "steps": [
      {
        "command": "nm -C ./symbols.o",
        "output": "0000000000000028 T announce()\n0000000000000004 d local_counter\n0000000000000000 T demo::square(int)\n0000000000000000 D initialized_counter\n                 U puts\n0000000000000000 B zero_buffer\n",
        "caption": "D/B/t/U classify symbols; U means resolved later, not necessarily a bug",
        "duration_ms": 4500
      },
      {
        "command": "nm -D --defined-only ./libdemo.so",
        "output": "0000000000000564 T demo_add\n",
        "caption": "Dynamic exports differ from the full static symbol table",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "initialized, zero-fill and undefined symbols have expected D/B/U types",
      "shared library exports demo_add dynamically"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1
    ]
  },
  {
    "id": "size",
    "title": "size: compare code, initialized data and zero-fill",
    "notes": "",
    "steps": [
      {
        "command": "size ./symbols.o ./tool_demo",
        "output": "   text\t   data\t    bss\t    dec\t    hex\tfilename\n    163\t      8\t   4096\t   4267\t   10ab\t./symbols.o\n  18482\t   1088\t    112\t  19682\t   4ce2\t./tool_demo\n",
        "caption": "The summary is ELF section accounting, not runtime memory usage",
        "duration_ms": 4500
      },
      {
        "command": "size -A ./symbols.o",
        "output": "./symbols.o  :\nsection           size   addr\n.text               72      0\n.data                8      0\n.bss              4096      0\n.rodata             11      0\n.debug_info       1830      0\n.debug_abbrev      482      0\n.debug_aranges      48      0\n.debug_line        163      0\n.debug_str        1041      0\n.debug_line_str    350      0\n.comment            46      0\n.note.GNU-stack      0      0\n.eh_frame           80      0\nTotal             8227\n\n\n",
        "caption": "A 4096-byte zero array contributes to .bss",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "Berkeley summary includes both fixtures",
      "section view counts the zero_buffer allocation in .bss"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1
    ]
  },
  {
    "id": "wget",
    "title": "wget: bounded local download and checksum comparison",
    "notes": "",
    "steps": [
      {
        "command": "wget --timeout=3 --tries=1 -O downloaded.txt http://127.0.0.1:45909/payload.txt",
        "output": "--2026-09-07 13:55:36--  http://127.0.0.1:45909/payload.txt\nConnecting to 127.0.0.1:45909... connected.\nHTTP request sent, awaiting response... 200 OK\nLength: 60 [text/plain]\nSaving to: \u2018downloaded.txt\u2019\n\n     0K                                                       100% 19.3M=0s\n\n2026-09-07 13:55:36 (19.3 MB/s) - \u2018downloaded.txt\u2019 saved [60/60]\n\n",
        "caption": "A local fixture avoids external content, credentials and network variability",
        "duration_ms": 4500
      },
      {
        "command": "sha256sum payload.txt downloaded.txt",
        "output": "1541251a19553c9e2dde2a971aa6d3f7b894b008fa2941a3562c5ecb7dd6b01b  payload.txt\n1541251a19553c9e2dde2a971aa6d3f7b894b008fa2941a3562c5ecb7dd6b01b  downloaded.txt\n",
        "caption": "Compare bytes after transport, not just the exit code",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "HTTP succeeds and downloaded bytes match the original",
      "both SHA-256 values are identical"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1
    ]
  },
  {
    "id": "scp",
    "title": "scp: copy through an isolated key-only SSH server",
    "notes": "Local loopback only. Every scanned host-key entry is verified against the server public key generated in this same fixture. Private keys are temporary and never published.",
    "steps": [
      {
        "command": "ssh-keygen -lf host_key.pub && ssh-keygen -lf known_hosts",
        "output": "256 SHA256:gO4mMpGAth7kwo/+Omrc3oMEsLHbIt+e7JmJBENfcZ4 learner@ace52f81e5a7 (ED25519)\n256 SHA256:gO4mMpGAth7kwo/+Omrc3oMEsLHbIt+e7JmJBENfcZ4 [127.0.0.1]:2222 (ED25519)\n",
        "caption": "Compare against the generated server key before trusting the scan",
        "duration_ms": 4500
      },
      {
        "command": "scp -v -P 2222 -i client_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/work/fixture/known_hosts payload.txt learner@127.0.0.1:/work/fixture/received.txt",
        "output": "Executing: program /usr/bin/ssh host 127.0.0.1, user learner, command scp -v -t /work/fixture/received.txt\nOpenSSH_8.9p1 Ubuntu-3ubuntu0.17, OpenSSL 3.0.2 15 Mar 2022\ndebug1: Reading configuration data /etc/ssh/ssh_config\ndebug1: /etc/ssh/ssh_config line 19: include /etc/ssh/ssh_config.d/*.conf matched no files\ndebug1: /etc/ssh/ssh_config line 21: Applying options for *\ndebug1: Connecting to 127.0.0.1 [127.0.0.1] port 2222.\ndebug1: Connection established.\ndebug1: identity file client_key type 3\ndebug1: identity file client_key-cert type -1\ndebug1: Local version string SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.17\ndebug1: Remote protocol version 2.0, remote software version OpenSSH_8.9p1 Ubuntu-3ubuntu0.17\ndebug1: compat_banner: match: OpenSSH_8.9p1 Ubuntu-3ubuntu0.17 pat OpenSSH* compat 0x04000000\ndebug1: Authenticating to 127.0.0.1:2222 as 'learner'\ndebug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory\ndebug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory\ndebug1: SSH2_MSG_KEXINIT sent\ndebug1: SSH2_MSG_KEXINIT received\ndebug1: kex: algorithm: curve25519-sha256\ndebug1: kex: host key algorithm: ssh-ed25519\ndebug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none\ndebug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none\ndebug1: expecting SSH2_MSG_KEX_ECDH_REPLY\ndebug1: SSH2_MSG_KEX_ECDH_REPLY received\ndebug1: Server host key: ssh-ed25519 SHA256:gO4mMpGAth7kwo/+Omrc3oMEsLHbIt+e7JmJBENfcZ4\ndebug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory\ndebug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory\ndebug1: Host '[127.0.0.1]:2222' is known and matches the ED25519 host key.\ndebug1: Found key in /work/fixture/known_hosts:1\ndebug1: ssh_packet_send2_wrapped: resetting send seqnr 3\ndebug1: rekey out after 134217728 blocks\ndebug1: SSH2_MSG_NEWKEYS sent\ndebug1: expecting SSH2_MSG_NEWKEYS\ndebug1: ssh_packet_read_poll2: resetting read seqnr 3\ndebug1: SSH2_MSG_NEWKEYS received\ndebug1: rekey in after 134217728 blocks\ndebug1: Will attempt key: client_key ED25519 SHA256:VahqSybqgWltl0cUdvGaWemxBAhquJwKHb1N8iFYyvA explicit\ndebug1: SSH2_MSG_EXT_INFO received\ndebug1: kex_input_ext_info: server-sig-algs=<ssh-ed25519,sk-ssh-ed25519@openssh.com,ssh-rsa,rsa-sha2-256,rsa-sha2-512,ssh-dss,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,sk-ecdsa-sha2-nistp256@openssh.com,webauthn-sk-ecdsa-sha2-nistp256@openssh.com>\ndebug1: kex_input_ext_info: publickey-hostbound@openssh.com=<0>\ndebug1: SSH2_MSG_SERVICE_ACCEPT received\ndebug1: Authentications that can continue: publickey\ndebug1: Next authentication method: publickey\ndebug1: Offering public key: client_key ED25519 SHA256:VahqSybqgWltl0cUdvGaWemxBAhquJwKHb1N8iFYyvA explicit\ndebug1: Server accepts key: client_key ED25519 SHA256:VahqSybqgWltl0cUdvGaWemxBAhquJwKHb1N8iFYyvA explicit\nAuthenticated to 127.0.0.1 ([127.0.0.1]:2222) using \"publickey\".\ndebug1: channel 0: new [client-session]\ndebug1: Requesting no-more-sessions@openssh.com\ndebug1: Entering interactive session.\ndebug1: pledge: network\ndebug1: client_input_global_request: rtype hostkeys-00@openssh.com want_reply 0\ndebug1: Remote: /work/fixture/authorized_keys:1: key options: agent-forwarding port-forwarding pty user-rc x11-forwarding\ndebug1: Remote: /work/fixture/authorized_keys:1: key options: agent-forwarding port-forwarding pty user-rc x11-forwarding\ndebug1: Sending environment.\ndebug1: channel 0: setting env LC_ALL = \"C.UTF-8\"\ndebug1: channel 0: setting env LANG = \"C.UTF-8\"\ndebug1: Sending command: scp -v -t /work/fixture/received.txt\nscp: debug1: fd 3 clearing O_NONBLOCK\nSending file modes: C0644 60 payload.txt\nSink: C0644 60 payload.txt\nscp: debug1: fd 6 clearing O_NONBLOCK\nscp: debug1: fd 0 clearing O_NONBLOCK\ndebug1: client_input_channel_req: channel 0 rtype exit-status reply 0\ndebug1: channel 0: free: client-session, nchannels 1\nTransferred: sent 2544, received 2396 bytes, in 0.0 seconds\nBytes per second: sent 53610.4, received 50491.5\ndebug1: Exit status 0\n",
        "caption": "Uppercase -P selects the SSH port; host-key checking stays enabled",
        "duration_ms": 4500
      },
      {
        "command": "sha256sum payload.txt received.txt",
        "output": "1541251a19553c9e2dde2a971aa6d3f7b894b008fa2941a3562c5ecb7dd6b01b  payload.txt\n1541251a19553c9e2dde2a971aa6d3f7b894b008fa2941a3562c5ecb7dd6b01b  received.txt\n",
        "caption": "Validate the destination contents, not an animated progress bar",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "scanned SSH key type and public-key bytes exactly match our generated server public key",
      "the known-hosts fingerprint equals the independently generated host-key fingerprint",
      "scp transfers through SSH with identical destination bytes",
      "source and SSH destination SHA-256 are equal"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1,
      2
    ]
  },
  {
    "id": "crontab",
    "title": "crontab: permissions and a minimal job environment",
    "notes": "No cron daemon runs and no crontab is installed. no-new-privileges blocks setgid elevation, so listing can be denied. The second step simulates a minimal shell environment, not cron scheduling.",
    "steps": [
      {
        "command": "crontab -l",
        "output": "crontabs/learner/: fopen: Permission denied\n",
        "caption": "This hardened container does not grant crontab setgid elevation",
        "duration_ms": 4500
      },
      {
        "command": "env -i PATH=/usr/bin:/bin /bin/sh /lab/cron_job.sh",
        "output": "HOME=not set\nPATH=/usr/bin:/bin\nshell=/lab/cron_job.sh\n/work/fixture\n1541251a19553c9e2dde2a971aa6d3f7b894b008fa2941a3562c5ecb7dd6b01b  /work/fixture/payload.txt\n",
        "caption": "Minimal-environment reproduction, not a real cron timer",
        "duration_ms": 4500
      }
    ],
    "checks": [
      "listing is empty or explicitly denied by the restricted container, without installing any schedule",
      "job works with explicitly declared PATH and absolute data path"
    ],
    "status": "passed",
    "gif_steps": [
      0,
      1
    ]
  }
]