Sanemaker
Semantic validation for Selfpatch-SLR
Sanemaker verifies that instructions continue to access the same logical structure fields after Selfpatch-SLR or Bootpatch-SLR randomize their physical layout.
Introduction
Selfpatch-SLR relies on static metadata produced by the Pinpoint compiler plugin and on a runtime patcher that consumes that metadata. Correctness therefore depends on identifying every layout-dependent memory access, associating it with the correct target field, and patching every affected instruction to reference the corresponding randomized offset.
Layout dependencies are not necessarily expressed as syntactically obvious structure field accesses. Valid C code may depend on a structure layout without ever referencing a field:
static struct foo obj;
void set_at_42(char c)
{
*((char *)&obj + 42) = c;
}
Similar situations may arise through pointer arithmetic, type punning, inline assembly, and architecture-specific code. Consequently, the generated metadata is only as complete as the compiler analysis that produced it.
A single incorrect access may read or write another structure member while remaining within valid memory. The resulting corruption often propagates long before it causes an observable failure, making crashes poor indicators of where semantic correctness was first lost.
Sanemaker validates the runtime transformation by comparing the logical structure fields accessed by each executed instruction before and after Selfpatch-SLR.
An instruction that accesses a particular set of logical fields without Selfpatch-SLR must access the same logical fields after randomization.
This makes Sanemaker suitable as a deployment validation tool. A CI pipeline can execute representative workloads with Selfpatch-SLR disabled and enabled, compare both executions, and reject any build containing unexplained semantic mismatches.
Principles
Semantic field-access equivalence
Sanemaker is implemented as a QEMU TCG plugin. It receives metadata describing randomized targets, fields, data pins, instruction pins, executable image ranges, and Sanemaker trap locations.
During execution, the plugin observes memory accesses made by translated instructions and maps the accessed bytes back to logical target fields. Because it has access to both the original and randomized layout, it can normalize accessed address ranges into logical portions of fields such as:
task_struct.rcu_read_lock_nesting[0-4]
task_struct.se[24-40]
This comparison is independent of the fields' actual byte offsets. A field may move anywhere inside its target structure and still compare equal as long as the instruction follows it correctly.
Pre-patch and post-patch execution
Sanemaker keeps separate access buckets for execution before and after the selfpatch boundary. Selfpatch explicitly signals this boundary when the host image has completed its patch pass:
sanemaker_signal(SANEMAKER_SIGNAL_PATCH_BOUNDARY);
This distinction lets Sanemaker expose transition-state bugs. A pre-patch mismatch may indicate that code executed while randomized data was already live but some instruction sites still used their original offsets. Issues that occur even before Selfpatch has been invoked also show up as pre-patch but are unlikely to be attributed to SPSLR.
What Sanemaker guarantees
For every instruction exercised in both compared runs, Sanemaker verifies whether its observed target-field accesses remained semantically equivalent. It can therefore detect incorrect accesses even when they do not immediately crash the program or fail a functional assertion.
Safe use therefore combines workloads deliberately designed to exercise the relevant code surface with an empty unexplained-mismatch report.
Validation Workflow
A normal Sanemaker validation consists of five stages:
binscan
↓
baseline execution
↓
randomized execution
↓
logmerge
↓
logdiff
1. Generate metadata with binscan
python3 binscan.py \
-o "binscan.json" \
"program"
binscan.py reads the target ELF image and the metadata emitted by
Pinpoint. -o selects the output JSON file, and the positional argument
names the ELF image to inspect. The generated JSON describes target structures and
fields, data pins, instruction pins, executable ranges, trap incisions, and logical
access units.
--no-ipins: omit instruction-pin metadata.--no-dpins: omit data-pin metadata.--no-traps: omit Sanemaker trap metadata.--combine FILE: load ranges whose instructions should be treated as one logical access unit.
Some operations are logically larger than one machine instruction. A compiler
may implement memcpy, a structure assignment, or a vectorized copy
with several loads and stores whose boundaries depend on alignment. After layout
randomization, the same logical copy can cause different fields to fall on
different instructions even though the complete operation remains correct.
The optional --combine FILE argument lets such a range be treated
as one logical access unit. Sanemaker then compares the union of fields touched by
all instructions within the unit instead of assigning semantic meaning to each
implementation-level instruction separately. The specified file contains one
keyword per line. Every function whose name contains one of those keywords is
emitted as a logical access unit. Unwanted entries can be manually removed from the
generated json file.
Sanemaker does not require the instruction or data pins, but to use it, the traps may not be omitted. Besides using the binscan output directly with Sanemaker, it can be used to verify whether specific instructions were properly instrumented.
2. Record the baseline execution
qemu-x86_64 \
-plugin "sanemaker.so,focus=binscan.json,out=logs/baseline,spslr=off" \
"program" ...
qemu-x86_64executes the userspace ELF under QEMU user-mode translation.-pluginloads Sanemaker.focus=...supplies binscan metadata.out=...selects the directory for per-process logs.spslr=offrecords the non-randomized semantic reference execution.
3. Record the randomized execution
qemu-x86_64 \
-plugin "sanemaker.so,focus=binscan.json,out=logs/spslr,spslr=on" \
"program" ...
This run uses the same image and workload, but spslr=on enables
runtime randomization. Reusing one binary avoids address drift and keeps the
comparison focused on the runtime transformation.
4. Merge per-process logs
On exit, each process dumps one log file with a unique name to the specified output directory. In linux-user mode, guest and host code runs within the same host process. A guest fork also forks the QEMU and Sanemaker state. Thus, each guest process eventually produces one log file. In system mode, QEMU typically uses only one actual process, within which many kernel tasks can run, so there is only one log file.
python3 logmerge.py \
out="baseline.json" \
logs/baseline/sanemaker.*.json
python3 logmerge.py \
out="spslr.json" \
logs/spslr/sanemaker.*.json
Logmerge combines the per-process logs into per-run aggregates. The
out=... argument selects the merged output file; the remaining
arguments are the JSON fragments.
5. Compare normalized accesses
python3 logdiff.py \
binscan="binscan.json" \
missing="missing.logdiff" \
mismatches="mismatches.logdiff" \
"baseline.json" \
"spslr.json"
binscan=...supplies symbol, field, image, and combine-range metadata.missing=...specifies the output dump location for instructions observed in only a subset of runs.mismatches=...specifies the output dump location for instructions that touched different fields.- The final arguments are the merged baseline and SPSLR logs.
Interpreting results
- Missing instructions: PCs observed in only one run.
- Mismatches: PCs observed in both runs that accessed different normalized fields.
Missing instructions often result from timing, scheduling, input-dependent control flow, or early termination. Mismatches are the principal correctness signal because they mean that the same instruction address had different field semantics between runs.
Post-patch mismatch at pc <main>:0x401234
common: [target.header[0-8]]
baseline.json additional: [target.payload[0-4]]
spslr.json additional: [target.flags[0-4]]
common lists fields touched in every run. Each
additional set lists fields unique to that run. Pre-patch
and Post-patch identify the bucket relative to the complete-image
boundary. <main> names the executable image, not a source-level
function.
Every mismatch must be investigated. It may identify a real missed access, an incorrect pin, an unsafe transition, or an access unit that should be combined.
CI and deployment
Sanemaker is intended to act as a release gate. A practical pipeline builds an instrumented image, records a baseline run, records an SPSLR-enabled run with the same workload, and rejects the build if the comparison contains an unexplained mismatch.
The strength of that gate depends on coverage. Production validation should deliberately expand the exercised surface with unit and integration tests, fuzzers, stress workloads, compiler and optimization matrices, architecture-specific tests, different program or kernel configurations, and error or recovery paths.
For Bootpatch-SLR, QEMU system emulation under TCG can be combined with kernel selftests, targeted initramfs scenarios, subsystem test suites, syzkaller-style workloads, and configuration matrices. Each job contributes evidence for the paths it executes.
Impact
Sanemaker changes the trust model. Instead of assuming that all access sites were found and patched correctly, a deployment pipeline can compare the original and randomized executions at the field level. It can detect silent corruption at the first observed divergent access, before that corruption becomes an unrelated crash or escapes testing entirely.
This does not eliminate the need for testing, code review, or careful patcher design. It makes those activities measurable. Coverage describes which execution surface was tested; Sanemaker describes whether layout randomization preserved the semantics of that surface.
The Linux kernel case study Sanemaking the Linux Kernel Boot Process demonstrates this workflow on a real Bootpatch-SLR failure. The article assumes the concepts and commands described on this page.