Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a036a6f
build(nix): keep debug outputs lean
daniel-noland Aug 15, 2026
b1ef45d
feat(debug): add a version-matched core viewer
daniel-noland Aug 15, 2026
74af5d2
feat(debug): add a live DAP debugger
daniel-noland Aug 15, 2026
43ad401
feat(debug): add a syscall tracer image
daniel-noland Aug 15, 2026
9b9b9bf
ci: publish debug images on a deliberate cadence
daniel-noland Aug 15, 2026
c666922
test(debug): exercise the debug images instead of only building them
daniel-noland Aug 16, 2026
06c0cca
feat(debug): run a binary or a single test inside the debug images
daniel-noland Aug 16, 2026
1f46121
docs: Record what a commit message carries
daniel-noland Aug 26, 2026
d2e63fe
build(just): Write an lcov report beside the html one
daniel-noland Aug 26, 2026
c99490e
build(nix): Add cargo-expand, cargo-mutants and cargo-show-asm to the…
daniel-noland Aug 26, 2026
d1398e3
feat(debug): Build gdb and perf to run where there is no nix store
daniel-noland Aug 26, 2026
9016291
feat(debug): Dump a core from a running process without ending it
daniel-noland Aug 26, 2026
3a27f88
build(nix): Add duvet to the dev toolchain
daniel-noland Aug 19, 2026
90a2468
build(nix): Add deno to the dev toolchain
daniel-noland Aug 20, 2026
1888d67
feat(spec): Cross-check a test citation against its implementation ci…
daniel-noland Aug 20, 2026
fa4dca5
build(just): Recipe-ise duvet, cargo-mutants, and the interlock
daniel-noland Aug 20, 2026
931636e
feat(spec): Let the interlock classify a mutant instead of only killi…
daniel-noland Aug 21, 2026
8b75611
feat(spec): Say why a mutant survived, using coverage
daniel-noland Aug 21, 2026
0ea0d48
fix(spec): Accept the second-half boundary the block guard makes unre…
daniel-noland Aug 21, 2026
5be2ae5
fix(net): Store the checksum an incremental update computes
daniel-noland Aug 7, 2026
88c3696
test(net): Check the HeadersView unsafe boundary against the safe mat…
daniel-noland Aug 8, 2026
6edcf93
docs(net): Correct where the HeadersView soundness check actually fires
daniel-noland Aug 8, 2026
83f48b5
test(net): Check the mutable half of the HeadersView boundary, and ma…
daniel-noland Aug 8, 2026
1ef005f
test(net): Extend the HeadersView differential to every arity the ora…
daniel-noland Aug 8, 2026
bcaf281
test(net): Reach the quoted packet inside an ICMP error, and the exte…
daniel-noland Aug 8, 2026
1f0592e
test(net): Cover the optional matchers, and find what their combinato…
daniel-noland Aug 9, 2026
c7cde9b
test(net): Make the shape follow the chain, and reach every arity's look
daniel-noland Aug 9, 2026
9833e03
test(net): Stack the extension headers the builder always could
daniel-noland Aug 9, 2026
214a62b
test(net): Specialize the ICMP subtypes, and find that conform is dead
daniel-noland Aug 9, 2026
f5188c3
test(net): Keep the embedded vacuity guard armed under coverage
daniel-noland Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ Production artifacts are produced via nix builds in a separate CI workflow.
- `ci:+miri` - Run Miri checks
- `ci:+wasm` - Run the WASM build check
- `ci:+concurrency` - Run Shuttle and Loom tests
- `ci:+debug-images` - Also build and push the core viewer, DAP debugger, and
syscall tracer images. They are built on main, in the merge queue, and on
dispatch regardless, and `ci:+merge-ready` turns them on too, since
`ci-gate` treats that label as enabling every gate; this is for when the
build itself needs debugging
- `ci:+cross` - Build all cross-platform containers
- `ci:+cross/full` - Also run the workspace test suite under qemu-user, on the
two aarch64 musl legs. Gated like every other job, so the merge queue and
Expand Down Expand Up @@ -104,13 +109,92 @@ If those queue failures stop being rare, the phasing is worth revisiting.
- Coverage: `debug` by default; `fuzz` on deep runs
- Miri: required on deep runs; opt-in on pull requests with `ci:+miri`
- Containers: debug/release for dataplane and FRR; release for validator
- Debug images (core viewer, DAP debugger, syscall tracer): deep runs only,
or on a pull request with `ci:+debug-images`
- VLAB configurations: spine-leaf fabric mode, L2VNI/L3VNI VPC modes,
with gateway enabled

### Artifacts

- Container images pushed to GitHub Container Registry (GHCR)
- Release containers published on tag pushes via `just push`
- `ghcr.io/githedgehog/dataplane/core-viewer` opens a core file from the lab.
It carries gdb plus the unstripped binaries and sources for the matching
`ghcr.io/githedgehog/dataplane` build.
Pull the tag matching the build the core came from; symbols only line up with
the exact version and profile that produced it.
The entrypoint takes the core as its only argument:

```console
docker run --rm -it -v /path/to/cores:/cores \
ghcr.io/githedgehog/dataplane/core-viewer:TAG /cores/core.1234
```

- `ghcr.io/githedgehog/dataplane/dev-debugger` debugs a live dataplane from an
editor. It carries bugstalker, which understands Rust's std collections and
enum layouts, and listens for a Debug Adapter Protocol client on port 4711.
Publish the port and point the editor's DAP client at it:

```console
docker run --rm -p 127.0.0.1:4711:4711 ghcr.io/githedgehog/dataplane/dev-debugger:TAG
```

Connecting does not by itself start anything. In remote-DAP mode bugstalker
waits for the client's `launch` request to name the program, so the editor
has to send `program`, and any dataplane arguments as `args`. A request
without `program` is rejected with `launch: missing arguments.program`.
For VS Code, in `.vscode/launch.json`. `type` has to match whatever debug
type the BugStalker extension you installed registers -- it is not a name we
choose, and it differs between extensions, so check the one you have rather
than copying this field blind:

```json
{
"type": "bs",
"request": "launch",
"name": "dataplane (container)",
"debugServer": 4711,
"program": "/bin/dataplane",
"args": []
}
```

For `nvim-dap`, where the first line names the adapter itself, so `type = "bs"`
below is our own label rather than an extension's:

```lua
dap.adapters.bs = { type = "server", host = "127.0.0.1", port = 4711 }
dap.configurations.rust = {
{
type = "bs",
request = "launch",
name = "dataplane (container)",
program = "/bin/dataplane",
args = {},
},
}
```

- `ghcr.io/githedgehog/dataplane/syscall-tracer` records what the dataplane
asks the kernel for, as JSON, using lurk.
It carries the same stripped binaries the release image ships, since nothing
here symbolizes, so it is smaller than the other two -- though not by as much
as that suggests: like them it ships the source tree, which the entrypoint
makes the working directory. Only the debug symbols and the debuggers
themselves are absent.

```console
docker run --rm ghcr.io/githedgehog/dataplane/syscall-tracer:TAG > trace.jsonl
```

The stream is one JSON object per line, except that tracing child threads
makes lurk announce each one with a bare `Attaching to child <pid>` line.
Filter those out if the consumer needs strict JSONL:

```console
jq -R 'fromjson? // empty' < trace.jsonl
```

- Coverage reports from each `coverage/<profile>` job, kept for 7 days:
- `coverage-html-<profile>.tar.gz` - `llvm-cov` HTML report, including the
per-branch counts that Codecov does not render. Unpack and open
Expand All @@ -121,6 +205,53 @@ If those queue failures stop being rare, the phasing is worth revisiting.
Both upload unarchived, so they download as the named file rather than
wrapped in a zip.

### Debugging locally

The published images debug what CI built. To debug what you are building, `just
debug` builds the matching image and runs a workspace binary or a single test
inside it, at whatever `profile`, `platform`, `instrument`, and `sanitize` you
pass. Symbols only line up when those match the build the problem appeared in,
which is the whole reason to go through the image rather than a system gdb.

```console
just debug # pick from a list
just debug bugstalker # pick, then wait for an editor
just debug lurk dataplane # trace syscalls, streams JSON, runs to exit
just debug gdb dataplane # gdbserver on 2345, waits for a client
just profile=fuzz debug gdb test_parse_interface args
just debug-list # print the same list without running anything
```

Name nothing and everything is offered through `skim`, which the dev shell
provides. Name a filter matching one test and it runs without asking; name one
matching several and those are offered. A filter matching nothing is an error
rather than a guess, and so is an ambiguous one when there is no terminal to
ask at, which is what makes this safe to call from a script.

The third argument narrows which archive is searched, so
`just debug gdb some_test args` builds only `args`' tests. It is worth passing:
the default builds every test in the workspace, which is a long wait if all you
wanted was to pick from a short list.

`gdb` and `bugstalker` block until you disconnect and interrupt them; that is
the point. `gdb` prints the `target remote` line to use. `bugstalker` prints a
`.zed/debug.json` entry ready to paste, because in remote-DAP mode it takes the
program from the client's launch request rather than from its own command line,
so connecting an editor is only half of it. The `tcp_connection` field in that
entry is what stops the editor spawning a second debugger of its own.

A test runs with its package directory as the working directory, the way
nextest runs it, so relative paths behave the same as under `just test`.

To open a core file:

```console
just inspect-core /path/to/core.1234
```

Pass the same build settings that produced the binary that dumped
(`just profile=release inspect-core ...`), for the same reason.

---

## Linting and Validation Workflows for Pull Requests
Expand Down
27 changes: 23 additions & 4 deletions .github/workflows/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ jobs:
outputs:
container_profiles: "${{ steps.container-profiles.outputs.value }}"
parallel: "${{ steps.parallel.outputs.value }}"
container_targets: "${{ steps.container-targets.outputs.value }}"
profiles: "${{ steps.profiles.outputs.value }}"
concurrency: "${{ steps.concurrency.outputs.value }}"
cross: "${{ steps.cross.outputs.value }}"
Expand Down Expand Up @@ -132,6 +133,16 @@ jobs:
on-value: '["debug", "release", "fuzz"]'
off-value: '["debug"]'

# Debug images are opt-in on pull requests because of their size.
- id: "container-targets"
uses: *gate
with:
labels: "debug-images"
# Keep this on one line: `ci-gate` writes the value to GITHUB_OUTPUT
# with a plain printf, which a multi-line value would corrupt.
on-value: '["frr.dataplane", "dataplane", "dataplane-core-viewer", "dataplane-dev-debugger", "dataplane-syscall-tracer", "validator"]'
off-value: '["frr.dataplane", "dataplane", "validator"]'

# Lab jobs require release images but not other release/fuzz checks.
- id: "container-profiles"
uses: *gate
Expand Down Expand Up @@ -425,10 +436,7 @@ jobs:
fail-fast: false
max-parallel: ${{ fromJSON(needs.plan.outputs.parallel) }}
matrix:
nix-target:
- frr.dataplane
- dataplane
- validator
nix-target: "${{ fromJSON(needs.plan.outputs.container_targets) }}"
# TODO: enable cfi and safe-stack on release when possible
profile: "${{ fromJSON(needs.plan.outputs.container_profiles) }}"
exclude:
Expand All @@ -443,6 +451,17 @@ jobs:
with:
recipe: "ci::push-container"
recipe_args: "${{ matrix.nix-target }} ${{ matrix.profile }} ${{ needs.version.outputs.version }}"

# A debug image that builds is not a debug image that works. Two of
# these shipped with entrypoints that could not do what the README
# documents, and building them said nothing about it.
- name: "smoke"
if: "${{ startsWith(matrix.nix-target, 'dataplane-') }}"
uses: *just
with:
recipe: "ci::smoke-container"
recipe_args: "${{ matrix.nix-target }} ${{ matrix.profile }}"

- *verify-clean-tree
- *tmate

Expand Down
18 changes: 18 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,21 @@ Follow the [development guide](development/README.md).

If you make a design decision or do a code review, try to cite the section of the
development guide you are following.

## Commit messages

A commit message signposts the diff; it does not restate it. Rationale belongs where it
constrains the code -- in the code, its doc comments, or the development guide -- because a
second copy in the message is a second thing to keep correct, and the two will drift.

A message carries what the diff cannot:

- the "why" of the commit,
- non-obvious process facts,

It does not re-summarize the change, narrate how it was made, or repeat an argument the
commit already puts in the tree. For a docs commit, the file is the content; the message
points at it.

Keep `git log` scannable: a few lines by default, longer only where the diff is opaque and
the message is what makes the commit reviewable.
4 changes: 4 additions & 0 deletions ci.just
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ cross platform libc +args:
cross-test platform libc:
NEXTEST_PROFILE=cross-qemu just {{ _lab }} platform={{ platform }} libc={{ libc }} profile=debug test

# Verify a debug image's entrypoint actually does its job.
smoke-container target profile:
just {{ _lab }} profile={{ profile }} platform=x86-64-v3 smoke-container {{ target }}

# Publish both content-derived and discoverable per-commit tags.
[script]
push-container target profile version:
Expand Down
Loading
Loading