diff --git a/README.md b/README.md
index 36f5076..abacfbf 100644
--- a/README.md
+++ b/README.md
@@ -11,8 +11,8 @@ example doubles as a standalone demo.
`systems-peer`) for anything privileged, kernel-observing, or two-host;
Podman LGTM stack (Grafana/Loki/Tempo/Mimir) + Performance Co-Pilot for
observability chapters.
-- **eBPF scope**: tooling only (bcc-tools, bpftrace, bpftool) — we observe our
- userspace programs; we do not write kernel eBPF.
+- **eBPF scope**: tooling only (bcc-tools, bpftrace, bpftool); we observe our
+ userspace programs and do not write kernel eBPF.
## Local development
@@ -24,7 +24,7 @@ bundle exec jekyll serve --baseurl "" # http://127.0.0.1:4000/
## Verification model
CI validates site integrity and that examples *build*. It cannot run VM, root,
-or observability demos — the authoritative gate is a host run of
+or observability demos: the authoritative gate is a host run of
`scripts/test-all-examples.py --mode all` with the lab up. A demo is marked
**verified** only when it produced its claimed observable effect, never merely
because it compiled.
@@ -33,12 +33,12 @@ because it compiled.
This project is dual-licensed to separate the code from the writing:
-- **Code** — everything under `examples/`, plus build and helper scripts and
- inline code snippets — is licensed under the **Apache License 2.0**
+- **Code**: everything under `examples/`, plus build and helper scripts and
+ inline code snippets, is licensed under the **Apache License 2.0**
([`LICENSE`](LICENSE)).
-- **Content** — the tutorial and book prose (chapters under `_docs/`),
+- **Content**: the tutorial and book prose (chapters under `_docs/`),
diagrams and images under `assets/`, and the slide decks under
- `presentation/` — is licensed under **Creative Commons Attribution 4.0
+ `presentation/`, is licensed under **Creative Commons Attribution 4.0
International (CC BY 4.0)** ([`LICENSE-docs.md`](LICENSE-docs.md)).
Copyright 2026 PatternCatalyst (Robert Sedor).
diff --git a/_docs/00-outline.md b/_docs/00-outline.md
index a819fb3..48450df 100644
--- a/_docs/00-outline.md
+++ b/_docs/00-outline.md
@@ -13,7 +13,7 @@ switchable code tabs, and every example doubles as a standalone demo.
{% include excalidraw.html
file="00-course-map"
- alt="Course map: four journey stages - Foundations, Core systems, Operating context, and Mastery - shown as bands containing the fourteen parts with their chapter ranges, and the six artifacts fwatch, shmkv, chatterd, pmon, bugfarm, and sysagent along the bottom, each with an arrow to the part where it grows."
+ alt="Course map: four stages, Foundations, Core systems, Operating context, and Mastery, shown as bands containing the fourteen parts with their chapter ranges, and the six artifacts fwatch, shmkv, chatterd, pmon, bugfarm, and sysagent along the bottom, each with an arrow to the part where it grows."
caption="Figure 0.1: the course at a glance." %}
Two threads run through the whole book and never stop:
@@ -63,8 +63,8 @@ artifacts that grow chapter by chapter:
Examples run **locally** on your Fedora 44 machine unless they need root, kernel
observation, or a second host; those run in the disposable KVM lab
-(`systems-target`, and `systems-peer` for two-host networking) that Chapter 2
-provisions. Observability chapters use a local Podman LGTM stack (Grafana,
+(`systems-target`, and `systems-peer` for two-host networking) that the lab
+setup provisions. Observability chapters use a local Podman LGTM stack (Grafana,
Loki, Tempo, Mimir) plus Performance Co-Pilot from its container image. Nothing
experimental ever runs un-snapshotted on your workstation.
diff --git a/_docs/01-prerequisites.md b/_docs/01-prerequisites.md
index e89897a..b1e85f6 100644
--- a/_docs/01-prerequisites.md
+++ b/_docs/01-prerequisites.md
@@ -13,8 +13,8 @@ book was written against, which is why this chapter is less "install these
packages" and more "here is *why* each pin exists, and here is one script that
tells you whether your host matches." By the end you will have run the book's
first program, `examples/01-hello-syscalls`, in all three languages, and
-cross-checked with `strace` that each of them really made the syscalls the
-source code claims.
+cross-checked with `strace` that each of them made the syscalls the source
+code claims.
The code is in `examples/01-hello-syscalls/`. `./demo.sh` there builds and
runs all three implementations; its `README.md` covers the run-script contract
@@ -81,7 +81,7 @@ per run; this one accumulates every result into a table and exits non-zero
only at the end, so one pass gives you the full shopping list. Second, it
distinguishes **hard requirements** (`fail`, which gates the exit code) from
**soft** ones (`warn`, informational). That split encodes what the book
-actually needs versus what is merely convenient.
+needs versus what is merely convenient.
The hard requirements, and the reason each is hard:
@@ -299,7 +299,7 @@ are `CStr`-like views, and `to_string_lossy()` handles the (theoretical)
non-UTF-8 case instead of panicking. Three languages, three defaults: C++
makes you construct the error discipline yourself, Go hands you an error value
and conventions, and Rust moves part of the manpage into the type system.
-Chapter 5 turns this comparison into policy.
+This comparison is the seed of the book's errors-three-ways policy.
## Build, run, observe
@@ -383,7 +383,7 @@ step ever requires the IDE.
- Every example follows one `demo.sh` contract: `build`, `run`, and
`TARGET= verified: on the Fedora 44 host: verified: on the Fedora 44 host: unverified: On the lab VM: the kernel-side view of this chapter (watching unverified: On the lab VM: the kernel-side view of this chapter (watching verified: all evidence in this chapter was produced on the Fedora 44 host this session: each of the three binaries printed verified: all evidence in this chapter was produced on the Fedora 44 host this session: each of the three binaries printed podman.socket enabled, podman compose -f examples/_infra/compose-lgtm.yaml up -d brought lsp-lgtm (otel-lgtm:0.8.1) to healthy, /api/health returned ok, the OTLP HTTP POST to :4318/v1/traces returned 200, and the Tempo query by trace ID through Grafana's datasource proxy returned the span r02-gate-smoke (service lsp-smoke). Not yet exercised: exporting telemetry from inside a lab VM via the 192.168.124.1 gateway; the endpoint-forwarding path is verified only as far as deploy-to-vm.sh running host-built binaries on the guest; a VM-side OTLP export lands with Chapter 38.podman.socket enabled, podman compose -f examples/_infra/compose-lgtm.yaml up -d brought lsp-lgtm (otel-lgtm:0.8.1) to healthy, /api/health returned ok, the OTLP HTTP POST to :4318/v1/traces returned 200, and the Tempo query by trace ID through Grafana's datasource proxy returned the span r02-gate-smoke (service lsp-smoke). Not yet exercised: exporting telemetry from inside a lab VM via the 192.168.124.1 gateway; the endpoint-forwarding path is verified only as far as deploy-to-vm.sh running host-built binaries on the guest; a VM-side OTLP export lands with OpenTelemetry instrumentation.sys_enter_* tracepoints fire with bpftrace as sysprobe runs, counting traps from inside the kernel instead of via ptrace) needs root and a disposable kernel, so it is not run here; chapter 30 (Debugging part) exercises exactly this on systems-target.sys_enter_* tracepoints fire with bpftrace as sysprobe runs, counting traps from inside the kernel instead of via ptrace) needs root and a disposable kernel, so it is not run here; the eBPF observation toolkit exercises exactly this on systems-target.copied 200000 bytes (exit 0) with cmp exiting 0, copyx: nope.bin: No such file or directory (exit 2), and copyx: write /dev/full: No space left on device (exit 3); the strace excerpt is real output showing write(4, …, 65536) = -1 ENOSPC before the exit-3; the Go trace showed the failing write on descriptor 5 with cpu.max held on 3, and the Rust trace showed the fragmented stderr writes; ls -li /dev/full showed the 1, 7 character device; and scripts/test-all-examples.py --only 05-errors-three-ways reported PASS for cpp, go, and rust (3 passed, 0 failed, 0 skipped). The lab-VM eBPF callout above is the one unverified item, deferred to Chapter 30.copied 200000 bytes (exit 0) with cmp exiting 0, copyx: nope.bin: No such file or directory (exit 2), and copyx: write /dev/full: No space left on device (exit 3); the strace excerpt is real output showing write(4, …, 65536) = -1 ENOSPC before the exit-3; the Go trace showed the failing write on descriptor 5 with cpu.max held on 3, and the Rust trace showed the fragmented stderr writes; ls -li /dev/full showed the 1, 7 character device; and scripts/test-all-examples.py --only 05-errors-three-ways reported PASS for cpp, go, and rust (3 passed, 0 failed, 0 skipped). The lab-VM eBPF callout above is the one unverified item, deferred to the eBPF observation toolkit.biolatency (the block
I/O latency histogram that separates fsync-every's many small flushes from
direct mode's steady stream) would show the kernel side of these runs.
They need root and a kernel with BPF tooling, so they are not run on this
-host; chapter 30 (Debugging part) exercises them on the lab VM.
tracepoint:syscalls:sys_enter_epoll_wait
and kprobes on the eventpoll internals, and forcing a real
IN_Q_OVERFLOW by shrinking max_queued_events,
-needs root and a disposable kernel, so it is not run here; chapter 30
-(Debugging part) exercises exactly this on systems-target.
+needs root and a disposable kernel, so it is not run here; the eBPF
+observation toolkit exercises exactly this on systems-target.
## What you learned
diff --git a/_docs/10-io-uring.md b/_docs/10-io-uring.md
index 8ef757d..d270386 100644
--- a/_docs/10-io-uring.md
+++ b/_docs/10-io-uring.md
@@ -14,15 +14,15 @@ By the end of this chapter you can:
- Construct an io_uring instance with `io_uring_setup`, map the SQ ring, CQ ring, and SQE array via the canonical `mmap`s, and submit batched operations with `io_uring_enter`.
- Write and link submission queue entries (`IORING_OP_READ`/`IORING_OP_WRITE`) with `IOSQE_IO_LINK` to express intra-pair ordering without a userspace round trip.
- Measure when completion-based I/O helps and when it costs more than a synchronous read/write loop, using `strace -c` syscall counts and wall-clock timings.
-- Account for io_uring's security exposure, including why container seccomp profiles deny it by default, and explain Go's deliberate refusal to expose a uring engine.
+- Account for io_uring's security exposure, including why container seccomp profiles deny it by default, and explain why Go refuses to expose a uring engine.
## Why this matters
Readiness and completion are the two mental models underlying every I/O-heavy system on Linux, and knowing which one a workload needs is a design decision with measurable consequences: this chapter's own numbers show a completion-based engine running roughly ten times slower than a synchronous loop on warm, small files, while the same engine would win decisively against deep queues on real storage or busy sockets. Fewer syscalls is not automatically faster, and this chapter proves it rather than asserting it.
-The chapter also surfaces a security tradeoff worth carrying forward: io_uring's opcode-dispatched interface can bypass syscall filters written against `read`/`write`/`connect`, which is why container runtimes deny it by default. That tension between a powerful kernel interface and a shrinking attack surface returns when Part 7 moves these programs into containers.
+The chapter also surfaces a security tradeoff worth carrying forward: io_uring's opcode-dispatched interface can bypass syscall filters written against `read`/`write`/`connect`, which is why container runtimes deny it by default. That tension between a broad kernel interface and a shrinking attack surface returns when these programs move into containers.
-Chapter 9 ended with `fwatch` blocked in an event loop, and with a promise:
+Epoll left `fwatch` blocked in an event loop, and with a promise:
epoll would not be the last word on Linux I/O. Epoll answers one question:
*which of these fds may I touch without blocking?* Then it leaves every
actual `read(2)` and `write(2)` for you to make, one syscall per operation.
@@ -33,9 +33,8 @@ Instead it *describes* batches of operations in memory the kernel shares with
the process, and submits sixteen of them per syscall. That is the
**completion model**, and readiness-versus-completion is the mental-model
pair this whole Part has been building toward. The chapter also delivers two
-deliberate surprises: our uring engine is measurably *slower* on this
-workload, and one of our three languages refuses to play at all. Both are
-intended, and both are worth understanding.
+surprises: our uring engine is measurably *slower* on this
+workload, and one of our three languages refuses to play at all.
The code is in `examples/10-io-uring/`. `./demo.sh` there builds and runs all
three implementations; its `README.md` covers the `scan`/`watch`/`sync`
@@ -53,8 +52,8 @@ subcommands and the two-engine contract.
## Readiness asks "may I?", completion says "it is done"
Every I/O interface you have met so far is synchronous at the moment of data
-movement: `read(2)` in chapter 7, `write(2)` through the page cache in
-chapter 8, and epoll in chapter 9, which, for all its scalability, only
+movement: `read(2)` on RAII-wrapped fds, `write(2)` through the page cache,
+and epoll, which, for all its scalability, only
*schedules* those same calls. Readiness is a hint about the future ("fd 7
will not block right now"), and it can even be wrong: a readable socket can
still return `EAGAIN` by the time you get there.
@@ -104,13 +103,13 @@ runtimes responded directly: on this host, podman's default seccomp profile
and its allowlist contains **zero** mentions of `io_uring_setup`,
`io_uring_enter`, or `io_uring_register`: deny by default. Run the uring
engine in an unmodified container and `io_uring_setup` fails with `EPERM`.
-Chapter 34, when our programs move into containers, deals with that head-on.
+Containerizing these programs deals with that head-on.
## Go's deliberate absence
`fwatch sync --engine uring` in Go prints
-`engine=uring: unsupported in Go (see chapter 10)` and exits 64. That is not
-laziness: it is the chapter's design lesson. Go already solved "many I/O
+`engine=uring: unsupported in Go (see chapter 10)` and exits 64: the chapter's
+design lesson. Go already solved "many I/O
operations, few OS threads" once, inside the runtime: every goroutine writes
ordinary blocking code, and the **netpoller** multiplexes those goroutines
over epoll, parking them until readiness arrives. The concurrency model *is*
@@ -414,7 +413,7 @@ contract cannot express the engine at all.
### Concurrency lens
Strip away the I/O and Figure 10.1 is a pair of single-producer,
-single-consumer lock-free ring buffers: the kernel is simply *the other
+single-consumer lock-free ring buffers: the kernel is *the other
thread*. That is why `try_get_sqe`/`submit_and_wait` are written in
acquire/release vocabulary: the store-release of the SQ tail publishes the
filled SQEs, the kernel's load-acquire observes them, and the mirror-image
@@ -426,7 +425,7 @@ not get to declare `std::atomic`. Meanwhile the kernel side is concurrent
too: `IORING_FEAT_NATIVE_WORKERS` in our trace is the io-wq worker-thread
pool, which executes any submitted operation that cannot complete
immediately, our buffered file writes among them, a fact that returns in
-the timings. Chapter 26 rebuilds this exact structure in user space, barrier
+the timings. Lock-free atomics rebuild this exact structure in user space, barrier
by barrier.
## Build, run, observe
@@ -448,8 +447,8 @@ cache, `ms=` as printed by the tool:
| `uring` | 129, 137, 130 ms | 105, 107, 107 ms | exit 64 (by design) |
On this workload, our uring engine is roughly **ten times
-slower**. Nothing is broken: the copies are byte-identical (checked below).
-The model just costs more than it saves here. Every file is page-cache
+slower**. The copies are byte-identical (checked below); the model just costs
+more than it saves here. Every file is page-cache
hot, so each `read(2)` in the rw loop is a short memcpy; the syscalls we so
cleverly eliminated were never the bottleneck. The uring path, meanwhile,
pays for what buffered file I/O cannot do asynchronously inline: the
@@ -457,7 +456,7 @@ kernel punts those operations to io-wq worker threads, buying context
switches and wakeups per batch, and our engine waits for each batch to
fully retire before building the next, with unregistered buffers reallocated
per file. This is the batch-size lesson the chapter owes you: completion
-wins when queues are deep, operations are actually asynchronous, and fixed
+wins when queues are deep, operations are asynchronous, and fixed
costs get amortized across real waiting, not when the "I/O" is a memcpy
that was already cheaper than the bookkeeping. On the small verify fixture
(5 files, 1,348,634 bytes) the gap shrinks to noise: `rw` printed `ms=0`,
@@ -508,8 +507,8 @@ claim above is a one-liner you can run yourself: `grep -c io_uring
> the natural next probe is watching io-wq from the inside,
> `bpftrace -e 'tracepoint:io_uring:io_uring_submit_req { @[comm] = count(); }'`
> while the two engines run. bcc/bpftrace are not part of this host's
-> toolchain by design; chapter 30 (Debugging part) builds that eBPF
-> observation kit on the lab VM and exercises exactly this.
+> toolchain by design; the eBPF observation toolkit covers that setup
+> on the lab VM and exercises exactly this.
## What you learned
@@ -526,7 +525,8 @@ claim above is a one-liner you can run yourself: `grep -c io_uring
small and fully synchronous. Completion pays off at depth, on real
waiting.
- io_uring's power is why container seccomp profiles deny it by default
- (podman's allowlist has no io_uring entries on this host; chapter 34),
+ (podman's allowlist has no io_uring entries on this host, confirmed when
+ these programs move into containers),
and why Go declines to expose it at all: the netpoller already owns the
event loop (golang/go#31908 stays on hold).
diff --git a/_docs/11-process-lifecycle.md b/_docs/11-process-lifecycle.md
index 6beae7a..755241a 100644
--- a/_docs/11-process-lifecycle.md
+++ b/_docs/11-process-lifecycle.md
@@ -10,7 +10,7 @@ duration: "45 minutes"
By the end of this chapter you can:
-- Trace a process from `fork(2)` through `execve(2)` to `wait4(2)`/`waitid(2)`, and explain what copy-on-write actually copies.
+- Trace a process from `fork(2)` through `execve(2)` to `wait4(2)`/`waitid(2)`, and explain what copy-on-write copies.
- Build a `O_CLOEXEC` self-pipe that lets a parent distinguish "exec failed" from "the program ran and exited."
- Observe a zombie process live in `ps` and `/proc/ERESTART_RESTARTBLOCK on poll; and the
SigCgt 0, SigCgt fffffffd7fc1feff,
SigCgt 14443) were read from live supervisors and decoded with
the Python shown. The "On the lab VM" bpftrace callout is unverified as
-marked and deferred to chapter 30.
+marked and deferred to the eBPF observation toolkit.
diff --git a/_docs/13-pidfd-modern-process-mgmt.md b/_docs/13-pidfd-modern-process-mgmt.md
index 8b9c68e..ef59039 100644
--- a/_docs/13-pidfd-modern-process-mgmt.md
+++ b/_docs/13-pidfd-modern-process-mgmt.md
@@ -21,7 +21,7 @@ By the end of this chapter you can:
A supervisor that names its children by pid carries a race it usually never notices: any reap it does not control, a runtime, a library, a signal handler, can free that pid for reuse before the supervisor acts on it again. On a busy build server or a container with a small `pid_max`, that window is not theoretical.
-Pidfds close the race by making a process a referenceable object instead of a reused number, and they compose directly with the event-loop model Chapter 9 built: a process exit becomes ordinary fd readiness. This chapter makes pidfds pmon's default engine, completing the supervision story Chapters 11 and 12 started before Chapter 14 turns to privilege.
+Pidfds close the race by making a process a referenceable object instead of a reused number, and they compose directly with the epoll-based event-loop model: a process exit becomes ordinary fd readiness. Making pidfds pmon's default engine completes the supervision story that restart-on-crash and signal handling started, before identity and privilege take over.
`pmon` v1 ended the last chapter supervising a crashing child correctly: no
async handlers, SIGCHLD arriving as data through a signalfd, a `WNOHANG` reap
@@ -32,7 +32,7 @@ come to mean a completely different process, and `kill(2)` will cheerfully
signal the stranger. The one new idea in this chapter is that a process can
be a *file descriptor*: `pidfd_open(2)` hands you an fd that refers to the
process itself, not to its number, pollable in exactly the event-loop style
-Chapter 9 built, reapable with `waitid(2)`, signalable with
+built on epoll, reapable with `waitid(2)`, signalable with
`pidfd_send_signal(2)`, and immune to reuse by construction. `pmon` v2 makes
that its default engine and keeps the v1 sigchld engine beside it,
byte-identical in output, so you can strace the difference.
@@ -61,8 +61,8 @@ table, nothing more; on this host the table wraps at
zombie: the pid stays reserved *only* until someone collects the exit
status. (3) Someone does, and here is the trap: in any program bigger than
a toy, "someone" is not always you. A library thread that spawns helpers, a
-language runtime that reaps everything (Chapter 11 showed Go's runtime
-holding exactly this power), a signal handler someone else installed: any
+language runtime that reaps everything (Go's runtime holds exactly this
+power), a signal handler someone else installed: any
`wait()` in the process can free the pid. (4) The kernel recycles 4242 for
an unrelated process. Under default settings that needs the counter to wrap,
but a busy build server wraps in hours, and a container with a small
@@ -85,12 +85,12 @@ The fix arrived in a burst across Linux 5.1–5.4, and this host's manpages
(kernel `7.1`) document each piece. `pidfd_open(2)`'s manpage (HISTORY:
"Linux 5.3") says it "creates a file descriptor that refers to the process whose
PID is specified in pid"; the close-on-exec flag is set automatically, so
-the Chapter 7 discipline costs nothing here. The fd's superpower is stated
+the RAII file-descriptor discipline costs nothing here. The fd's superpower is stated
under NOTES: it "can be monitored using poll(2), select(2), and epoll(7).
When the process that it refers to terminates, these interfaces indicate the
file descriptor as readable": a process exit becomes an ordinary readiness
-event, composable with every other fd your Chapter 9 event loop owns. There
-is nothing to actually read: `read(2)` on a pidfd fails with `EINVAL`. The
+event, composable with every other fd your epoll event loop owns. There
+is nothing to read: `read(2)` on a pidfd fails with `EINVAL`. The
manpage also warns that an fd obtained by opening `/proc/FUTEX_WAKE INT_MAX returning 1, slashless kernel mq names,
mq_unlink on exit); the context-switch evidence (watch 2→10,
poll 285→4,059 over 4 s; rusage 120 vs 2,002) was sampled live; and the
-perf_event_paranoid = 2 limitation was observed, not assumed.
+perf_event_paranoid = 2 restriction was observed, not assumed.
The bcc-tools callout is unverified as marked, and all latency absolutes are
bounded by this host.
diff --git a/_docs/21-tcp-sockets.md b/_docs/21-tcp-sockets.md
index 3a9ad32..e4f9fde 100644
--- a/_docs/21-tcp-sockets.md
+++ b/_docs/21-tcp-sockets.md
@@ -22,8 +22,8 @@ By the end of this chapter you can:
- Build `chatterd` v0, a thread-per-connection TCP chat room, in C++, Go,
and Rust with an RAII/move-only socket that owns every fd
- Identify the two failure modes of thread-per-connection service
- (per-connection memory cost, lock-held-across-I/O blocking) that chapter
- 22 addresses
+ (per-connection memory cost, lock-held-across-I/O blocking) that an
+ epoll event loop with a thread pool fixes
- Verify a live server with `ss -tlnp`/`-tnp` and read the wire bytes at the
`read`/`write` syscall boundary with `strace`
@@ -33,7 +33,7 @@ A TCP server looks like a pipe with an address until the first production
incident proves otherwise: a restart that cannot rebind its own port, a
message that arrives split across two `read` calls, or a slow client that
freezes every other connection behind a shared lock. Each of those is a
-direct consequence of what TCP actually promises, ordered bytes with no
+direct consequence of what TCP promises: ordered bytes with no
message boundaries, not what a quick read-write test suggested.
This chapter builds `chatterd` v0 from the four raw syscalls that make a
@@ -56,7 +56,8 @@ on it: that a byte stream has no message boundaries, so *you* must add them.
The code is in `examples/21-tcp-sockets/`. `./demo.sh` there builds all three
implementations; its `README.md` fixes the CLI, the exit codes, and the wire
-frame that chapters 22–24 and 27 grow but never replace.
+frame that later event-loop, peer-discovery, deadline, and async-runtime work
+grows but never replaces.
{% include excalidraw.html
file="21-server-lifecycle"
@@ -102,8 +103,8 @@ Result01); ss -uanp showed the joined
chatterd fd-4 UDP socket and ip maddr show enp1s0 the
239.23.7.1 membership; and both guests' stdout printed one
discovered peer and one peer … says: line as quoted.
-No libvirt NAT workaround was required: the guests share one virbr0
+No special libvirt NAT configuration was required: the guests share one virbr0
L2 segment. The bcc-tools tcpconnect/tcplife/offcputime
callout is unverified as marked and is exercised in Part 8.
diff --git a/_docs/24-time-timers-deadlines.md b/_docs/24-time-timers-deadlines.md
index d849600..95521b0 100644
--- a/_docs/24-time-timers-deadlines.md
+++ b/_docs/24-time-timers-deadlines.md
@@ -42,7 +42,7 @@ jittered exponential backoff is the standard discipline for reconnecting to a
recovering service without a fleet of clients hammering it back down the
moment it comes up.
-Chapters 21–23 built `chatterd` into a peer-to-peer chat daemon that links two
+`chatterd` is a peer-to-peer chat daemon that links two
hosts over TCP and passes framed messages. It has one fatal naïveté: it assumes
the peer stays. Pull the cable, kill the process, or let a switch drop the
flow, and a `chatterd` peer sits forever in a `read` that will never return:
@@ -55,13 +55,13 @@ turns a heartbeat into a liability the first time NTP corrects your wall clock.
The code is in `examples/24-time-timers-deadlines/`. `./demo.sh` there builds
all three implementations; its `README.md` specifies the CLI, the wire format
-(the same canonical frame chapters 21–23 used, now carrying a `PING` TYPE this
+(the same canonical frame the chat protocol already used, now carrying a `PING` TYPE this
chapter adds), and the exit codes all three languages share.
{% include excalidraw.html
file="24-clock-taxonomy"
alt="Top band: the four clocks clock_gettime can read as boxes: CLOCK_REALTIME (dashed, wall time, settable, can step backward), CLOCK_MONOTONIC (amber, since a boot epoch, never steps, steady_clock/Instant), CLOCK_BOOTTIME (monotonic plus suspend time), and PROCESS_CPUTIME_ID (this process's on-CPU time). Bottom band: while a peer is silent and ntpd corrects the wall clock, a dashed REALTIME box shows a wall-clock deadline rewinding so a dead peer looks alive, beside an amber MONOTONIC box where elapsed is always non-negative and the timeout fires on real silence; an amber arrow from CLOCK_MONOTONIC points to it labeled chatterd reads this."
- caption="Figure 24.1: the clock taxonomy and the monotonic-vs-wall bug: a wall-clock deadline slips when the clock is stepped; a CLOCK_MONOTONIC deadline does not" %}
+ caption="Figure 24.1: the clock taxonomy and the monotonic-vs-wall-clock hazard: a wall-clock deadline slips when the clock is stepped; a CLOCK_MONOTONIC deadline does not" %}
> **Tools used**: `strace`, `python3` (host); `cyclictest` (systems-target
> VM, exercised in Part 8). Everything host-side is checked by
@@ -85,7 +85,7 @@ useful for profiling, useless for wall deadlines.
The rule that follows is absolute: **any duration or deadline must be measured
on a monotonic clock.** A heartbeat timeout is a duration ("no traffic for
3 seconds"), so it reads `CLOCK_MONOTONIC`, never `CLOCK_REALTIME`. Figure 24.1
-is the bug you avoid by obeying this. Suppose you computed a deadline as
+is the failure mode you avoid by obeying this. Suppose you computed a deadline as
`wall_now() + 3s` and stored it. Halfway through, `ntpd` decides the clock is
0.5 s fast and steps it *back*. Now `wall_now()` reads earlier than when you
started; your deadline recedes into the future; a peer that died stays "alive"
@@ -100,7 +100,7 @@ so `time.Since` cannot be fooled by a wall-clock step.
Reading the clock happens constantly (on every loop iteration `chatterd`
recomputes how long until the deadline), so it had better be cheap. It is, and
-the reason is chapter 4's vDSO. `CLOCK_MONOTONIC` and `CLOCK_REALTIME` reads do
+the reason is the vDSO. `CLOCK_MONOTONIC` and `CLOCK_REALTIME` reads do
not trap into the kernel: the kernel maps a shared page (the vDSO) into every
process, updates a timekeeping structure there on each tick, and the libc
`clock_gettime` wrapper reads it in userspace with no `syscall` instruction.
@@ -227,8 +227,8 @@ timer wrapped in a file descriptor: `timerfd_create(CLOCK_MONOTONIC, …)` makes
one, `timerfd_settime` arms it (an `it_value` with no `it_interval` is a
one-shot; both set makes it periodic), and it becomes *readable* when it
expires: a `read` returns a `uint64` count of expirations. That last property
-is why it belongs in this chapter: a timer that is an fd drops straight into the
-`poll`/`epoll` loop chapter 9 built, no signal handler required.
+is why it belongs in this chapter: a timer that is an fd drops straight into an existing
+`poll`/`epoll` event loop, no signal handler required.
The session loop is where the deadline lives. In the C++ `run_session`, every
iteration computes how long to block from the monotonic deadline, and hands
@@ -252,7 +252,7 @@ MSG, or PING, all count as traffic. The `poll` array holds three fds: the
socket. When `poll` returns 0, the timeout elapsed with nothing to read (the
peer went silent for `timeout_ms`) and the session returns `TimedOut`. When
the heartbeat fd fires, the loop reads its 8-byte tick and sends a `PING`. This
-is the same one-loop-three-sources shape as chapter 9's `fwatch`, now serving
+is the same one-loop-three-sources shape as the `epoll`-based `fwatch` event loop, now serving
liveness instead of file events. The three languages express the deadline
differently: C++ subtracts two `time_point`s, Rust does the identical
arithmetic on `Instant`, and Go resets a `time.Timer` and `select`s on its
@@ -311,7 +311,7 @@ error returns in Go, `Result` in Rust, all printing the same `usage:` block.
The C++ and Rust sessions are single-threaded: one `poll` loop multiplexes the
socket, the heartbeat timerfd, and the shutdown signalfd, so there is no shared
state to lock: the timer and the deadline are just two more fds and one
-`time_point`. Go inverts it, exactly as chapter 9 did: a per-connection reader
+`time_point`. Go inverts it, exactly as its event-loop examples did: a per-connection reader
goroutine turns the socket into a `chan frame`, and the session is a `select`
over four cases: `ticker.C` (heartbeat), `deadline.C` (liveness), the frame
channel, and `ctx.Done()` (shutdown from `signal.NotifyContext`). The subtlety
@@ -344,7 +344,7 @@ All three languages agree to within noise: Go printed `actual=1066us` /
`1005us`, Rust `1055us` / `1002us`. The number to sit with is the overshoot: a
1 ms sleep took ~1056 µs, ~56 µs long. That gap is not the clock's resolution
(1 ns); it is the *scheduler's wake-up latency*, the time between the timer
-expiring and your thread actually getting a CPU. On a general-purpose kernel
+expiring and your thread getting a CPU. On a general-purpose kernel
this is tens of microseconds and variable; the timerfd path is a touch tighter
(~1013 µs) because it wakes straight out of `read` rather than through
`nanosleep`'s return. Now the heartbeat scenario, driven by hand: a listener
@@ -431,7 +431,7 @@ Every claim in the chapter is on those lines. `timerfd_settime` arms the
heartbeat with a 300 ms interval. The `sendto(4, "CH\1\1\0\1B", 7, …)` is the
7-byte JOIN frame, `43 48 01 01 00 01 42`, the canonical header (`CH`,
version 1, type 1 = JOIN) plus the name `B`, matching the wire format byte
-for byte, the same header chapters 21–23 used; only the TYPE byte and the
+for byte, the same header the chat protocol already used; only the TYPE byte and the
engine around it differ by chatterd version. When fd 5 (the timerfd) fires,
the loop reads its 8-byte tick (`\1\0\0\0\0\0\0\0`, one expiration) and
sends the 6-byte PING `CH\1\5\0\0` (`43 48 01 05 00 00`). And
diff --git a/_docs/28-gdb-and-remote-debugging.md b/_docs/28-gdb-and-remote-debugging.md
index 717ef24..19b033a 100644
--- a/_docs/28-gdb-and-remote-debugging.md
+++ b/_docs/28-gdb-and-remote-debugging.md
@@ -37,7 +37,7 @@ occasionally or only in an environment you cannot freely recompile in.
The three languages also make the same bug look completely different at the
tooling layer: a core file is the right artifact for C++, useless for Rust's
panic-and-unwind, and only accidentally adequate for Go once goroutines enter
-the picture. Knowing which tool actually sees your language's failure mode,
+the picture. Knowing which tool sees your language's failure mode,
and which one just looks like it does, is what keeps a debugging session from
turning into a search for frames that were never going to be there.
@@ -92,7 +92,7 @@ gdb's job starts where that photograph does. Opened against a binary that
still carries debug info; this chapter's C++ build uses the `release`
CMake preset, which is `RelWithDebInfo` (`-O2 -g`, optimized *and*
debuggable), gdb reconstructs source-level frames, local variables, and
-even C++ types from the DWARF the compiler embedded, not from guesswork.
+even C++ types straight from the DWARF the compiler embedded.
That last part is what "pretty-printers" buy you: without them, a
`std::string_view` is a two-word struct (`{ _M_str, _M_len }`) and you'd read
its text by hand out of memory; with libstdc++'s printers loaded (they ship
@@ -162,7 +162,7 @@ Thread 1 "app" received signal SIGSEGV, Segmentation fault.
That result is flattering and it hides the real problem. gdb's thread list
maps to **OS threads** (the `M`s in Go's `G`/`M`/`P` scheduler), not to
**goroutines** (`G`s): a goroutine that is parked on a channel, blocked in a
-`select`, or simply not currently scheduled onto any `M` has no OS thread for
+`select`, or not currently scheduled onto any `M` has no OS thread for
gdb to show you, and gdb has no notion of "goroutine 41" as a selectable
frame context the way it has "thread 3". The moment your program has more
than a handful of concurrent goroutines, `info threads` and `bt` stop mapping
@@ -241,7 +241,7 @@ Remote debugging using systems-target:2345
afterward; the right default for a scripted reproduction, since a stray
`gdbserver` left listening on a lab VM is a debug backdoor with no
authentication. `target remote` is the one gdb command that changes
-everything downstream of it: every `break`, `run` (really `continue`, since
+everything downstream of it: every `break`, `run` (which behaves as `continue`, since
the process is already started), `print`, and `bt` you already know behaves
identically, dispatched over the wire instead of a local `ptrace(2)`. The
binary path you pass to gdb on the host must be the *unstripped*, debug-info
@@ -397,20 +397,20 @@ languages part ways, and the table is the chapter in miniature:
| Go | nil deref → runtime catches `SIGSEGV`, converts to panic | 2 | no | the goroutine trace in the panic output |
| Rust | `Option::unwrap()` on `None` → panic | 101 | no | `RUST_BACKTRACE=1` |
-One more thing worth reading off the C++ trace directly: the `ShutdownGuard`
-RAII type in `run_supervisor`'s clean path prints "supervisor exiting
-cleanly" from its destructor, and that line never appears on the crash path,
-confirmed in every transcript above. RAII buys you cleanup on every
-*C++-level* exit (`return`, a thrown exception, `std::exit` via a registered
-handler); it buys you nothing against a signal, because a signal does not
-unwind the stack; the kernel simply stops the process where it stands.
+The C++ trace also shows the `ShutdownGuard` RAII type in `run_supervisor`'s
+clean path printing "supervisor exiting cleanly" from its destructor; that
+line never appears on the crash path, confirmed in every transcript above.
+RAII buys you cleanup on every *C++-level* exit (`return`, a thrown
+exception, `std::exit` via a registered handler); it buys you nothing
+against a signal, because a signal does not unwind the stack: the kernel
+stops the process where it stands.
## What each language's tooling gives you
-This chapter is tooling-heavy rather than concurrency-heavy, so the lens
-here is what each debugger sees, not a data race: gdb sees raw memory,
-registers, and DWARF: it is precise but blind to any concept a language
-runtime invented for itself. That is exactly why it is the *only* tool for
+gdb, `delve`, and `rust-gdb` differ in what they see: gdb sees raw memory,
+registers, and DWARF, precise but blind to any concept a language runtime
+invented for itself.
+That is exactly why it is the *only* tool for
the C++ crash (there is no higher-level runtime to ask) and only an
*accidentally* adequate one for Go (it happened to catch this crash while
the program was still single-goroutine); `delve` exists specifically to
@@ -484,7 +484,7 @@ without it.
Every address gdb reported above is derived from the same `.debug_line`
section `readelf` and `objdump` can decode independently, with no gdb
involved at all; if they agree, gdb isn't inventing anything. `readelf -S`
-first confirms the binary actually carries full DWARF (it is optimized, not
+first confirms the binary carries full DWARF (it is optimized, not
stripped):
```console
@@ -552,7 +552,7 @@ and gdb all read the same way.
on `.debug_line` are the independent cross-check that confirms gdb's own
addresses rather than trusting them blind.
-Next, **valgrind, sanitizers, and Miri**: three very different ways of
+Next, **valgrind, sanitizers, and Miri**: three different ways of
catching the memory bugs a debugger only shows you *after* they've already
happened.
diff --git a/_docs/29-valgrind-sanitizers-miri.md b/_docs/29-valgrind-sanitizers-miri.md
index 8670ff1..5df53ef 100644
--- a/_docs/29-valgrind-sanitizers-miri.md
+++ b/_docs/29-valgrind-sanitizers-miri.md
@@ -48,7 +48,7 @@ need to worry about (directly from this chapter's own catch/miss matrix) is
what turns "run every tool on everything" into picking the right one for the
binary in front of you.
-Chapter 28 read a crash after it happened. This chapter catches five bugs
+A debugger reads a crash after it happened. This chapter catches five bugs
*before* they get the chance to crash anything; or, in one case, watches a
tool fail to catch one at all. `bugfarm` is a single CLI,
`app nsenter -p -m -u -n ps -ef was run live and
showed the host's full process table, confirming the missing
/proc remount. The Go ENOMEM failure quoted in
the Concurrency lens is this session's own strace output
-against the pre-fix build, kept as evidence of a real, reproduced defect
+against the pre-fix build, kept as evidence of the real failure mode
before the Cloneflags fix that now ships and passes. A
-one-line verify.lua bug this chapter also surfaced and fixed
-(an unescaped - in a Lua pattern silently failing the
+one-line verify.lua fix this chapter also surfaced
+(an unescaped - in a Lua pattern that silently failed the
hostname assertion) is recorded in the example's own history, not
restated here as a finding about the chapter's subject matter.
diff --git a/_docs/33-seccomp-and-landlock.md b/_docs/33-seccomp-and-landlock.md
index 8b340a3..c05d4c2 100644
--- a/_docs/33-seccomp-and-landlock.md
+++ b/_docs/33-seccomp-and-landlock.md
@@ -2,7 +2,7 @@
title: "Seccomp and Landlock"
order: 33
part: "Containers and Virtualization"
-description: "fwatch sandboxes itself with two independent, unprivileged kernel layers -- a seccomp-bpf syscall allowlist built from a real strace profile, and a Landlock ruleset that restricts filesystem reads to one directory -- probing the Landlock ABI at runtime instead of assuming it, and proving both with negative controls that watch the exact denial happen."
+description: "fwatch sandboxes itself with two independent, unprivileged kernel layers (a seccomp-bpf syscall allowlist built from a real strace profile, and a Landlock ruleset that restricts filesystem reads to one directory), probing the Landlock ABI at runtime instead of assuming it, and proving both with negative controls that watch the exact denial happen."
duration: "55 minutes"
---
@@ -20,26 +20,26 @@ By the end of this chapter you can:
## Why this matters
-seccomp-bpf and Landlock let an unprivileged process restrict itself, no root, no namespace, no VM required. That makes them two of the cheapest isolation techniques available to any program this book has built, and they compose directly with the namespaces and cgroups from Chapter 32 as part of what a real container runtime assembles.
+seccomp-bpf and Landlock let an unprivileged process restrict itself, no root, no namespace, no VM required. That makes them two of the cheapest isolation techniques available to any program this book has built, and they compose directly with namespaces and cgroups as part of what a real container runtime assembles.
-Both are also easy to get wrong in ways that look fine until tested: a filter built from a guessed syscall list breaks the program it was meant to protect, an ABI assumed rather than probed misbehaves on a different kernel, and a restriction applied to only one thread in a multithreaded runtime leaves every other thread unsandboxed. The design discipline this chapter teaches, profile first, probe the ABI, verify with a negative control, is what turns seccomp and Landlock from documentation into a sandbox you can trust.
+Both also go wrong in ways that look fine until tested: a filter built from a guessed syscall list breaks the program it was meant to protect, an ABI assumed rather than probed misbehaves on a different kernel, and a restriction applied to only one thread in a multithreaded runtime leaves every other thread unsandboxed. The design discipline this chapter teaches, profile first, probe the ABI, verify with a negative control, is what turns seccomp and Landlock from documentation into a sandbox you can trust.
-Chapter 32 gave `fwatch` its own view of the process tree and its own
-resource limits through namespaces and cgroup v2 -- isolation the kernel
-grants to a group of processes. This chapter hands the same idea to a
+Namespaces and cgroup v2 already gave `fwatch` its own view of the process
+tree and its own resource limits: isolation the kernel grants to a group
+of processes. This chapter hands the same idea to a
*single* process, no grouping required: `watch --sandbox DIR` now installs
a syscall allowlist and a filesystem allowlist against itself, using two
mechanisms any unprivileged process can invoke directly. **seccomp-bpf**
decides, syscall by syscall, whether the calling process is allowed to make
this call at all. **Landlock** decides, path by path, which parts of the
filesystem that process may still read. Neither needs root, a namespace, or
-a VM -- which is also why every number in this chapter was reproduced on
+a VM, which is also why every number in this chapter was reproduced on
the plain host and confirmed unchanged on the `systems-target` lab guest.
The one new idea worth sitting with: seccomp-bpf is **classic BPF**, the
same in-kernel bytecode VM whose modern descendant, eBPF, this book has so
far only used as an *observation* tool (`bpftrace`, `bcc-tools`, `bpftool`
-in Part 8). A seccomp filter is the mirror image -- you are not attaching a
+in Part 8). A seccomp filter is the mirror image: you are not attaching a
program that watches a syscall and reports it, you are attaching a program
that *decides* the syscall's outcome, on the hot path, before the kernel
does the work. Same VM, same verifier discipline, opposite job.
@@ -50,7 +50,7 @@ contract, and the paired positive/negative verification in full.
{% include excalidraw.html
file="33-seccomp-filter-flow"
- alt="Two stacked bands. The top userspace band shows fwatch issuing a syscall such as epoll_wait, read, or socket, beside a note that this is classic BPF -- you write the filter, Part 8's bpftrace only observes. The bottom kernel band shows the filter running on every syscall entry: a BPF_LD_ABS load of the syscall number and architecture, a compiled jump table of 26 entries for cpp and rust or 39 for go, a match arrow to SECCOMP_RET_ALLOW that returns control to the caller, and a no-match arrow down to the filter's default action. Two default-action boxes sit side by side: SCMP_ACT_ERRNO(EPERM), the one fwatch actually installs, where the process keeps running and sees EPERM on socket(2); and a dashed, unused SCMP_ACT_KILL_PROCESS box, where a separate probe program's getpid() call ends the whole process with SIGSYS. Footer notes report the real guest transcript: socket(AF_INET,...) = -1 EPERM under fwatch's filter, and the ACT_KILL_PROCESS probe's getpid() ending in killed by SIGSYS (core dumped)."
+ alt="Two stacked bands. The top userspace band shows fwatch issuing a syscall such as epoll_wait, read, or socket, beside a note that this is classic BPF: you write the filter, Part 8's bpftrace only observes. The bottom kernel band shows the filter running on every syscall entry: a BPF_LD_ABS load of the syscall number and architecture, a compiled jump table of 26 entries for cpp and rust or 39 for go, a match arrow to SECCOMP_RET_ALLOW that returns control to the caller, and a no-match arrow down to the filter's default action. Two default-action boxes sit side by side: SCMP_ACT_ERRNO(EPERM), the one fwatch actually installs, where the process keeps running and sees EPERM on socket(2); and a dashed, unused SCMP_ACT_KILL_PROCESS box, where a separate probe program's getpid() call ends the whole process with SIGSYS. Footer notes report the real guest transcript: socket(AF_INET,...) = -1 EPERM under fwatch's filter, and the ACT_KILL_PROCESS probe's getpid() ending in killed by SIGSYS (core dumped)."
caption="Figure 33.1: a syscall's path through fwatch's seccomp-bpf filter: one cBPF program, run again on every entry, deciding ALLOW versus its default action" %}
> **Tools used**: `strace` (host and `systems-target` VM, to build and
@@ -66,18 +66,18 @@ contract, and the paired positive/negative verification in full.
`seccomp(2)` installs a cBPF program that the kernel runs on *every*
syscall the process makes from that point on. The program reads two fields
-out of `struct seccomp_data` -- the architecture and the syscall number --
+out of `struct seccomp_data` (the architecture and the syscall number)
and returns one of a small set of verdicts: `SECCOMP_RET_ALLOW` (run it),
`SECCOMP_RET_ERRNO` (fail it with a chosen errno, without ever entering the
real syscall handler), or `SECCOMP_RET_KILL_PROCESS` (end the process with
`SIGSYS`, no return). Once a filter is loaded it can only ever be
-*tightened* -- filters stack, and a later one can narrow what an earlier
+*tightened*: filters stack, and a later one can narrow what an earlier
one allowed but never widen it.
`fwatch`'s allowlist did not come from guessing. The comment in the C++
source says it directly: the syscall set is "empirically confirmed with
`strace -f` across a full watch session." That is the practical way to
-build a seccomp profile -- run the real workload under `strace -f -c`,
+build a seccomp profile: run the real workload under `strace -f -c`,
read off the syscalls it actually issues, and allowlist exactly those.
Reproduced on the host against the unsandboxed watch loop:
@@ -116,14 +116,14 @@ That is exactly the source of `kWatchSyscalls` in `examples/33-seccomp-and-landl
calls, the allocator's `mmap`/`munmap`/`brk`, and `fstat`/`rt_sigprocmask`).
The rows elided above (`execve`, `openat`, `pread64`, `access`,
`arch_prctl`, `set_tid_address`, `set_robust_list`, `prlimit64`, `rseq`) are
-the dynamic linker resolving `libc`/`libstdc++` *before* `main` runs --
+the dynamic linker resolving `libc`/`libstdc++` *before* `main` runs:
real syscalls this same process makes, but not ones the watch loop itself
issues, so they are outside what the filter needs to allow once it loads
after startup.
`install_seccomp` (same file) calls `seccomp_init(SCMP_ACT_ERRNO(deny_errno))`
--- that argument is the filter's **default action**, applied to anything
-not explicitly allowed -- then `seccomp_rule_add(ctx, SCMP_ACT_ALLOW, nr, 0)`
+(that argument is the filter's **default action**, applied to anything
+not explicitly allowed), then `seccomp_rule_add(ctx, SCMP_ACT_ALLOW, nr, 0)`
once per syscall, and `seccomp_load(ctx)` to install it. `seccomp_load`
requires `no_new_privs` first (set via `prctl`), a kernel guard against a
sandboxed process regaining privilege through a setuid binary it execs.
@@ -133,7 +133,7 @@ sandboxed process regaining privilege through a setuid binary it execs.
`fwatch` chose `SCMP_ACT_ERRNO(EPERM)`, not
`SCMP_ACT_KILL_PROCESS`, as its default action, and the difference is
observable, not cosmetic. With `ACT_ERRNO`, a denied syscall returns `-1`
-with `errno` set and the calling code continues to run -- the process can
+with `errno` set and the calling code continues to run: the process can
log the denial, clean up, and exit on its own terms; that is what makes
`probe --forbidden-syscall`'s own `EPERM`-and-continue behavior possible.
With `ACT_KILL_PROCESS`, a denied syscall never returns at all: the kernel
@@ -150,7 +150,7 @@ getpid() = 39
+++ killed by SIGSYS (core dumped) +++
```
-`strace` shows the syscall entering the kernel and *never returning* --
+`strace` shows the syscall entering the kernel and *never returning*:
the trailing line is `strace` reporting the process's death, not a normal
return value. `ACT_ERRNO` is the right choice whenever the caller has
useful cleanup to do or a human is watching the diagnostic; `ACT_KILL` is
@@ -162,17 +162,17 @@ continuing at all is the greater risk.
Landlock restricts what a process (and everything it execs from here on)
may still do to the filesystem, expressed as three calls with no
-convenience wrapper in any of the three languages' standard libraries --
+convenience wrapper in any of the three languages' standard libraries:
`landlock_create_ruleset`, `landlock_add_rule`, `landlock_restrict_self`,
reached through a raw `syscall(2)` in C++ and Go and through the `landlock`
crate (which itself wraps the same three numbers) in Rust. A **ruleset**
-declares *which access rights this program intends to handle at all* --
+declares *which access rights this program intends to handle at all*:
`fwatch` handles only `LANDLOCK_ACCESS_FS_READ_FILE` and `_READ_DIR`, and
leaves `handled_access_net` at zero. A **rule** then grants a subset of
those handled rights to one filesystem object, here a `PATH_BENEATH` rule
binding read rights to a directory fd. `restrict_self` is the point of no
return: it activates the ruleset against the *calling* process and every
-descendant, and there is no syscall to lift it -- the only way out is
+descendant, and there is no syscall to lift it: the only way out is
`execve`-ing a program that never asked for the restriction to begin with,
and even then the new program inherits the same ruleset.
@@ -190,7 +190,7 @@ fwatch: probe outside /etc/hostname: EACCES (Permission denied)
Same distribution, same syscall numbers, and yet the host (kernel
`7.1`) reports ABI 9 while `systems-target` (kernel
-`6.19.10-300.fc44.x86_64`) reports ABI 7 -- two real Fedora 44 kernels a
+`6.19.10-300.fc44.x86_64`) reports ABI 7, two real Fedora 44 kernels a
few point releases apart, disagreeing about what Landlock supports, while
agreeing completely on the observable behavior the ruleset enforces. Every
call in this example reaches that number the only real way: calling
@@ -201,30 +201,30 @@ misbehave the moment it ran on the guest.
{% include excalidraw.html
file="33-landlock-ruleset"
- alt="Two stacked bands. The top band builds the ruleset: landlock_create_ruleset called first with NULL/0/VERSION to probe the ABI, returning 9 on the host versus 7 on the systems-target guest despite both being Fedora 44 kernels; then landlock_create_ruleset(attr) with handled_access_fs set to READ_FILE and READ_DIR, feeding into landlock_add_rule binding a PATH_BENEATH rule to one directory's O_PATH file descriptor; then prctl(PR_SET_NO_NEW_PRIVS,1) feeding into landlock_restrict_self, marked irrevocable, with a note that Go's build also passes the TSYNC flag here to cover every OS thread already running. The bottom band shows the filesystem view after restrict_self: the sandboxed directory /tmp/lsp33-watch-sandbox with READ_FILE and READ_DIR still allowed and the watch loop working normally, beside /tmp/lsp33-outside-target/secret.txt where open(O_RDONLY) returns EACCES, with an arrow from the ruleset's restrict_self box down to each outcome, solid to the grant and dashed to the denial. A footer note across the bottom states that handled_access_net was left at zero in this ruleset, and that Landlock's TCP bind/connect rights have existed since ABI 4 and UDP rights only since ABI 10 -- so even at ABI 7 or 9 a Landlock-only sandbox does not cover the network by itself."
+ alt="Two stacked bands. The top band builds the ruleset: landlock_create_ruleset called first with NULL/0/VERSION to probe the ABI, returning 9 on the host versus 7 on the systems-target guest despite both being Fedora 44 kernels; then landlock_create_ruleset(attr) with handled_access_fs set to READ_FILE and READ_DIR, feeding into landlock_add_rule binding a PATH_BENEATH rule to one directory's O_PATH file descriptor; then prctl(PR_SET_NO_NEW_PRIVS,1) feeding into landlock_restrict_self, marked irrevocable, with a note that Go's build also passes the TSYNC flag here to cover every OS thread already running. The bottom band shows the filesystem view after restrict_self: the sandboxed directory /tmp/lsp33-watch-sandbox with READ_FILE and READ_DIR still allowed and the watch loop working normally, beside /tmp/lsp33-outside-target/secret.txt where open(O_RDONLY) returns EACCES, with an arrow from the ruleset's restrict_self box down to each outcome, solid to the grant and dashed to the denial. A footer note across the bottom states that handled_access_net was left at zero in this ruleset, and that Landlock's TCP bind/connect rights have existed since ABI 4 and UDP rights only since ABI 10, so even at ABI 7 or 9 a Landlock-only sandbox does not cover the network by itself."
caption="Figure 33.2: the Landlock ruleset/rule model, with the ABI probed live rather than assumed and the real grant/deny split from this chapter's probes" %}
-**What Landlock can and cannot do.** Filesystem mediation --
-open/read/write/execute/rename, scoped to a path -- has been solid since
+**What Landlock can and cannot do.** Filesystem mediation
+(open/read/write/execute/rename, scoped to a path) has been solid since
ABI 1 (Linux 5.13). Network mediation is newer and arrived in pieces:
`LANDLOCK_ACCESS_NET_BIND_TCP`/`_CONNECT_TCP` since ABI 4, but UDP
bind/connect rights not until ABI 10; `fwatch` targets neither, so on both
of these machines a Landlock-restricted process can still open arbitrary
-sockets outside the tree entirely -- that gap is exactly why `fwatch`
+sockets outside the tree entirely: that gap is exactly why `fwatch`
layers seccomp *underneath* it rather than relying on Landlock alone.
IPC-scoping flags (restricting signals and abstract-socket connections
between sandboxed and unsandboxed processes) arrived at ABI 6, so both
machines here have them even though neither example uses them. The
practical rule this chapter teaches: **probe the ABI, then gate each
-feature on the version number it actually needs** -- never gate on "did
+feature on the version number it actually needs**: never gate on "did
`landlock_create_ruleset` return non-negative."
## How the code works
Both mechanisms share one setup shape across all three languages: probe
the Landlock ABI, build and enforce the ruleset, then install the seccomp
-filter, each step logging a status line before the identical epoll watch
-loop from chapter 09 runs unmodified. C++ and Rust do this **in-process**
+filter, each step logging a status line before the identical epoll-and-
+inotify watch loop runs unmodified. C++ and Rust do this **in-process**
with raw syscalls; Go does something structurally different, and the
concurrency lens below explains why.
@@ -308,15 +308,15 @@ fn install_seccomp(allowed: &[i64], deny_errno: i32) -> Resultverified -- every +
verified: every
transcript and number above was produced this session on this host
(Fedora Linux 44, kernel 7.1, 16 cpus,
podman 5.8.4 rootless): python3 scripts/test-all-examples.py --only
diff --git a/_docs/35-virtualization-and-kvm.md b/_docs/35-virtualization-and-kvm.md
index 85e7d1d..94ee102 100644
--- a/_docs/35-virtualization-and-kvm.md
+++ b/_docs/35-virtualization-and-kvm.md
@@ -20,24 +20,23 @@ By the end of this chapter you can:
## Why this matters
-Choosing between a process, a container, a microVM, and a full VM is a cost decision, not an abstract security ranking, and the cost only becomes real once you measure it in the same units this book has used throughout: nanoseconds per syscall, gigabytes per second, operations per second. `overheadbench` turns "which deployment option" into a question with a number attached, run on the exact hardware and kernel you'll actually ship on.
+Choosing between a process, a container, a microVM, and a full VM is a cost decision, not an abstract security ranking, and the cost only becomes real once you measure it in the same units this book has used throughout: nanoseconds per syscall, gigabytes per second, operations per second. `overheadbench` turns "which deployment option" into a question with a number attached, run on the exact hardware and kernel you'll ship on.
The VMX mental model matters beyond benchmarking too: knowing that a VM exit is triggered by touching state the guest doesn't own, not merely by "leaving the guest", explains why a program's syscall-heavy and I/O-heavy paths can have wildly different overhead profiles inside the same guest, a distinction that shapes where to spend optimization effort.
Every daemon in this book has, until now, run either directly on the host or
inside a Podman container sharing the host's kernel. This chapter puts a
third option on the table: a full KVM guest with its own kernel, and asks the
-one question a systems programmer actually needs answered before choosing
+one question a systems programmer needs answered before choosing
between them: not "which is more secure" in the abstract, but *what does
each one cost*, measured in the same units this book has used throughout:
nanoseconds per syscall, gigabytes per second, operations per second.
`overheadbench` is a tri-language microbenchmark built for exactly this: the
same binary, unmodified, runs on the bare host, inside the `systems-target`
KVM guest, and inside a rootless Podman container, and the three runs land in
-one table. The isolation spectrum this chapter walks (process, container,
-microVM, full VM) is not a taxonomy exercise; it is the same spectrum every
-one of this book's daemons could be deployed onto, and this is the chapter
-that prices each rung.
+one table. The isolation spectrum spanning process, container, microVM, and
+full VM is not a taxonomy exercise; it is the same spectrum every one of
+this book's daemons could be deployed onto, and each rung gets a price here.
The code is in `examples/35-virtualization-and-kvm/`. The run script there
builds and runs it; its `README.md` covers the CLI and the sanity ranges
@@ -46,7 +45,7 @@ builds and runs it; its `README.md` covers the CLI and the sanity ranges
{% include excalidraw.html
file="35-kvm-virtio-stack"
alt="Three horizontal bands. Top: host, VMX root mode, showing kvm.ko and kvm_intel.ko managing EPT tables, a QEMU/VMM userspace box running the vCPU thread and virtio-blk backend, and host NVMe/btrfs where a real fsync lands. Middle: guest, VMX non-root mode, showing the guest kernel serving getpid in-guest with no host trap, overheadbench in guest ring 3 issuing the syscall bench's raw getpid loop and the io bench's open/write/fsync calls, a virtio-blk front-end driver, and a virtqueue descriptor ring. Bottom: the trapped path for fsync: fsync in the guest causes a VM exit, the KVM exit handler hands off to the QEMU vCPU thread, the virtio-blk backend issues the real host fsync and triggers VM enter back into the guest. A fourth band gives the measured overheadbench deltas: syscall about 88ns/call on both host and guest because getpid never triggers a vmexit, mem about 14 to 15 GB/s on both because the EPT walk cost is amortized after the TLB warms up, and io about 1.3k ops/s on the host against about 0.5k ops/s on the guest, the real vmexit tax."
- caption="Figure 35.1: the VMX root/non-root boundary, the virtio-blk round trip that fsync actually pays, and where overheadbench's own measured host-vs-guest deltas land" %}
+ caption="Figure 35.1: the VMX root/non-root boundary, the virtio-blk round trip that fsync pays, and where overheadbench's own measured host-vs-guest deltas land" %}
> **Tools used**: `overheadbench` itself, `podman` (build and run, host),
> `ssh`/`scp` via `scripts/lab/deploy-to-vm.sh` and the Python runner
@@ -56,7 +55,7 @@ builds and runs it; its `README.md` covers the CLI and the sanity ranges
> checked by `scripts/check-host.sh` or ship with Fedora's cloud image on the
> lab VMs.
-## What actually runs a VM
+## What runs a VM
A container's isolation is namespaces and cgroups layered over *one* kernel;
every syscall a containerized process makes is a syscall the host kernel
@@ -76,13 +75,13 @@ instructions execute directly on the real CPU (no instruction-level
emulation) until something the guest cannot handle itself forces a **VM
exit** back to root mode.
-The key fact this chapter measures is *what actually causes an exit*.
-Ordinary instructions (arithmetic, memory loads and stores, and crucially
+The key fact this chapter measures is *what causes an exit*.
+Ordinary instructions (arithmetic, memory loads and stores, and
**syscalls the guest kernel can serve itself**, like `getpid(2)`) never
leave non-root mode at all. The guest kernel's syscall entry point runs
exactly like it would on bare metal; there is no hypervisor in that path to
trap into. What *does* exit are instructions that touch state the guest does
-not really own: port I/O, certain MSR accesses, and, the one this chapter's
+not own: port I/O, certain MSR accesses, and, the one this chapter's
`io` bench hits on every iteration, an access to an emulated device like
`virtio-blk`, the paravirtualized block device virtio defines so guest I/O
doesn't have to trap on every disk instruction the way full hardware
@@ -107,7 +106,7 @@ beyond native.
Four points, not two. A **bare process** has no isolation boundary at all: a
syscall it makes *is* a host syscall, and an escape is
"you are already root, or you exploit a bug to become root." A **container**
-(Chapter 34's Podman UBI images) adds namespaces and cgroup v2 as a soft
+(the Podman UBI images built in the containers chapter) adds namespaces and cgroup v2 as a soft
boundary layered over the *same* kernel: still one syscall table, so
`overheadbench`'s `syscall` and `mem` numbers inside a container should track
the host almost exactly, and the `io` bench, run against a directory bind-
@@ -215,13 +214,14 @@ fn bench_syscall(iters: u64) -> f64 {
}
```
-Three deliberate choices, identical in all three: the loop calls a **raw**
+Three choices repeat, identical in all three: the loop calls a **raw**
syscall rather than a libc-cached identity (glibc does not cache `getpid`
today, but the explicit `syscall(SYS_getpid)`/`unix.Getpid`/`rustix::getpid`
spelling makes that guarantee visible in the source, not implicit in a libc
version); the clock (`std::chrono::steady_clock`, `time.Now`/`Since`,
-`Instant::now`) is a **monotonic** one, because Chapter 24 already
-established that a wall-clock read can step backward mid-measurement; and
+`Instant::now`) is a **monotonic** one, because the time-and-deadlines
+material already established that a wall-clock read can step backward
+mid-measurement; and
each language guards the compiler from proving the loop is dead:
`black_box` in Rust wraps the return value, C++ takes no extra step here
because the syscall itself is an unremovable side effect, and the Go build
@@ -288,7 +288,7 @@ already made writable for that uid.
## Errors, three ways
`overheadbench`'s error contract is the same shape this book has used since
-Chapter 5: **argument-parsing failures exit 2** with the usage line on
+the errors-three-ways chapter: **argument-parsing failures exit 2** with the usage line on
stderr, and **bench-execution failures exit 1** without it, kept as two
distinct exit codes so a caller can tell "you asked wrong" from "it tried and
the environment failed." An unknown `--bench` value, a flag missing its
@@ -299,7 +299,7 @@ the first bucket in all three languages: C++'s `parse_args` returns
plain `error` that `main` prints beside the same `usage` constant; Rust's
`parse_args` returns `Result7.1) and the two lab guests
python3 scripts/test-all-examples.py --only 41-capstone-fleet --mode
vm printed PASS PASS PASS (3 passed, 0 failed;
verify.lua reported PASS 31 / FAIL 0 for each of
-cpp/go/rust, exercised entirely on loopback per the Chapter 23 vm-peer
-convention). On the real two-host run, pmon on both
+cpp/go/rust, exercised entirely on loopback per the vm-peer
+convention established elsewhere in the book). On the real two-host run, pmon on both
systems-target and systems-peer reached
pmon: health chatterd=up sysagent=up fwatch=up; a message sent
on target (nick dave, text "hello across hosts")
@@ -837,8 +837,8 @@ difference; and with OTEL_EXPORTER_OTLP_ENDPOINT=http://192.168.124.1:
4318 set on both guests, sysagent_cpu_pct{node="peer"}
reached the host's Prometheus (queried through Grafana's proxy) with value
1.75, confirming the guest→host OTLP path over the libvirt
-gateway. Not exercised: seccomp filtering of any fleet child (Chapter 33
-proved seccomp alongside Landlock; this final project reuses only the
+gateway. Not exercised: seccomp filtering of any fleet child (seccomp was
+proven alongside Landlock earlier in the book; this final project reuses only the
capability-bounding-set-drop and Landlock layers, not seccomp) and a run
where the LGTM stack was unreachable (that path reports SKIP for the
telemetry assertions, not a failing example, per the manifest's
diff --git a/_docs/42-embedded-lua-scripting.md b/_docs/42-embedded-lua-scripting.md
index bddade0..c5c97e8 100644
--- a/_docs/42-embedded-lua-scripting.md
+++ b/_docs/42-embedded-lua-scripting.md
@@ -2,7 +2,7 @@
title: "Embedding Lua as a policy engine: one policy.lua, three host runtimes"
order: 42
part: "Deep Dives"
-description: "lua-policy moves pmon's restart/backoff decision out of the compiled binary and into a single sandboxed policy.lua, embedded three structurally different ways -- C++23 via sol2, Go via gopher-lua (Lua 5.1), Rust via mlua (Lua 5.4) -- and reloaded live on a real SIGHUP with no process restart. Verified on the Fedora 44 host: byte-identical decisions across all three languages (diff-confirmed), PASS 19/FAIL 0 per language, and three distinct sandbox-rejection diagnostics for the same malicious policy."
+description: "lua-policy moves pmon's restart/backoff decision out of the compiled binary and into a single sandboxed policy.lua, embedded three structurally different ways (C++23 via sol2, Go via gopher-lua (Lua 5.1), Rust via mlua (Lua 5.4)) and reloaded live on a real SIGHUP with no process restart. Verified on the Fedora 44 host: byte-identical decisions across all three languages (diff-confirmed), PASS 19/FAIL 0 per language, and three distinct sandbox-rejection diagnostics for the same malicious policy."
duration: "45 minutes"
---
@@ -20,13 +20,13 @@ By the end of this chapter you can:
## Why this matters
-A restart policy baked into compiled C++, Go, or Rust means a tuning change costs a rebuild and a redeploy. Moving that decision into one sandboxed `policy.lua` instead means `pmon` can change its crash-looping threshold or its backoff curve on a live `SIGHUP`, with the same process and the same PID throughout, which is the capability an operator actually wants during an incident.
+A restart policy baked into compiled C++, Go, or Rust means a tuning change costs a rebuild and a redeploy. Moving that decision into one sandboxed `policy.lua` instead means `pmon` can change its crash-looping threshold or its backoff curve on a live `SIGHUP`, with the same process and the same PID throughout, which is the capability an operator wants during an incident.
-The harder question this chapter answers is whether "the same policy" really means the same behavior once it is embedded three structurally different ways. Proving sol2, gopher-lua, and mlua reach byte-identical restart decisions from one unmodified file, across a Lua 5.1/5.4 version gap, is what makes "one policy.lua, three host runtimes" more than a slogan.
+The harder question this chapter answers is whether "the same policy" means the same behavior once it is embedded three structurally different ways. Proving sol2, gopher-lua, and mlua reach byte-identical restart decisions from one unmodified file, across a Lua 5.1/5.4 version gap, is what makes "one policy.lua, three host runtimes" more than a slogan.
-Chapter 12 taught `pmon` to treat a signal as data instead of an
+Earlier in the book, `pmon` learned to treat a signal as data instead of an
interruption: block `SIGCHLD` process-wide, then read it off a `signalfd` on
-the next turn of an ordinary `poll` loop. Chapter 41's final-project fleet leaned
+the next turn of an ordinary `poll` loop. The fleet final project leaned
on the same discipline for `SIGTERM`/`SIGINT`. This chapter reaches for a
different signal (`SIGHUP`, "please re-read your configuration") to
reload something `pmon` has never had before: a restart policy that is not
@@ -45,7 +45,7 @@ decisions from it, live reload included.
{% include excalidraw.html
file="42-host-script-boundary"
- alt="Three stacked host-runtime boxes on the left -- C++23/sol2 against PUC-Rio Lua 5.4, Go/gopher-lua against Lua 5.1, Rust/mlua against a vendored PUC-Rio Lua 5.4 -- each with a bidirectional arrow into a center box labeled policy.lua, one file, marked on_exit(info) going in and decision{action,delay_ms,reason} coming back. The policy.lua box sits inside a shaded sandbox-boundary band with a ghost box reading blocked: os, io, package, debug (never opened); nilled: load/loadfile/dofile/require/collectgarbage/loadstring. A host table box on the right connects to policy.lua with a bidirectional arrow labeled host.log / host.now_ms. A caption note reads Lua 5.1 (gopher-lua) vs Lua 5.4 (sol2, mlua) forces the shared subset -- one policy.lua drives all three."
+ alt="Three stacked host-runtime boxes on the left: C++23/sol2 against PUC-Rio Lua 5.4, Go/gopher-lua against Lua 5.1, Rust/mlua against a vendored PUC-Rio Lua 5.4, each with a bidirectional arrow into a center box labeled policy.lua, one file, marked on_exit(info) going in and decision{action,delay_ms,reason} coming back. The policy.lua box sits inside a shaded sandbox-boundary band with a ghost box reading blocked: os, io, package, debug (never opened); nilled: load/loadfile/dofile/require/collectgarbage/loadstring. A host table box on the right connects to policy.lua with a bidirectional arrow labeled host.log / host.now_ms. A caption note reads Lua 5.1 (gopher-lua) vs Lua 5.4 (sol2, mlua) forces the shared subset: one policy.lua drives all three."
caption="Figure 42.1: the host↔script boundary: three differently-built sandboxed Lua states, one shared policy.lua, and the host table it calls back into" %}
> **Tools used**: `lua` (host, drives `verify.lua` and is the PUC-Rio 5.4
@@ -498,7 +498,7 @@ error and keep serving whatever policy was already loaded; a
## Concurrency lens
Every host consumes the *same* `SIGHUP` through three different primitives:
-the same divergence Chapter 41 mapped for `SIGTERM`, reached here through a
+the same divergence the fleet final project mapped for `SIGTERM`, reached here through a
reload path with one new wrinkle: after the signal lands, **every** binding
discards the whole interpreter and builds a brand-new one.
@@ -574,7 +574,7 @@ void wait_for_sighup(int sigfd) {
.context("reloading policy")?;
```
-C++ reuses ch12/ch13's pattern outright: `SIGHUP` is blocked process-wide
+C++ reuses the pattern established earlier in the book outright: `SIGHUP` is blocked process-wide
with `sigprocmask` up front (in `make_sighup_fd`, not shown), then consumed
as ordinary readability on a `signalfd` inside a `poll` loop; no signal
handler runs at all. Go reaches for the idiomatic equivalent: `signal.Notify`
@@ -661,12 +661,12 @@ Each language's `verify.lua` reported `PASS 19 / FAIL 0`.
{% include excalidraw.html
file="42-sighup-reload-lifecycle"
- alt="A horizontal sequence of six boxes inside a band labeled one pmon process -- PID never changes: load v1 (base=500ms max_restarts=8), phase-1 decisions (web: stop; worker: restart 500ms to 1000ms), awaiting SIGHUP (a dashed ghost box), reload (fresh Lua state rebuilt, on_load logs v1.1.0), phase-2 decisions (api: restart 100ms; worker: escalate after 2 restarts), and exit 0. Above the awaiting-SIGHUP box, a ghost box shows the operator action -- edit the policy file, then kill -HUP $(pgrep app) -- with an arrow into the flow. Below it, three sub-boxes show the three per-language consumption mechanisms: C++ signalfd + poll, Go signal.Notify(ch), Rust signal_hook flag (AtomicBool + 20ms poll). A bottom note reads: the process is NOT restarted -- only a fresh, sandboxed Lua state is built in place and the decision logic changes underneath it."
+ alt="A horizontal sequence of six boxes inside a band labeled one pmon process: PID never changes: load v1 (base=500ms max_restarts=8), phase-1 decisions (web: stop; worker: restart 500ms to 1000ms), awaiting SIGHUP (a dashed ghost box), reload (fresh Lua state rebuilt, on_load logs v1.1.0), phase-2 decisions (api: restart 100ms; worker: escalate after 2 restarts), and exit 0. Above the awaiting-SIGHUP box, a ghost box shows the operator action: edit the policy file, then kill -HUP $(pgrep app), with an arrow into the flow. Below it, three sub-boxes show the three per-language consumption mechanisms: C++ signalfd + poll, Go signal.Notify(ch), Rust signal_hook flag (AtomicBool + 20ms poll). A bottom note reads: the process is not restarted; only a fresh, sandboxed Lua state is built in place and the decision logic changes underneath it."
caption="Figure 42.2: the SIGHUP reload lifecycle: v1's decisions, a real SIGHUP consumed three different ways, a fresh Lua state built in place, and v1.1.0's tighter decisions, same process throughout" %}
## Cross-check: one script, three interpreters, byte-identical decisions
-The claim this chapter makes is falsifiable and was actually falsified
+The claim this chapter makes is falsifiable and was tested
against: `policy.lua`, unmodified, drives sol2/PUC-Lua 5.4, gopher-lua's
pure-Go 5.1 VM, and mlua/PUC-Lua 5.4 to the *exact same sequence of restart
decisions*, including the exact backoff numbers and the exact `reason`
@@ -708,8 +708,8 @@ not three green exit codes agreeing by coincidence.
`pmon`'s PID and child-tracking state survive untouched; only the Lua
state is thrown away and rebuilt from `policy-v2.lua`, so no closure or
global from the old policy leaks into the new one.
-- **Three signals, one outcome, three primitives.** C++ reuses ch12's
- `sigprocmask` + `signalfd` + `poll`; Go hands the signal to the runtime's
+- **Three signals, one outcome, three primitives.** C++ reuses the
+ `sigprocmask` + `signalfd` + `poll` pattern established earlier in the book; Go hands the signal to the runtime's
own machinery via `signal.Notify` and blocks on a channel; Rust's
`signal-hook` flips an `AtomicBool` from a minimal `sigaction` handler a
separate thread polls every 20 ms, armed before the "awaiting" line prints
diff --git a/_docs/43-rust-macros-for-systems.md b/_docs/43-rust-macros-for-systems.md
index eb8d580..dcb1478 100644
--- a/_docs/43-rust-macros-for-systems.md
+++ b/_docs/43-rust-macros-for-systems.md
@@ -2,7 +2,7 @@
title: "Rust macros for systems code: proc-macros, syn/quote, and what miri proves about unsafe"
order: 43
part: "Deep Dives"
-description: "sysmacros is chapter 43's proc-macro crate, powering two Rust-only techniques in the app binary -- #[derive(SysError)], a miniature thiserror built with syn 2.0's AST and quote's token emission, and #[instrument], a compile-time tracing attribute that times a function with Instant -- plus a sound unsafe unaligned read that cargo +nightly miri validates clean and, under the --oob-read path, catches as undefined behaviour. The book's first single-language deep dive, verified on the Fedora 44 host with verify.lua PASS 8/FAIL 0, the exact six-line stable stdout, and miri's own out-of-bounds diagnostic reproduced byte for byte."
+description: "sysmacros is this chapter's proc-macro crate, powering two Rust-only techniques in the app binary (#[derive(SysError)], a miniature thiserror built with syn 2.0's AST and quote's token emission, and #[instrument], a compile-time tracing attribute that times a function with Instant) plus a sound unsafe unaligned read that cargo +nightly miri validates clean and, under the --oob-read path, catches as undefined behaviour. The book's first single-language deep dive, verified on the Fedora 44 host with verify.lua PASS 8/FAIL 0, the exact six-line stable stdout, and miri's own out-of-bounds diagnostic reproduced byte for byte."
duration: "35 minutes"
---
@@ -31,8 +31,9 @@ equally. This one breaks that pattern by design. Proc-macros are a Rust
compiler feature with no C++ or Go equivalent worth building a codetab
around, and `miri`, the interpreter that catches undefined behaviour a
normal build would run right past, only exists because Rust's `unsafe`
-carries obligations the type system stops checking. Chapter 12 gave
-`pmon` a `PmonError`-shaped hierarchy by hand, written out field by field;
+carries obligations the type system stops checking. Signal-safe error
+handling earlier gave `pmon` a `PmonError`-shaped hierarchy by hand, written
+out field by field;
this chapter asks how much of that boilerplate a macro can write for
you, and how you prove the `unsafe` block sitting a few lines below it is
still doing exactly what its comment claims.
@@ -50,7 +51,7 @@ skips the check, which is exactly the distinction `miri` exists to draw.
{% include excalidraw.html
file="43-proc-macro-pipeline"
- alt="Two horizontal bands. Top band: the app crate, main.rs's annotated source (#[derive(SysError)] enum PmonError, #[instrument] fn hash_chunk) feeding rustc, which packages a TokenStream and hands it off; on the right, rustc splices tokens back into the AST, typechecks, and compiles the result into the app binary (generated Display + Error impls, the instrumented hash_chunk). Bottom band: sysmacros, a separate proc-macro crate compiled FIRST, containing two nodes -- syn::parse_macro_input! turning the TokenStream into a real DeriveInput/ItemFn AST, and quote! emitting a new TokenStream2 (the Display impl or the wrapped function). Arrows cross the band boundary labeled TokenStream (macro input) going down and TokenStream (macro output) coming back up. A note at the bottom reads: sysmacros never runs at app runtime -- it is pure tokens-in/tokens-out, executed only while rustc is compiling app."
+ alt="Two horizontal bands. Top band: the app crate, main.rs's annotated source (#[derive(SysError)] enum PmonError, #[instrument] fn hash_chunk) feeding rustc, which packages a TokenStream and hands it off; on the right, rustc splices tokens back into the AST, typechecks, and compiles the result into the app binary (generated Display + Error impls, the instrumented hash_chunk). Bottom band: sysmacros, a separate proc-macro crate compiled first, containing two nodes: syn::parse_macro_input! turning the TokenStream into a real DeriveInput/ItemFn AST, and quote! emitting a new TokenStream2 (the Display impl or the wrapped function). Arrows cross the band boundary labeled TokenStream (macro input) going down and TokenStream (macro output) coming back up. A note at the bottom reads: sysmacros never runs at app runtime; it is pure tokens-in/tokens-out, executed only while rustc is compiling app."
caption="Figure 43.1: the compile-time-only pipeline: annotated source becomes a TokenStream, the separately-compiled sysmacros crate parses it with syn and re-emits new tokens with quote, and rustc splices the result into app" %}
> **Tools used**: `cargo` (host, build/run the pinned stable toolchain,
@@ -75,7 +76,7 @@ execute the finished `app` binary; every line of it runs once, at build
time, and produces tokens that rustc splices into the program in its
place.
-The macro `PmonError` actually uses is the plainer of the two ideas:
+The macro `PmonError` uses is the plainer of the two ideas:
each variant just needs a message:
```rust
@@ -126,11 +127,11 @@ fn read_record(buf: &[u8], oob: bool) -> Record {
An `unsafe` block is a promise to the compiler: "I checked something the
type system can't, and this operation is valid anyway." `read_unaligned`
needs three things to be true: the pointer is readable for
-`size_of::4 MB goal once a real percent was
restored; a fresh GODEBUG=schedtrace=1000,scheddetail=1
capture showed the startup threads=5 summary line and
per-P/M/G detail including the Go 1.25+
-GOMAXPROCS updater (idle) goroutine. The deliberate contrast
+GOMAXPROCS updater (idle) goroutine. This contrast
run, GODEBUG=asyncpreemptoff=1 ./go/bin/app sched, reproduced
ticker_incr=0 ticker_incr>0 FAIL and sched: FAIL
exactly, with the other three sched checks unaffected. Green
@@ -620,5 +618,5 @@ Tea GC's default-on status in Go 1.26 and the removal of the dedicated
notes and runtime source, not measured directly; Green Tea leaves no
signal in gctrace output by design. Not exercised:
examples/manifest.yaml marks this mode: local:
-no VM or LGTM path applies; the asyncpreemptoff=1 run is a
-deliberate FAIL demo, never asserted green in verify.lua.
asyncpreemptoff=1 run is
+designed to fail, never asserted green in verify.lua.
diff --git a/_docs/45-linux-analysis-suites.md b/_docs/45-linux-analysis-suites.md
index 31f6e13..a91bb41 100644
--- a/_docs/45-linux-analysis-suites.md
+++ b/_docs/45-linux-analysis-suites.md
@@ -2,7 +2,7 @@
title: "Linux analysis suites: Cockpit, SystemTap, and PCP watch a whole fleet"
order: 45
part: "Appendices: Tooling"
-description: "linux-analysis-suites builds a single Fedora-based, multi-stage Containerfile (lsp45-pcp, 265 MB) shipping Performance Co-Pilot's pmcd with two live PMDAs -- openmetrics and podman -- run rootless with podman, alongside Cockpit's zero-scripting web console and SystemTap's kernel-module tracing on the systems-target VM: verified PASS 10/0 with a real pminfo value 42, live podman-socket container visibility, a working SystemTap kernel-module smoke test, and Cockpit's socket answering HTTP 200."
+description: "linux-analysis-suites builds a single Fedora-based, multi-stage Containerfile (lsp45-pcp, 265 MB) shipping Performance Co-Pilot's pmcd with two live PMDAs (openmetrics and podman) run rootless with podman, alongside Cockpit's zero-scripting web console and SystemTap's kernel-module tracing on the systems-target VM: verified PASS 10/0 with a real pminfo value 42, live podman-socket container visibility, a working SystemTap kernel-module smoke test, and Cockpit's socket answering HTTP 200."
duration: "35 minutes"
---
@@ -15,15 +15,15 @@ appendices, and it pivots from single-process tools to three fleet-wide
**suites**, each built on a different philosophy for the same underlying
question: what is this system doing right now. Cockpit answers it with a
web console you never script. SystemTap answers it by compiling a kernel
-module per probe, bcc's harder-to-build sibling from Chapter 30. And PCP
-(Performance Co-Pilot) answers it by pulling metrics out of things this book
-already built: a `podman.sock` (Chapter 34) and an OpenMetrics document
-(Chapter 38).
+module per probe, the harder-to-build sibling to bcc-tools and bpftrace. And
+PCP (Performance Co-Pilot) answers it by pulling metrics out of things this
+book already built: a `podman.sock` from its container fleet work and an
+OpenMetrics document from its OpenTelemetry pipeline.
{% include excalidraw.html
file="45-analysis-suite-landscape"
- alt="Three side-by-side boxes under a banner reading three philosophies for fleet-wide observation. Left: a browser icon into a Cockpit box, labeled zero-scripting web console, cockpit-ws multiplexing sessions over a socket answering on port 9090. Middle: a stap CLI box compiling into a kernel-module icon, labeled SystemTap -- bcc's harder-to-build kernel-module-compiling sibling from Chapter 30, needing matching kernel-devel. Right: a pmcd hub box with two labeled PMDA arms -- pmda-openmetrics reading a file:// document and pmda-podman reading a podman.sock -- both arrows curving back to two smaller ghost boxes captioned Chapter 34 containers and Chapter 38 OpenMetrics, showing PCP pulls metrics out of things this book already built. Caption: three ways to observe a fleet -- click, script, or scrape."
- caption="Figure 45.1: three philosophies for fleet-wide observation: Cockpit's zero-scripting console, SystemTap's kernel-module scripting (bcc's harder-to-build sibling from Chapter 30), and PCP pulling metrics out of two things this book already built" %}
+ alt="Three side-by-side boxes under a banner reading three philosophies for fleet-wide observation. Left: a browser icon into a Cockpit box, labeled zero-scripting web console, cockpit-ws multiplexing sessions over a socket answering on port 9090. Middle: a stap CLI box compiling into a kernel-module icon, labeled SystemTap: bcc's harder-to-build kernel-module-compiling sibling, needing matching kernel-devel. Right: a pmcd hub box with two labeled PMDA arms: pmda-openmetrics reading a file:// document and pmda-podman reading a podman.sock, both arrows curving back to two smaller ghost boxes captioned containers and OpenMetrics, showing PCP pulls metrics out of things this book already built. Caption: three ways to observe a fleet: click, script, or scrape."
+ caption="Figure 45.1: three philosophies for fleet-wide observation: Cockpit's zero-scripting console, SystemTap's kernel-module scripting (bcc's harder-to-build sibling), and PCP pulling metrics out of two things this book already built" %}
> **Tools used**: `podman` (host, build/run/exec, in
> `scripts/check-host.sh`); `pmcd`/`pminfo`/`pmrep`/`pcp` (a third location:
@@ -43,8 +43,8 @@ running the check once rather than three times for no behavioral gain.
Whichever of the three is given, the image built and the container run are
byte-identical. Code excerpts below are therefore plain fenced blocks, never
the tri-language `codetabs` include this book uses everywhere else; there
-is only one path through this example, the same handling Chapter 44 used for
-its single-language runtime deep dive.
+is only one path through this example, the same handling the Go runtime
+chapter used for its single-language deep dive.
## Cockpit: a web console with no scripting required
@@ -60,9 +60,9 @@ answering before this chapter's demo ever runs.
## SystemTap: bcc's kernel-module-compiling sibling
-Chapter 30 put SystemTap side by side with bcc-tools and bpftrace and found
-its kernel-module backend blocked by a real `kernel-devel`/running-kernel
-mismatch, a friction point flagged, not resolved, in that chapter. This
+Putting SystemTap side by side with bcc-tools and bpftrace surfaced its
+kernel-module backend blocked by a real `kernel-devel`/running-kernel
+mismatch, a friction point flagged but not resolved at the time. This
appendix is where it gets resolved: once `kernel-devel-$(uname -r)` matches
the running kernel, `stap` compiles a real, loadable kernel module per probe
script, runs it, and unloads it when the script exits, the same
@@ -83,9 +83,10 @@ namespace, queryable with `pminfo`, `pmrep`, or the summary command `pcp`
itself. This chapter's image layers exactly two PMDAs, and both pull their
numbers from things this book already built rather than from anything new:
`pmda-openmetrics` bridges an OpenMetrics/Prometheus-style document, the
-same format Chapter 38 wired a real exporter around, into PCP's namespace,
-and `pmda-podman` reads container and pod state over a `podman.sock`, the
-same rootless socket Chapter 34 built a container fleet around. PCP doesn't
+same format this book's OpenTelemetry pipeline wired a real exporter around,
+into PCP's namespace, and `pmda-podman` reads container and pod state over a
+`podman.sock`, the same rootless socket this book's container fleet work
+built around. PCP doesn't
introduce a new thing to observe; it gives a uniform namespace to metrics
this book's own examples already produce.
@@ -107,7 +108,7 @@ scrape of a real file, every run, with zero network dependency.
### Why Fedora, not UBI
-Every other container example in this book, Chapter 34's, builds on a
+Every other container example in this book builds on a
UBI 10 base, per this project's own convention. This one doesn't, and the
reason is direct: checked directly against a real `dnf
install`, `pcp` and its PMDA packages are **not resolvable on UBI 9 or UBI
@@ -118,18 +119,18 @@ exists, but it needs Red Hat portal authentication, and there is no
conventions, are assumed **not** to have a RHEL subscription. Fedora's own
repositories carry `pcp 7.1.5` and every PMDA package this chapter needs,
fully unauthenticated, so `registry.fedoraproject.org/fedora:44` is the
-base that actually builds for everyone, not just readers with a Red Hat
+base that builds for everyone, not just readers with a Red Hat
account.
One PMDA is absent from this image: `pmda-bcc`, PCP's own
-bridge into the eBPF world Chapter 30 covered directly. Activating it needs
-a working BCC/eBPF stack underneath: raw `BPF` syscalls, `/sys/kernel/
+bridge into the eBPF world bcc-tools and bpftrace cover directly. Activating
+it needs a working BCC/eBPF stack underneath: raw `BPF` syscalls, `/sys/kernel/
debug`, a kernel-devel package matching the exact running kernel, which in
turn needs a **super-privileged container**, the opposite of the rootless
posture every other example in this book targets. It's covered here only as
a name: PCP's privileged sibling PMDA, demonstrated properly on the
-`systems-target` VM where Chapter 30's real eBPF tooling already runs, not
-shipped inside this rootless image.
+`systems-target` VM where real bcc-tools and bpftrace tooling already runs,
+not shipped inside this rootless image.
## How the code works
@@ -211,13 +212,13 @@ ENTRYPOINT ["/usr/libexec/pcp/bin/pmcd", "-f"]
```
`FROM smoke AS runtime` both reuses `smoke`'s already-activated PMDA state
-(no third `./Install` re-run needed) and forces `podman build` to actually
-build `smoke` first: the gate stage only runs because something real
-depends on it existing.
+(no third `./Install` re-run needed) and forces `podman build` to build
+`smoke` first: the gate stage only runs because something real depends on
+it existing.
{% include excalidraw.html
file="45-pcp-build-gate"
- alt="A vertical three-stage pipeline. Stage one, builder, installs pcp packages, copies lsp45.prom, starts pmcd, and runs two ./Install calls against that live pmcd. An arrow labeled FROM builder AS smoke descends into stage two, smoke, which restarts pmcd and asserts both PMDAs are listed plus a real value 42 and a real kernel.all.load sample, gated by grep -q calls that can fail the whole podman build. A ghost, disconnected box beside stage two reads FROM builder AS runtime -- the skipped path, dashed and crossed out, with a note that podman silently never builds a stage nothing chains onto. The real arrow, solid and amber, is labeled FROM smoke AS runtime and descends into stage three, runtime, which reuses the smoke stage's already-activated PMNS/pmcd.conf and carries only EXPOSE 44321 and the pmcd entrypoint. Caption: chaining onto smoke, not builder, is what forces the gate stage to actually build."
+ alt="A vertical three-stage pipeline. Stage one, builder, installs pcp packages, copies lsp45.prom, starts pmcd, and runs two ./Install calls against that live pmcd. An arrow labeled FROM builder AS smoke descends into stage two, smoke, which restarts pmcd and asserts both PMDAs are listed plus a real value 42 and a real kernel.all.load sample, gated by grep -q calls that can fail the whole podman build. A ghost, disconnected box beside stage two reads FROM builder AS runtime: the skipped path, dashed and crossed out, with a note that podman silently never builds a stage nothing chains onto. The real arrow, solid and amber, is labeled FROM smoke AS runtime and descends into stage three, runtime, which reuses the smoke stage's already-activated PMNS/pmcd.conf and carries only EXPOSE 44321 and the pmcd entrypoint. Caption: chaining onto smoke, not builder, is what forces the gate stage to build."
caption="Figure 45.2: the PCP image's build gate: chaining runtime onto smoke forces the gate stage to build, and a PMDA that fails to register fails the build, not just the demo" %}
## Errors, three ways
@@ -256,16 +257,17 @@ environment-dependent optional feature look like a broken build.
Each suite here answers "how many things are running at once" a different
way. `pmcd` is one reactor process fronting **N independent PMDA
processes**, each talking to `pmcd` over its own pipe, the same
-process-isolation discipline Chapter 11 built `pmon` around, applied here
-to metrics collection instead of service supervision: a PMDA that hangs or
-crashes takes down only its own slice of the namespace, never `pmcd`
-itself or any sibling agent. SystemTap's `<<<` operator is the sharpest
-concurrency idea in this chapter: it's a per-CPU-safe **statistical
-aggregation**: every CPU accumulates into its own private bucket with no
-shared-memory contention at all, and only the `end` probe's `@count()`
-call folds those per-CPU buckets together into the single number a script
-prints. That's the same lock-free instinct Chapter 26 built atomics and
-CAS loops around, reached here through a completely different mechanism:
+process-isolation discipline this book's process-lifecycle work built
+`pmon` around, applied here to metrics collection instead of service
+supervision: a PMDA that hangs or crashes takes down only its own slice of
+the namespace, never `pmcd` itself or any sibling agent. SystemTap's `<<<`
+operator is the sharpest concurrency idea in this chapter: it's a
+per-CPU-safe **statistical aggregation**: every CPU accumulates into its own
+private bucket with no shared-memory contention at all, and only the `end`
+probe's `@count()` call folds those per-CPU buckets together into the single
+number a script prints. That's the same lock-free instinct behind this
+book's atomics and CAS loops, reached here through a completely different
+mechanism:
avoid the race by never sharing the counter in the first place, rather
than serializing access to one. Cockpit's concurrency story is the
simplest of the three: one `cockpit-ws` process multiplexes every browser
@@ -333,7 +335,7 @@ podman.container.running
(`root pmcd proc pmproxy xfs linux mmv kvm jbd2`), all eleven active. The
openmetrics PMDA hands back the exact `42` `config/lsp45.prom` ships, every
run. The podman PMDA, once the container's own `podman.sock` is mounted
-into it, sees `lsp45-pcp-demo` (the very container running the query),
+into it, sees `lsp45-pcp-demo`, the same container running the query,
reporting itself as `podman.container.running value 1`. `pmrep`'s three
load-average columns come from `kernel.all.load`, a core PCP metric with
nothing to do with either PMDA this chapter added, confirming that `pmcd`'s
@@ -369,7 +371,7 @@ stap-module-ok
That one line is a real kernel module compiled against
`kernel-devel-$(uname -r)` (matched to the running `6.19.10-300.fc44`
-kernel), loaded, run once, and unloaded: the payoff Chapter 30 flagged as
+kernel), loaded, run once, and unloaded: the payoff flagged earlier as
blocked by a kernel-devel mismatch, resolved here once the matching
package is installed. A slightly larger script aggregates real syscall
activity from a controlled workload:
@@ -399,7 +401,8 @@ PASS 10 / FAIL 0
```
Image size on this host: **265 MB** (`podman images lsp45-pcp`), much
-larger than Chapter 34's few-tens-of-MB runtime images, because PCP's own
+larger than this book's other few-tens-of-MB container runtime images,
+because PCP's own
dependency chain (Python, `pcp-libs`, `pcp-system-tools`) is the size
driver, and there's no further multi-stage trim available when `runtime`
needs the exact same userspace `smoke` just proved works.
@@ -441,11 +444,11 @@ shipped, and started cold. Neither check trusts the other's result.
- **Three suites, three philosophies for the same question.** Cockpit
answers "what is this system doing" with a browser and zero scripting;
- SystemTap answers it by compiling a real kernel module per probe, bcc's
- harder-to-build sibling from Chapter 30; PCP answers it by giving a
- uniform namespace to metrics pulled out of things this book already
- built: a `podman.sock` (Chapter 34) and an OpenMetrics document
- (Chapter 38).
+ SystemTap answers it by compiling a real kernel module per probe, the
+ harder-to-build sibling to bcc-tools and bpftrace; PCP answers it by
+ giving a uniform namespace to metrics pulled out of things this book
+ already built: a `podman.sock` from its container fleet work and an
+ OpenMetrics document from its OpenTelemetry pipeline.
- **Fedora, not UBI, because the packages aren't there.** `pcp` and
its PMDAs are unresolvable on UBI 9/10 without a RHEL entitlement this
book's readers are assumed not to have;
@@ -453,7 +456,7 @@ shipped, and started cold. Neither check trusts the other's result.
- **A disconnected build stage is a build stage that never runs.**
`FROM builder AS runtime` compiles cleanly and silently skips `smoke`
entirely; `FROM smoke AS runtime` is what forces `podman build` to
- actually execute the gate.
+ execute the gate.
- **A PMDA's own `Install` script can silently revert itself.** With no
systemd in a build layer, `systemctl restart pmcd.service` fails and
`Install`'s failure path un-does its own `pmcd.conf` edit: starting
@@ -467,7 +470,7 @@ shipped, and started cold. Neither check trusts the other's result.
- **`pmda-bcc` is excluded by design, not by oversight.** Real eBPF
attachment needs a super-privileged container, the opposite of every
rootless posture in this book, so it's named here as PCP's privileged
- sibling PMDA and left to Chapter 30's real eBPF toolkit on
+ sibling PMDA and left to the real bcc-tools/bpftrace toolkit on
`systems-target`.
Part 13's appendices continue from here, tooling this book leaned on
diff --git a/_docs/46-cpp-toolbox.md b/_docs/46-cpp-toolbox.md
index 5d63880..0596c0e 100644
--- a/_docs/46-cpp-toolbox.md
+++ b/_docs/46-cpp-toolbox.md
@@ -2,18 +2,19 @@
title: "The C++ toolbox: CMake presets, Conan lockfiles, and GCC-vs-clang parity"
order: 46
part: "Appendices: Tooling"
-description: "cpp-toolbox is one C++23 binary and the full toolchain wrapped around it -- seven CMakePresets.json configurations sharing one _base, a committed Conan 2 lockfile for an isolated fmt sub-target, and a GCC-vs-clang parity gate on toolbox report -- read by clang-tidy, clang-format, and a custom gdb Python pretty-printer, with ccache and mold demonstrated but absent on the reference host. Verified on the Fedora 44 host: verify.lua PASS 20/FAIL 0, GCC and clang release builds byte-identical (digest=0x481984990deee5ff, diff empty), UBSan catching a seeded signed-integer overflow at toolbox.cpp:43, and gdb rendering Digest(0x481984990deee5ff) through toolbox-printers.py."
+description: "cpp-toolbox is one C++23 binary and the full toolchain wrapped around it (seven CMakePresets.json configurations sharing one _base, a committed Conan 2 lockfile for an isolated fmt sub-target, and a GCC-vs-clang parity gate on toolbox report) read by clang-tidy, clang-format, and a custom gdb Python pretty-printer, with ccache and mold demonstrated but absent on the reference host. Verified on the Fedora 44 host: verify.lua PASS 20/FAIL 0, GCC and clang release builds byte-identical (digest=0x481984990deee5ff, diff empty), UBSan catching a seeded signed-integer overflow at toolbox.cpp:43, and gdb rendering Digest(0x481984990deee5ff) through toolbox-printers.py."
duration: "50 minutes"
---
-Chapter 45 opened this part with three suites that watch a whole fleet from
-outside (Cockpit's console, SystemTap's kernel modules, PCP's pull-based
-metrics), and none of them cared what language produced the process they
-were watching. This chapter turns all the way around: one language, one
-small program, and every tool that touches it before it ever runs. Chapter
-31 already gave C++ one paragraph of toolbelt each (`perf`, `clang-tidy`,
-GCC's `-fanalyzer`) on the way to covering Go and Rust too. This chapter
-reopens exactly that toolchain and does not move on. `toolbox` is a single
+The linux-analysis-suites chapter opened this part with three suites that
+watch a whole fleet from outside (Cockpit's console, SystemTap's kernel
+modules, PCP's pull-based metrics), and none of them cared what language
+produced the process they were watching. This chapter turns all the way
+around: one language, one small program, and every tool that touches it
+before it ever runs. The per-language toolbelts chapter already gave C++
+one paragraph of toolbelt each (`perf`, `clang-tidy`, GCC's `-fanalyzer`) on
+the way to covering Go and Rust too. This chapter reopens exactly that
+toolchain and does not move on. `toolbox` is a single
C++23 binary, engineered so that every tool pointed at it (two compilers,
a build-system generator, a package manager, a static analyzer, a
formatter, a debugger that needs help understanding a custom type) has
@@ -31,7 +32,7 @@ whether they agree.
{% include excalidraw.html
file="46-cmake-preset-graph"
- alt="A tree diagram. At the top, a hidden _base configure preset (generator Ninja, binaryDir sourceDir/build/presetName) fans out to six visible presets: release (RelWithDebInfo, GCC), debug (Debug, GCC), asan (Debug + -fsanitize=address,undefined), and ubsan (Debug + -fsanitize=undefined -fno-sanitize-recover=undefined) sit in one column; release-clang and debug-clang sit beside them, each drawn inheriting from release/debug respectively with an arrow labeled inherits plus a CMAKE_CXX_COMPILER=clang++ override. Each of the six feeds its own build/go.mod
pins go 1.26 and toolchain go1.26.5;
toolbox report printed
toolbox report: payload_len=16 digest=0x481984990deee5ff,
-byte-identical to Chapter 46's C++ digest; go generate ./...
+byte-identical to the C++ toolbox chapter's C++ digest; go generate ./...
regenerated tools_generated.go byte-for-byte against the
committed copy (copy-aside diff empty) and toolbox tools
printed the generated table's count=10 plus its sorted
@@ -970,7 +970,7 @@ class="status status--unverified">unverified: gofumpt,
dlv, and benchstat are all absent from this
reference host (confirmed by command -v for each), so gates
F, G, H, I, and K each printed an informational SKIP: rather
-than actually running, and their sections above show committed configs
+than running, and their sections above show committed configs
(.golangci.yml, .dlvinit) and designed-for
findings, the exact tokens ((unused), U1000,
SA4006, the decimal 5195329438047200767,
diff --git a/_docs/48-rust-toolbox.md b/_docs/48-rust-toolbox.md
index e841d75..1966e4e 100644
--- a/_docs/48-rust-toolbox.md
+++ b/_docs/48-rust-toolbox.md
@@ -2,19 +2,20 @@
title: "The Rust toolbox: rust-toolchain.toml pins, rustfmt and clippy as toolchain components, and the cargo-test-to-nextest workflow"
order: 48
part: "Appendices: Tooling"
-description: "rust-toolbox is one zero-dependency crate and the toolchain wrapped around it -- rust-toolchain.toml pinning channel and components so rustup resolves the same rustc, rustfmt, and clippy for everyone, an isolated misformatted fixture and a feature-gated clippy bait that make the format and lint gates assert real findings, and cargo test asserting the digest literal -- with committed deny.toml and clippy.toml for the cargo-nextest/cargo-deny/cargo-llvm-cov/cargo-audit tier shown as reference but absent on the host. Verified on the Fedora 44 host: verify.lua PASS 20/FAIL 0 fully offline, toolbox report digest=0x481984990deee5ff (byte-identical to ch46's C++ and ch47's Go digest), cargo fmt --check clean while rustfmt flags the fixture, cargo clippy -D warnings clean by default and naming needless_return under --features lintbait, and cargo test passing the digest unit test."
+description: "rust-toolbox is one zero-dependency crate and the toolchain wrapped around it (rust-toolchain.toml pinning channel and components so rustup resolves the same rustc, rustfmt, and clippy for everyone, an isolated misformatted fixture and a feature-gated clippy bait that make the format and lint gates assert real findings, and cargo test asserting the digest literal) with committed deny.toml and clippy.toml for the cargo-nextest/cargo-deny/cargo-llvm-cov/cargo-audit tier shown as reference but absent on the host. Verified on the Fedora 44 host: verify.lua PASS 20/FAIL 0 fully offline, toolbox report digest=0x481984990deee5ff (byte-identical to the C++ toolbox's and the Go toolbox's digest), cargo fmt --check clean while rustfmt flags the fixture, cargo clippy -D warnings clean by default and naming needless_return under --features lintbait, and cargo test passing the digest unit test."
duration: "50 minutes"
---
-Chapter 46 pointed every C++ tool it could find at one small binary; Chapter
-47 did the same for Go and found the depth had moved: out of the build
-system and into `go.mod`'s own toolchain directives. Rust closes the arc, and
-it moves the line one step further in the same direction. Go pins its
-compiler in the module file; Rust pins its compiler *and its formatter and
-its linter*, in a file `rustup` reads before `cargo` has done anything at
-all. That single fact reorganizes this whole chapter. In Chapter 47,
-`gofumpt`, `golangci-lint`, and `staticcheck` had to be gated-if-present,
-because they are separate downloads that a given host may simply not have. In
+The C++ toolbox chapter pointed every C++ tool it could find at one small
+binary; the Go toolbox chapter did the same for Go and found the depth had
+moved: out of the build system and into `go.mod`'s own toolchain directives.
+Rust closes the arc, and it moves the line one step further in the same
+direction. Go pins its compiler in the module file; Rust pins its compiler
+*and its formatter and its linter*, in a file `rustup` reads before `cargo`
+has done anything at all. That single fact reorganizes this whole chapter. In
+the Go toolbox chapter, `gofumpt`, `golangci-lint`, and `staticcheck` had to
+be gated-if-present, because they are separate downloads that a given host
+may not have. In
Rust, `rustfmt` and `clippy` are *components of the pinned channel*: if the
pin resolves, they are there, at exactly the version the pin names. So the
formatter and the linter graduate out of the "shown as reference" tier and
@@ -45,8 +46,8 @@ never just a clean exit.
> `examples/48-rust-toolbox` is `mode: local` in `examples/manifest.yaml`.
Because the Rust toolchain is itself the subject, `examples/48-rust-toolbox/`
-ships `langs: [rust]` only, the same way Chapter 43's macro chapter, Chapter
-46's C++ chapter, and Chapter 47's Go chapter each stayed inside one
+ships `langs: [rust]` only, the same way the Rust-macros chapter, the C++
+toolbox chapter, and the Go toolbox chapter each stayed inside one
language: a C++ or Go build has nothing to say about a
`rust-toolchain.toml` component list or a `clippy.toml` MSRV. Every code
block below is a plain fenced `rust`/`toml`/`console` block; there is no
@@ -106,8 +107,9 @@ $ cd /tmp && rustup show active-toolchain
stable-x86_64-unknown-linux-gnu (default)
```
-That is the property Chapter 47 got from `GOTOOLCHAIN` and Chapter 46 had to
-build by hand out of seven `CMakePresets.json` configurations: the version
+That is the property the Go toolbox chapter got from `GOTOOLCHAIN` and the
+C++ toolbox chapter had to build by hand out of seven `CMakePresets.json`
+configurations: the version
decision lives with the source, not with whoever checked it out. What Rust
adds on top is that the *tools* travel with it too. Every binary in the chain
reports the same release:
@@ -136,7 +138,7 @@ as the inclusions. `llvm-tools` (needed by `cargo-llvm-cov`) and `rust-src`
(needed by miri and rust-analyzer) are left out, because `rustup` fetches any
missing component **over the network, on the first `cargo` invocation in this
directory**. Pinning a component a reader does not have turns `cargo build
---offline` into a download: exactly the failure this example exists
+--offline` into a download: the failure this example exists
to avoid. The chapter's own draft hit it: an earlier `components` list
included `llvm-tools`, and the first build in a fresh checkout printed
`info: downloading component llvm-tools` before compiling anything. The file
@@ -204,13 +206,13 @@ mechanism for changing language semantics without breaking old code: a 2024
crate and a 2015 crate can link together in one binary, each compiled under
its own rules. Editions are per-crate, the channel is per-repository, and
they are set in different files: `Cargo.toml` and `rust-toolchain.toml`
-respectively. Both of ch48's gates check both, because getting one right and
+respectively. Both of this chapter's gates check both, because getting one right and
the other wrong is the common failure: a 2024 crate compiled by a toolchain
too old to know that edition fails with a confusing error about an unknown
edition, not an obviously-wrong one.
-The payload and the hash are lifted byte-for-byte from Chapter 46's C++ and
-Chapter 47's Go toolbox, which is the whole point:
+The payload and the hash are lifted byte-for-byte from the C++ and Go
+toolbox chapters, which is the whole point:
```rust
/// ch46/ch47's exact 16-byte payload (`"The quick brown."`), reused
@@ -241,9 +243,9 @@ pub fn fnv1a(data: &[u8]) -> Digest {
`*` on `u64` panics on overflow in a debug build and wraps in a release
build, so the same source would compute the digest in release and abort in
`cargo test`, which builds with debug assertions on. `wrapping_mul` states
-the intent the algorithm actually requires (FNV-1a is defined modulo 2⁶⁴) and
+the intent the algorithm requires (FNV-1a is defined modulo 2⁶⁴) and
makes the two profiles agree. C++ got this behavior by using `uint64_t`,
-where wrapping is simply what the language does; Go got it the same way. Rust
+where wrapping is what the language does; Go got it the same way. Rust
is the only one of the three where you must ask for it, and the only one
where forgetting is caught rather than silent.
@@ -343,7 +345,7 @@ that output: nonzero exit, a `Diff in …fixtures/misformatted.rs` line, and
the specific reformatted signature `pub fn classify(n: i64) -> &'static str
{`, because any one of them alone could pass for the wrong reason. A nonzero
exit could be a missing file; a `Diff in` line could name some other file;
-only the reformatted signature proves `rustfmt` actually parsed this source
+only the reformatted signature proves `rustfmt` parsed this source
and produced this rewrite.
## `clippy`: clean by default, loud behind a feature
@@ -492,7 +494,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
```
The gate asserts the named test line and the `test result: ok. 1 passed; 0
-failed` summary, not the process exit code — a `cargo test` run that compiled
+failed` summary, not the process exit code: a `cargo test` run that compiled
zero tests also exits 0, and would sail past a weaker check.
Watching the gate fail matters too, because a gate nobody has
@@ -523,8 +525,8 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out;
One byte, and `assert_eq!` prints both sides in decimal because that is how
`Digest`'s derived `Debug` formats a `u64`. `5195329438047200767` is
-`0x481984990deee5ff`: the identical decimal Chapter 47's `delve` gate
-printed when it read the Go struct's field out of a live process. Two
+`0x481984990deee5ff`: the identical decimal the Go toolbox chapter's `delve`
+gate printed when it read the Go struct's field out of a live process. Two
languages, two entirely different tools, one number.
This is also why the test gate is separate from the report gate rather than
@@ -593,7 +595,7 @@ not been "fixed."
{% include excalidraw.html
file="48-rust-tool-gates"
- alt="One Rust crate box splits into two gate lanes. The amber hard lane lists the rustup pin at channel 1.97.1 and edition 2024, the FNV-1a digest gate at 0x481984990deee5ff, cargo fmt --check clean on tracked sources and nonzero on the fixture, cargo clippy -D warnings with --features lintbait naming needless_return, and cargo test asserting the digest literal -- all running offline from the pinned toolchain alone because rustfmt and clippy ship as toolchain components. A dashed ghost lane beside it lists cargo-nextest, cargo-deny with its deny.toml, cargo-llvm-cov asserting a filename, and cargo-audit marked advisory-db slash network, plus cargo-watch, cargo-flamegraph, and sccache marked not gated, all shown as reference and not verified on this host."
+ alt="One Rust crate box splits into two gate lanes. The amber hard lane lists the rustup pin at channel 1.97.1 and edition 2024, the FNV-1a digest gate at 0x481984990deee5ff, cargo fmt --check clean on tracked sources and nonzero on the fixture, cargo clippy -D warnings with --features lintbait naming needless_return, and cargo test asserting the digest literal: all running offline from the pinned toolchain alone because rustfmt and clippy ship as toolchain components. A dashed ghost lane beside it lists cargo-nextest, cargo-deny with its deny.toml, cargo-llvm-cov asserting a filename, and cargo-audit marked advisory-db slash network, plus cargo-watch, cargo-flamegraph, and sccache marked not gated, all shown as reference and not verified on this host."
caption="Figure 48.2: one crate, two gate lanes: the pinned-toolchain hard gates (rustup pin, digest, cargo fmt, cargo clippy, cargo test) that need no cargo install, beside the gated-if-present tools (cargo-nextest, cargo-deny, cargo-llvm-cov, cargo-audit) and the shown-not-gated accelerators (cargo-watch, cargo-flamegraph, sccache)" %}
## Errors, three ways
@@ -639,9 +641,9 @@ panic. `toolbox` has no `Result` in it, which is itself the design decision:
the report path has nothing that can fail (a fixed array, an infallible hash,
a `println!`), so introducing a `Result` would be inventing an error case to
have one. The panics that do appear are all in test code, where a panic *is*
-the reporting mechanism. Chapter 5 (the book's own "Errors, three ways")
-and Chapter 43 cover `Result` propagation and `?` at length for code that has
-real fallible I/O; a tooling chapter's
+the reporting mechanism. The errors chapter (the book's own "Errors, three
+ways" section) and the Rust-macros chapter cover `Result` propagation and
+`?` at length for code that has real fallible I/O; a tooling chapter's
contribution is the opposite observation: that a deterministic gate should
have no error paths to test, because every branch it does not have is a
branch that cannot drift.
@@ -661,8 +663,8 @@ reproducibility but not for speed: the graph's *shape* is fixed by
single crate, `rustc` parallelizes differently again, by splitting the crate
into codegen units (16 in the release profile by default, 1 under
`lto = "fat"`), which trades compile time against optimization quality: the
-same trade `mold` and `ccache` addressed from the outside in Chapter 46, made
-from the inside here.
+same trade `mold` and `ccache` addressed from the outside in the C++
+toolbox chapter, made from the inside here.
The second is the test harness. `cargo test` builds one test binary and
libtest runs the `#[test]` functions **in threads, inside that one process**,
@@ -694,8 +696,9 @@ rather than the whole run, which for a book full of `mmap`, signal, and
`rlimit` examples is not a marginal benefit. The cost is process-spawn
overhead per test, which matters for thousands of microsecond-scale unit
tests and does not matter here. Go made the opposite structural choice
-(one process per *package*, tests within a package sharing it), and Chapter
-47's `b.RunParallel` benchmark lived entirely inside that shared process. Same
+(one process per *package*, tests within a package sharing it), and the Go
+toolbox chapter's `b.RunParallel` benchmark lived entirely inside that shared
+process. Same
problem, two different places to put the isolation boundary.
## The gated-if-present tier: `cargo-nextest`, `cargo-deny`, `cargo-llvm-cov`, `cargo-audit`
@@ -783,8 +786,8 @@ Solution: Upgrade to >=0.4.3
```
The durable token there is the `RUSTSEC-YYYY-NNNN` identifier: stable,
-citable, and the thing a policy actually references. Chapter 47 made the same
-call for `govulncheck` and for the same two reasons: network, and no
+citable, and the thing a policy references. The Go toolbox chapter made the
+same call for `govulncheck` and for the same two reasons: network, and no
committed vulnerability.
## Shown, not gated: `cargo-watch`, `cargo-flamegraph`, `sccache`
@@ -799,26 +802,28 @@ A check that never returns is not a check.
`cargo flamegraph --bin toolbox -- report` produces an SVG of where CPU time
went. It needs `perf`, which needs `kernel.perf_event_paranoid` lowered or
-root, and it produces output that differs run to run by construction. Chapter
-31 already ran `cargo-flamegraph` against a Rust binary and taught
-how to read the result, and Chapter 26 hit the `perf_event_paranoid=2`
-privilege wall head-on; the reason it is not gated *here* is the same reason
-Chapter 46 did not gate `ccache`: sampling output is not a deterministic
-assertion.
+root, and it produces output that differs run to run by construction. The
+earlier Per-Language Toolbelts coverage already ran `cargo-flamegraph`
+against a Rust binary and taught how to read the result, and the atomics
+chapter hit the `perf_event_paranoid=2` privilege wall head-on; the reason it
+is not gated *here* is the same reason the C++ toolbox chapter did not gate
+`ccache`: sampling output is not a deterministic assertion.
`sccache` caches compilation artifacts across builds
(`RUSTC_WRAPPER=sccache`). It is a pure accelerator: a correct build and a
correct cached build produce identical output, so there is nothing about
correctness to assert. Its own metric (cache hit rate) depends on what you
-compiled five minutes ago, which is exactly the kind of number this book
-refuses to gate on. `ccache` and `mold` got the same treatment in Chapter 46.
+compiled five minutes ago, which is the kind of number this book
+refuses to gate on. `ccache` and `mold` got the same treatment in the C++
+toolbox chapter.
**miri** deserves a pointer rather than a section: it interprets Rust at the
MIR level and catches undefined behavior in `unsafe` code that no amount of
formatting, linting, or testing will. It is not re-taught here because
-Chapters 29 and 43 already own it, and `toolbox` has no `unsafe` block for it
-to find anything in: which is the correct reason to omit a tool, as opposed
-to omitting it because it is inconvenient.
+earlier coverage of unsafe Rust and the Rust-macros chapter already own it,
+and `toolbox` has no `unsafe` block for it to find anything in: which is the
+correct reason to omit a tool, as opposed to omitting it because it is
+inconvenient.
## Build, run, observe
@@ -894,8 +899,8 @@ $ examples/48-rust-toolbox/rust/target/release/toolbox report
toolbox report: payload_len=16 digest=0x481984990deee5ff
```
-(The C++ line carries an extra `label=[toolbox]` field from Chapter 46's
-argument-echo demo; the `payload_len` and `digest` fields are identical
+(The C++ line carries an extra `label=[toolbox]` field from the C++ toolbox
+chapter's argument-echo demo; the `payload_len` and `digest` fields are identical
across all three.)
FNV-1a is language-independent (a shift, an xor, and a multiply modulo
@@ -912,43 +917,45 @@ none of them did.
## Where this sits next to the book's other tooling chapters
Part 13 covers tooling in four chapters and this is the last of them, so here
-is the arc. Chapter 45 handled the *analysis suites*: tools that observe a
-running system from outside it. Chapters 46, 47, and 48
-each took one language and pointed its whole toolchain at one small binary,
+is the arc. The Linux-analysis-suites chapter handled the *analysis suites*:
+tools that observe a running system from outside it. The C++, Go, and Rust
+toolbox chapters each took one language and pointed its whole toolchain at
+one small binary,
and the interesting result is how differently the three languages distribute
the same responsibilities:
-- **C++ (ch46)** puts almost nothing in the language distribution. CMake,
+- **C++** puts almost nothing in the language distribution. CMake,
Conan, clang-tidy, clang-format, the sanitizers, gdb, ccache, and mold are
eight separate projects with eight separate versions, and `CMakePresets.json`
exists to make "which of these, configured how" an explicit, committed
choice. The chapter's parity gate (GCC and clang producing byte-identical
output) is a check the other two languages have no reason to run.
-- **Go (ch47)** puts the compiler, the formatter, the vetter, the test
+- **Go** puts the compiler, the formatter, the vetter, the test
runner, the profiler, and the generator inside one `go` binary, and pins
that binary's version in `go.mod`. What it leaves outside (gofumpt,
- golangci-lint, staticcheck, delve, benchstat) is precisely what Chapter 47
- had to gate as if-present.
-- **Rust (ch48)** pins a *toolchain* rather than a binary, and that toolchain
+ golangci-lint, staticcheck, delve, benchstat) is precisely what the Go
+ toolbox chapter had to gate as if-present.
+- **Rust** pins a *toolchain* rather than a binary, and that toolchain
includes the formatter and the linter as components. The consequence is
this chapter's structure: the hard, offline core covers formatting and
- linting, which Chapter 47's could not, while the if-present tier is a
- different set of tools entirely (nextest, deny, llvm-cov, audit), every
- one of them a `cargo install` rather than a component.
+ linting, which the Go toolbox chapter's could not, while the if-present
+ tier is a different set of tools entirely (nextest, deny, llvm-cov, audit),
+ every one of them a `cargo install` rather than a component.
-Chapter 31 sits underneath all three: it ran each language's *profiling*
-toolbelt (`perf` for C++, the `pprof` ecosystem for Go, `cargo-flamegraph`
-and clippy for Rust) against one shared workload, which is why these three
-chapters could concentrate on the build-and-check side instead of re-teaching
-how to read a flamegraph.
+The earlier Per-Language Toolbelts coverage sits underneath all three: it ran
+each language's *profiling* toolbelt (`perf` for C++, the `pprof` ecosystem
+for Go, `cargo-flamegraph` and clippy for Rust) against one shared workload,
+which is why these three chapters could concentrate on the build-and-check
+side instead of re-teaching how to read a flamegraph.
The dependency and supply-chain story splits the same way. C++ needed Conan
lockfiles to have a dependency graph worth checking at all; Go's module
system makes the graph free and `govulncheck` the tool that reads it; Rust
gets the graph free too and splits the reading in two: `cargo-deny` for
policy answerable from the lockfile, `cargo-audit` for the RustSec advisory
-database over the network. Chapters 47 and 48 made the identical call about
-that network half: show it, name the durable identifier, never gate it.
+database over the network. The Go and Rust toolbox chapters made the
+identical call about that network half: show it, name the durable
+identifier, never gate it.
## What you learned
@@ -957,7 +964,7 @@ that network half: show it, name the durable identifier, never gate it.
installed. Because `rustfmt` and `clippy` are components, pinning the
channel pins the formatter and the linter at the same commit as `rustc`:
which is why this chapter's format and lint gates are hard and offline
- where Chapter 47's equivalents could not be.
+ where the Go toolbox chapter's equivalents could not be.
- **`rustup show` states the override.** `active because: overridden by
'…/rust-toolchain.toml'` is the mechanism reported by the tool, and it
disappears one directory outside the crate.
@@ -1016,8 +1023,8 @@ sets edition = "2024"; rustc --version resolved
through the pin to rustc 1.97.1 (8bab26f4f 2026-07-14);
cargo tree --offline printed exactly one node;
./demo.sh rust run report printed toolbox report:
-payload_len=16 digest=0x481984990deee5ff, byte-identical to Chapter
-46's C++ and Chapter 47's Go digest; cargo fmt --check exited 0
+payload_len=16 digest=0x481984990deee5ff, byte-identical to the C++
+and Go toolbox chapters' digest; cargo fmt --check exited 0
with no output while rustfmt --edition 2024 --check
fixtures/misformatted.rs exited 1 with the diff quoted above;
cargo clippy --offline --all-targets -- -D warnings exited 0
@@ -1045,6 +1052,7 @@ design and no vulnerable dependency will be committed to it.
this host but not gated (never terminates / needs
perf privileges and produces non-deterministic output), and
sccache's cache-hit rate is not a correctness property; miri is
-cross-referenced to Chapters 29 and 43 and has no unsafe block
-here to examine. examples/manifest.yaml marks this example
+cross-referenced to the earlier unsafe-Rust coverage and the Rust-macros
+chapter and has no unsafe block here to examine.
+examples/manifest.yaml marks this example
mode: local: no VM or LGTM path applies.
diff --git a/_docs/49-concurrency-vs-parallelism.md b/_docs/49-concurrency-vs-parallelism.md
index 8ad6556..7d100f2 100644
--- a/_docs/49-concurrency-vs-parallelism.md
+++ b/_docs/49-concurrency-vs-parallelism.md
@@ -2,7 +2,7 @@
title: "Concurrency vs parallelism: one workload, four execution models, and observables that are counts rather than clocks"
order: 49
part: "Compendium: C++ Concurrency"
-description: "conc runs one identical workload under four execution models -- sequential, concurrent (pinned to one CPU by sched_setaffinity), parallel (one thread per CPU), and both (2x oversubscribed) -- and reports structure rather than time: cpus_allowed from sched_getaffinity as a permission, distinct_cpus unioned from sched_getcpu as an outcome, a max_inflight high-water mark, and interleaves counted from gaps in a monotonic ticket sequence. All four converge on digest=0x481984990deee5ff, the same value ch46's C++, ch47's Go, and ch48's Rust toolbox produce. Verified on the Fedora 44 host: verify.lua PASS 43/FAIL 0, concurrent reporting cpus_allowed=1 distinct_cpus=1 max_inflight=16 interleaves=16 while parallel reports distinct_cpus=16, C++26 Contracts built four times for four distinct behaviors under GCC 16, and a hardened build trapping on __n < this->size() where the unchecked build is asserted statically to contain no bounds check at all."
+description: "conc runs one identical workload under four execution models (sequential, concurrent (pinned to one CPU by sched_setaffinity), parallel (one thread per CPU), and both (2x oversubscribed)) and reports structure rather than time: cpus_allowed from sched_getaffinity as a permission, distinct_cpus unioned from sched_getcpu as an outcome, a max_inflight high-water mark, and interleaves counted from gaps in a monotonic ticket sequence. All four converge on digest=0x481984990deee5ff, the same value the C++ toolbox, the Go toolbox, and the Rust toolbox produce. Verified on the Fedora 44 host: verify.lua PASS 43/FAIL 0, concurrent reporting cpus_allowed=1 distinct_cpus=1 max_inflight=16 interleaves=16 while parallel reports distinct_cpus=16, C++26 Contracts built four times for four distinct behaviors under GCC 16, and a hardened build trapping on __n < this->size() where the unchecked build is asserted statically to contain no bounds check at all."
duration: "55 minutes"
---
diff --git a/_docs/50-pthreads.md b/_docs/50-pthreads.md
index 5067c36..3d31a2c 100644
--- a/_docs/50-pthreads.md
+++ b/_docs/50-pthreads.md
@@ -2,7 +2,7 @@
title: "pthreads: the thread under std::thread, its two identities, and the six controls the standard does not give you"
order: 50
part: "Compendium: C++ Concurrency"
-description: "pthreads runs one facet per subcommand over the per-thread controls POSIX offers and the C++ standard library does not expose: gettid() versus the opaque pthread_t (and why the main thread's tid equals the pid), pthread_setname_np landing in /proc/self/task/ exited status `. Credentials from the kernel, not from
the client's own claims.
-- **The log is opened O_RDWR|O_APPEND** — a write-only fd would arrive
+- **The log is opened O_RDWR|O_APPEND**: a write-only fd would arrive
write-only in pmctl (the *file description* crosses the socket, flags and
all), and the read side would fail with EBADF.
@@ -58,13 +58,13 @@ plus cmsg) out.
| **Go 1.26** | `unix.UnixRights` / `unix.ParseSocketControlMessage` / `unix.ParseUnixRights` over `net.UnixConn.WriteMsgUnix`/`ReadMsgUnix` | reaper goroutine; restart backoff vs. shutdown is a `select` on a timer and a stop channel; wrapped errors end at `pmon: error:` |
| **Rust (2024)** | `nix::sys::socket` `sendmsg`/`recvmsg` with `ControlMessage::ScmRights` and `cmsg_space!` | reaper thread, `Arc<(Mutex child exited normally
@@ -10,7 +10,7 @@
// exits 127 with no report lines.
//
// Where C++ forks and execs by hand, Go's os/exec does the same dance
-// internally (fork/exec with a CLOEXEC status pipe — see syscall.forkExec);
+// internally (fork/exec with a CLOEXEC status pipe; see syscall.forkExec);
// the exec failure that the C++ version ships over its self-pipe surfaces
// here as an error from cmd.Start. The wait-side facts come from
// os.ProcessState: Sys() is the raw wait status, SysUsage() the rusage that
diff --git a/examples/11-process-lifecycle/rust/src/main.rs b/examples/11-process-lifecycle/rust/src/main.rs
index 697abc8..e982926 100644
--- a/examples/11-process-lifecycle/rust/src/main.rs
+++ b/examples/11-process-lifecycle/rust/src/main.rs
@@ -1,4 +1,4 @@
-//! pmon run -- CMD [ARGS...] — minimal process monitor for the process
+//! pmon run -- CMD [ARGS...]: minimal process monitor for the process
//! lifecycle chapter: spawn CMD, wait for it, report its fate and rusage.
//!
//! ```text
diff --git a/examples/11-process-lifecycle/verify.lua b/examples/11-process-lifecycle/verify.lua
index 6ebc440..f840ae5 100644
--- a/examples/11-process-lifecycle/verify.lua
+++ b/examples/11-process-lifecycle/verify.lua
@@ -9,7 +9,7 @@
-- exact maxrss/user/sys/wall shape with plausible values (wall covers a real
-- 200 ms sleep, maxrss is nonzero, user CPU registers for a busy loop); and
-- an exec failure prints only "pmon: exec ` are required;
## Verification
`verify.lua` (mode `vm-peer`) runs the **local** two-process assertion on
-loopback multicast — it clears `TARGET` so `demo.sh run` executes on the host,
+loopback multicast: it clears `TARGET` so `demo.sh run` executes on the host,
starts `alice` and `bob` with distinct names and TCP ports, and asserts each
discovers the other exactly once, exchanges the chat frame, and exits 0, plus
the usage/exit-code shapes. Distinct ports per language keep back-to-back
diff --git a/examples/23-udp-and-peer-discovery/cpp/src/main.cpp b/examples/23-udp-and-peer-discovery/cpp/src/main.cpp
index 04731c3..cb5af38 100644
--- a/examples/23-udp-and-peer-discovery/cpp/src/main.cpp
+++ b/examples/23-udp-and-peer-discovery/cpp/src/main.cpp
@@ -1,9 +1,9 @@
-// chatterd v2 (C++23) — UDP multicast peer discovery + a TCP chat exchange.
+// chatterd v2 (C++23): UDP multicast peer discovery + a TCP chat exchange.
// Grows the same chatterd program introduced in chapter 21: the TCP frame is
// the canonical chatterd chat frame (2-byte magic "CH" + 1-byte version +
// 1-byte type + 2-byte big-endian length + UTF-8 payload) shared by every
// chatterd version (ch21-ch27). The UDP discovery beacon below is a
-// completely separate wire object — a plain ASCII datagram, no relation to
+// completely separate wire object: a plain ASCII datagram, no relation to
// the chat frame's magic/version/type/length header.
//
// chatterd discover --group 239.7.7.7 --port 51888 --name alice \
@@ -43,7 +43,7 @@ constexpr std::string_view kBeaconMagic = "CHATTERD1"; // UDP beacon tag only
// 0..65535), payload (UTF-8, type-specific). This header is byte-identical
// across every chatterd version; ch23's post-discovery greeting uses only
// JOIN (announce the dialer's name) and DELIVER (name + NUL + text, the
-// listener's reply) — the same types chapters 21/22/24/27 share.
+// listener's reply): the same types chapters 21/22/24/27 share.
constexpr std::uint8_t kFrameMagic0 = 0x43; // 'C'
constexpr std::uint8_t kFrameMagic1 = 0x48; // 'H'
constexpr std::uint8_t kFrameVersion = 0x01;
diff --git a/examples/23-udp-and-peer-discovery/go/main.go b/examples/23-udp-and-peer-discovery/go/main.go
index ae27e27..e763a8f 100644
--- a/examples/23-udp-and-peer-discovery/go/main.go
+++ b/examples/23-udp-and-peer-discovery/go/main.go
@@ -1,9 +1,9 @@
-// chatterd v2 (Go) — UDP multicast peer discovery + a TCP chat exchange.
+// chatterd v2 (Go): UDP multicast peer discovery + a TCP chat exchange.
// Grows the same chatterd program introduced in chapter 21: the TCP frame is
// the canonical chatterd chat frame (2-byte magic "CH" + 1-byte version +
// 1-byte type + 2-byte big-endian length + UTF-8 payload) shared by every
// chatterd version (ch21-ch27). The UDP discovery beacon below is a
-// completely separate wire object — a plain ASCII datagram, no relation to
+// completely separate wire object: a plain ASCII datagram, no relation to
// the chat frame's magic/version/type/length header.
//
// chatterd discover --group 239.7.7.7 --port 51888 --name alice \
@@ -36,7 +36,7 @@ const beaconMagic = "CHATTERD1" // UDP beacon tag only
// 0..65535), payload (UTF-8, type-specific). This header is byte-identical
// across every chatterd version; ch23's post-discovery greeting uses only
// JOIN (announce the dialer's name) and DELIVER (name + NUL + text, the
-// listener's reply) — the same types chapters 21/22/24/27 share.
+// listener's reply), the same types chapters 21/22/24/27 share.
const (
frameMagic0 = 0x43 // 'C'
frameMagic1 = 0x48 // 'H'
diff --git a/examples/23-udp-and-peer-discovery/rust/src/main.rs b/examples/23-udp-and-peer-discovery/rust/src/main.rs
index 239bade..2533eeb 100644
--- a/examples/23-udp-and-peer-discovery/rust/src/main.rs
+++ b/examples/23-udp-and-peer-discovery/rust/src/main.rs
@@ -1,9 +1,9 @@
-// chatterd v2 (Rust 2024) — UDP multicast peer discovery + a TCP chat
+// chatterd v2 (Rust 2024): UDP multicast peer discovery + a TCP chat
// exchange. Grows the same chatterd program introduced in chapter 21: the TCP
// frame is the canonical chatterd chat frame (2-byte magic "CH" + 1-byte
// version + 1-byte type + 2-byte big-endian length + UTF-8 payload) shared by
// every chatterd version (ch21-ch27). The UDP discovery beacon below is a
-// completely separate wire object — a plain ASCII datagram, no relation to
+// completely separate wire object: a plain ASCII datagram, no relation to
// the chat frame's magic/version/type/length header.
//
// chatterd discover --group 239.7.7.7 --port 51888 --name alice \
@@ -32,7 +32,7 @@ const BEACON_MAGIC: &str = "CHATTERD1"; // UDP beacon tag only
// 0..65535), payload (UTF-8, type-specific). This header is byte-identical
// across every chatterd version; ch23's post-discovery greeting uses only
// JOIN (announce the dialer's name) and DELIVER (name + NUL + text, the
-// listener's reply) — the same types chapters 21/22/24/27 share.
+// listener's reply), the same types chapters 21/22/24/27 share.
const FRAME_MAGIC: [u8; 2] = [0x43, 0x48]; // "CH"
const FRAME_VERSION: u8 = 0x01;
const FRAME_JOIN: u8 = 1;
diff --git a/examples/23-udp-and-peer-discovery/verify.lua b/examples/23-udp-and-peer-discovery/verify.lua
index a2ceefc..593ee59 100644
--- a/examples/23-udp-and-peer-discovery/verify.lua
+++ b/examples/23-udp-and-peer-discovery/verify.lua
@@ -4,7 +4,7 @@
-- mode:vm-peer, but the verify itself is the LOCAL two-process assertion: two
-- chatterd instances with distinct names run on loopback multicast, discover
-- each other, and exchange one canonical chatterd TCP chat frame (JOIN then
--- DELIVER — the same frame ch21/22/24/27 share). (The real two-host demo
+-- DELIVER, the same frame ch21/22/24/27 share). (The real two-host demo
-- across systems-target/systems-peer is driven separately by the chapter's
-- demo; see README.) demo.sh run is forced local here by clearing TARGET, so
-- the assertion never touches a VM.
diff --git a/examples/24-time-timers-deadlines/README.md b/examples/24-time-timers-deadlines/README.md
index 221566f..a4d92e3 100644
--- a/examples/24-time-timers-deadlines/README.md
+++ b/examples/24-time-timers-deadlines/README.md
@@ -1,7 +1,7 @@
-# 24-time-timers-deadlines — chatterd v3, heartbeats + deadlines
+# 24-time-timers-deadlines: chatterd v3, heartbeats + deadlines
-Chapter 24 grows `chatterd` — the peer-to-peer chat daemon introduced in
-chapter 21 — into a version that survives a peer going away. The lesson is
+This chapter grows `chatterd` (the peer-to-peer chat daemon introduced in
+the TCP sockets chapter) into a version that survives a peer going away. The lesson is
*time*: how to keep a connection alive with a periodic heartbeat, how to
declare a peer dead by measuring **silence on a monotonic clock**, and how to
come back with a **jittered exponential backoff** instead of a hot reconnect
@@ -32,8 +32,8 @@ opts: --heartbeat-ms H (default 1000) PING interval
`res` is `clock_getres(CLOCK_MONOTONIC)`; the two `actual` fields are the
measured wall-time of a 1 ms `nanosleep(2)` and a 1 ms one-shot `timerfd`,
each timed on a **monotonic/steady clock** (never `CLOCK_REALTIME`). On a
- typical host both land near 1050–1100 µs — the overshoot is the scheduler's
- wake-up latency, which is exactly the point.
+ typical host both land near 1050–1100 µs: the overshoot is the scheduler's
+ wake-up latency, which is the point.
- **`listen`** binds, prints `chatterd: ` — the consumer's running total. Because the values are `0..N-1`,
+- `items=`: the consumer's running total. Because the values are `0..N-1`,
a correct run **always** yields `s = N·(N-1)/2`. Any dropped, duplicated, or
torn hand-off changes this number, so the sum is a concurrency oracle.
-- `throughput_mops=