Refactoring elfuse code
Refactoring preserves what the guest observes, not merely a host return value. Do not call a behavior change a cleanup to make it easier to review.
Route the change first
Use this skill only after the code is known to work. A fault, hang, wrong
errno, or intermittent result starts with elfuse-debug instead.
Read the sibling skill that owns a boundary before moving code across it:
elfuse-guest-abifor guest registers, page permissions, HVC, TLBI, EL0 return state, fork, or Rosetta state.elfuse-syscallfor syscall dispatch, guest-memory translation, paths, file descriptors, or lock discipline in the syscall surface.elfuse-verifywhen changing arithmetic covered bysrc/proved/or adding bounds math over guest-controlled values.
Do not fold a correctness finding into a cleanup. Report it separately: a diff that both moves code and changes behavior cannot be reviewed as either.
Decide whether the finding is real
Name the friction before treating it, and let that name bound the diff: which call site is hard to read, which contract is hard to find, which edit had to be repeated across files. A cleanup with no named friction has no stop condition, and the reviewer inherits the job of deciding where it should have ended. A second finding met on the way out is reported in its own diff, not absorbed into this one.
Measure it. references/measuring.md carries the scans for function size,
duplication, and nesting, and the traps in reading each. Report a number with
the command that produced it and a judgment as a judgment; never quote a count
from a document, this one included.
Prefer deletion, an existing local helper, or a direct rewrite over a new abstraction. A parameter every caller gives the same value, a hook with one implementation, and future-facing scaffolding are dead flexibility. Remove them unless a tracked contract needs them.
Extract only when it gives a named operation or makes one contract easier to see. Splitting a single Linux flag matrix across helpers hides the syscall contract; separating path resolution, a host call, and result translation can clarify it.
Check all callers before changing a helper. A 0/-1 helper used only as a
success/failure test should normally return bool, but read the body before
converting: most such helpers here set errno on the failure path or forward a
primitive's, which is what elfuse-conventions reserves int for. A sweep by
call-site shape alone converts far more than it should. A repeated ABI literal
belongs in src/syscall/abi.h or src/syscall/linux-wire.h, whichever the
neighboring constants already use.
Treat these as intentional until the surrounding contract proves otherwise:
- Similar Linux and macOS ABI structures are a translation boundary, not duplicate types. Keep packed Linux structures beside their translation.
- Repeated conversion switches belong in
src/syscall/translate.c. An inline conversion elsewhere is the smell. sc_wrappers insrc/syscall/syscall.care typed dispatch unpacking. Their unused-parameter runs are the widest hit any duplication scan reports here, every time. Collapsing them behind a macro hides which arguments a handler ignores.- Fixed-width guest values and per-vCPU
_Thread_localstate carry ABI and concurrency meaning. Do not wrap or promote them for appearance alone. - A test fixture that only prints may have its oracle in a shell lane. Find the lane before adding an assertion.
Before deleting a symbol, search src/syscall/dispatch.tbl, src/core/shim.S,
and tests/ as well as C callers. Generated dispatch, assembly, and test
lanes are reachability paths.
Preserve the boundaries
- A helper that acquires a lock changes lock order at every caller. The lock
order comment in
src/syscall/internal.his the contract. A helper that releases one, or drops and retakes it, is the same hazard read backwards, and the name has to carry it:_lockedalready means "call me holding it", so a helper that hands the lock back needs a different suffix and a comment saying the caller must not touch that lock again. - Moving an interruptible wait into a helper moves its restart classification.
scripts/check-eintr-contract.pykeys on the function that decides, not the syscall that returns, so the new helper takes the inventory entry and the caller, now a forwarder, drops out of it. The gate fails the build until both happen. Carry any reasoning the departing entry held into a comment at the code it described rather than deleting it with the entry. - Vector entry stubs in
src/core/shim.Scannot clobber a GPR beforesvc_handler; do not consolidate them through a scratch-register prologue. - Do not unify X8 drop-frame-marker writes by appearance. Find current writes
with
grep -rn 'HV_REG_X8' src/, readsrc/core/shim.S, then followelfuse-guest-abi. src/syscall/dispatch.tblowns generated dispatch. Change that input, not generated output.
When two functions encode the same forward and reverse bit correspondence, state the mapping once as a table and use it in both directions. Similar code is not enough: the two bodies must represent the same fact.
Work in small, evidenced steps
Read docs/testing.md, section "Validation Strategy By Change Type", and run
the selected baseline before a multi-step cleanup. Keep inherited failures
separate from the change. Stop when the baseline is red in the area being
changed.
Make one behavior-preserving step at a time, then run its mapped lanes. A pure
cleanup uses the same validation as the feature area, not a weaker set, and
elfuse-verify owns reading the verdicts. Changes under src/proved/ require
make verify and make verify-mutants.
make lint is advisory except for readability-function-size, which
.clang-tidy names in WarningsAsErrors, so a function crossing the ceiling
fails the job. Consult .clang-tidy for its current checks; report only
warnings introduced by touched code, plus any function-size result. Do not
claim that lint is clean.
Keep formatting local. make indent should be a no-op on a clean tree; a
tree-wide formatter diff signals a version or tooling problem, not cleanup.
Check git diff --numstat before handoff and split unrelated churn out.
Extracting a helper almost always leaves a call site the formatter wants to
wrap differently, so expect make check-format to fail once per extraction.
Run the formatter on that file and confirm from git diff --numstat that it
touched only the lines you wrote: a file reporting far more is the version
problem above, not the wrap. commentflow reflows adjacent comment lines into
one paragraph, so a new comment block placed directly under an existing one
merges into it; separate them with a bare * or # line.
For a prose cleanup, follow
.claude/skills/elfuse-conventions/references/prose-reduction.md. A Python
docstring may be runtime data, so search its consumers before removing it.
Authoritative sources
.clang-tidyfor the enabled lint checks and which of them gates.references/measuring.mdfor the scans behind any count reported here.scripts/check-eintr-contract.pyfor the restart classifications.docs/testing.md, section "Validation Strategy By Change Type", for validation lanes.src/syscall/internal.hfor lock order.src/core/shim.Sfor shim entry and return conventions.