Architecture
Compiler-guided runtime patching
Learn about SPSLR internals, from compile-time instrumentation to metadata and runtime patching.
Pinpoint
pinpoint is the compiler-side component of SPSLR. It runs as a
staged GCC plugin because no single GCC representation contains all information
required for runtime structure layout randomization. Early frontend hooks still
know which source-level field is being accessed. Later optimization passes decide
which offset computations survive. Final RTL exposes the concrete register
allocation and instruction stream that must be patched. In the end, pinpoint emits
SPSLR metadata directly into ELF sections of the generated object file. These
sections become the interface consumed by selfpatch.
Target discovery
Target structures are selected with __attribute__((spslr)). When
GCC registers such a type, pinpoint records it as a compilation-unit-local target.
The target is keyed by GCC's main record variant rather than by spelling alone, so
typedefs and compatible variants resolve to the same compiler-side target.
struct example_target {
int flags;
void *ptr __attribute__((spslr_field_fixed));
unsigned long counter;
} __attribute__((spslr));
Once GCC has completed the type, pinpoint extracts the target layout. For each
field it records the field name, byte offset, size, alignment, and flags. Fields
marked with __attribute__((spslr_field_fixed)), bitfields, zero-sized
members, and overlapping byte ranges are treated as dangerous layout regions. Such
regions remain part of the target description, but pinpoint avoids generating
ordinary randomized field-offset pins for them.
Overlapping fields are merged into one fixed dangerous byte range. This keeps the runtime layout model byte-oriented and avoids pretending that sub-byte or overlapping storage can be independently randomized.
Preserving field-offset dependencies
The central problem for pinpoint is that GCC normally folds structure field offsets into constants and drops any association with struct and field identifiers. Once this happens, later compiler passes no longer know that an immediate value came from a particular field of a particular target structure.
SPSLR therefore uses a small GCC hook that exposes COMPONENT_REF
expressions while the frontend still knows that an expression is a field access.
When pinpoint sees a field access into an SPSLR target, it rewrites the access into
explicit pointer arithmetic whose offset is produced by a synthetic separator
expression.
Conceptually, a source-level access like this:
obj->field
is transformed into something equivalent to:
*(typeof(obj->field) *)((char *)obj +
__spslr_offsetof(target_uid, field_offset))
The separator is not a runtime call. It is a compiler-internal marker carrying two constants: the compilation-unit-local target ID and the original field offset. Later pinpoint passes must remove every separator before final code generation.
Mainline GCC folds many offsetof-like expressions while parsing,
before the earliest ordinary plugin hooks can inspect them. This causes important
access information to be unobtainable by any GCC plugin. To address this issue,
SPSLR adds the PLUGIN_BUILD_COMPONENT_REF callback to GCC. Every time
a COMPONENT_REF is constructed, the callback is invoked and
pinpoint is given a chance to record or rewrite it.
Stage 0: offset separation
The first GIMPLE pass, separate_offset, walks statements looking
for relevant field-access chains. It rebuilds nested component and array accesses
so that SPSLR-relevant field offsets become explicit separator computations, while
unrelated offsets remain ordinary constant pointer arithmetic.
This allows pinpoint to handle access chains such as nested structures and arrays while preserving only the randomized target offsets that matter to SPSLR.
outer.elems[i].inner.value
In this example, struct outer and struct elem are
SPSLR targets. The access therefore contains two SPSLR-controlled offsets: the
offset of elems inside struct outer and the offset of
inner inside struct elem.
After offset separation, these target-dependent offsets are rewritten into explicit separator expressions while ordinary address arithmetic remains unchanged. Conceptually, the access becomes:
*(typeof(outer.elems[i].inner.value) *)(
(char *)&outer
+ __spslr_offsetof(OUTER_ID, ELEMS_OFFSET)
+ sizeof(struct elem) * i
+ __spslr_offsetof(ELEM_ID, INNER_OFFSET)
+ offsetof(struct inner, value)
)
Each randomized target offset now appears as a separate compiler-visible expression that carries both the target identity and the original field offset. Later pinpoint passes transform these separator expressions into patchable instruction pins.
Stage 1: asm markers
The second GIMPLE pass, asm_offset, replaces separator calls with
synthetic inline-assembly markers. These markers still do not contain the final
machine code. Their purpose is to keep the offset value alive as a real data
dependency while giving GCC freedom to choose the destination register to hold
it.
Thus, each asm marker has one register output and two immediate inputs:
- the compilation-unit-local target ID,
- the original field offset.
Conceptually, the assembly markers do not carry any information that was not already present in the separator calls. Both representations encode the same target ID and original field offset. The transformation exists purely for code-generation reasons. If pinpoint kept the separator calls until final RTL, GCC would have to treat them as ordinary calls for much of compilation. This would force the surrounding code to respect normal calling-convention requirements, such as preserving caller-saved registers and modeling call side effects. Replacing the calls with register-output assembly markers preserves the exact same SPSLR metadata while allowing GCC to treat the value as an ordinary computed operand rather than a real function call. The markers therefore eliminate potential runtime overhead without changing the information carried through the compiler pipeline.
The marker is intentionally non-volatile. If an offset computation becomes unused after optimization, GCC may delete it. Only offset computations that survive optimization become instruction pins.
Stage 2: final instruction pins
The final RTL pass, spslr_rtl_pin_lower, runs shortly before GCC's
final output pass. At this point, GCC has selected a concrete destination register
for each surviving asm marker. Pinpoint then lowers the marker into explicit x86_64
instruction bytes.
Currently, pinpoint emits a dedicated mov imm32, r64 instruction.
The four-byte immediate field is labeled with a local assembler symbol. That symbol
becomes the address selfpatch later uses when rewriting the field offset.
; movq imm32, %rax
.byte 0x48, 0xc7, 0xc0
; the labeled imm32 value
.Lspslr_ipin_0:
.type .Lspslr_ipin_0, @object
.size .Lspslr_ipin_0, 4
.long 8
The symbol does not name a callable code location. It names the immediate bytes inside the instruction stream. The associated SPSLR metadata records the symbol, the immediate size, and the expression needed to compute the replacement value after layout randomization.
This design keeps runtime patching simple: selfpatch only overwrites the labeled immediate bytes. The tradeoff is that current pinpoint lowering materializes field offsets in a register instead of folding them directly into memory-addressing displacements.
mov $field_offset, %rax
mov (%rdi,%rax,1), %rax
A future implementation could avoid this extra materialization instruction by teaching the assembler to label operands directly or by introducing a relocation-like mechanism for patchable instruction operands. Until such support exists, pinpoint contains a small x86_64-specific instruction emitter for these immediate materialization sites.
Data pins
Instruction pins describe code locations. Data pins describe static storage that already contains SPSLR target instances in their original compile-time layout.
During declaration processing, pinpoint inspects every non-external static variable. If the variable contains an SPSLR target directly, inside an array, or nested inside another aggregate, pinpoint records a data pin for the corresponding object location.
struct inner {
int a, b;
} __attribute__((spslr));
struct outer {
int flags;
struct inner target;
};
static struct outer obj;
In this example, pinpoint records that obj contains a target
instance at the offset of outer::target. For arrays, it emits one
component for each element. For nested aggregates, it records the nesting level of
each target component.
Reordering an outer object's fields may relocate a member of a target type. This would invalidate the data pin referring to the original location of that inner instance. Thus, when metadata is emitted, data pin components are ordered by descending nesting level. This causes selfpatch to rewrite deeper embedded targets first, which may afterwards be moved as a whole when the outer object is rewritten.
Metadata
The interface between pinpoint and selfpatch consists
of a set of packed metadata structures. When finalizing a compilation unit,
pinpoint emits these directly into the assembler output stream. The
generated object file therefore contains ordinary ELF sections that hold the SPSLR
metadata. These sections are linked into the final subject image and later
discovered through linker-provided boundary symbols.
Compilation units own instruction pins, data pins, and CU-local target reference arrays. Targets and target layouts are globally deduplicated and shared across all compilation units that describe the same target type. All metadata is immutable after linking; for brevity, the following cards omit repeated const qualifiers.
extern struct spslr_unit
__start_spslr_units[];
extern struct spslr_unit
__stop_spslr_units[];
extern struct spslr_target
__start_spslr_targets[];
extern struct spslr_target
__stop_spslr_targets[];
Metadata roots
Every SPSLR subject exposes two metadata ranges: the compilation-unit array and an array of all unique targets within a subject. Selfpatch begins metadata discovery by iterating over these linker-defined boundaries. Together they form the root of the SPSLR metadata graph.
struct spslr_unit {
const char *source;
spslr_u64 target_cnt;
spslr_target_ref *target_refs;
spslr_u64 ipin_cnt;
struct spslr_ipin *ipins;
spslr_u64 dpin_cnt;
struct spslr_dpin *dpins;
} __packed;
struct spslr_unit
Represents one compilation unit that was compiled with pinpoint. Each unit owns the instruction pins, data pins, and target-reference array generated from that translation unit.
Pins never refer directly to global targets. Instead, they use unit-local target indices that are resolved through the unit's target-reference array.
typedef struct spslr_target
*spslr_target_ref;
spslr_target_ref
Each compilation unit defines an array of target references that maps compilation-unit-local target indices to globally unique target descriptors.
Instruction pins and data pins do not reference targets directly.
Instead, their unit_target_idx field indexes this array, which
resolves the pin's local target identifier to a global target
descriptor.
struct spslr_target {
char hash[16];
const char *name;
struct spslr_target_layout
*layout;
} __packed;
struct spslr_target
Describes a target type independently of any particular compilation unit.
The layout hash uniquely identifies the target's compile-time layout. Equivalent targets emitted by multiple compilation units are merged through COMDAT deduplication, allowing all units to reference the same shared target descriptor.
struct spslr_target_layout {
spslr_u64 size;
spslr_u64 field_cnt;
struct spslr_target_field
*fields;
} __packed;
struct spslr_target_layout
Describes the complete compile-time memory layout of a target type.
Selfpatch uses this information to construct randomized layouts and to translate compile-time field offsets into their randomized runtime locations.
The layout owns the array of field descriptors that describe the target's individual fields and fixed regions.
struct spslr_target_field {
const char *name;
spslr_u64 size;
spslr_u64 offset;
spslr_u64 alignment;
spslr_u64 flags;
} __packed;
struct spslr_target_field
Field descriptors define the constraints under which a target may be randomized, including original offsets, alignment requirements, and fixed-layout regions.
Fields marked with SPSLR_FLAG_FIELD_FIXED retain their original position in all randomized layouts.
struct spslr_ipin {
void *addr;
spslr_u64 size;
struct spslr_ipin_expr
*expr;
} __packed;
struct spslr_ipin
Represents a patchable instruction operand in executable code.
The address identifies the immediate bytes emitted and labeled by pinpoint. During randomization, selfpatch overwrites these bytes with a replacement value computed from the associated expression.
Instruction pins are owned by a compilation unit and are reached through spslr_unit::ipins.
struct spslr_ipin_expr {
spslr_u64 unit_target_idx;
spslr_u64 field_idx;
} __packed;
struct spslr_ipin_expr
The current expression format represents the randomized offset of a field within a target type. Selfpatch resolves the unit-local target index, looks up the corresponding target layout, and then obtains the randomized offset of the referenced field. The computed expression result is the replacement value for an instruction pin.
struct spslr_dpin {
void *addr;
spslr_u64 unit_target_idx;
} __packed;
struct spslr_dpin
Locates a statically allocated target instance that lives inside a subject's image. The target type is resolved through the owning compilation unit's target-reference array.
foo.c\0bar.c\0struct foo\0struc
t bar\0counter\0flags\0next\0he
ad\0node\0list\0module_state\0r
efcount\0parent\0child\0foo_tar
get\0list_node\0module_cfg\0ins
tance_count\0next\0............
String table
Stores all metadata strings used by SPSLR, including source file names, target names, and field names.
Metadata structures reference strings through ordinary pointers into this section.
Selfpatch
As the SPSLR runtime component, selfpatch is responsible for
constructing randomized target layouts and then applying those layouts to both code
and data described by pinpoint metadata.
Layout randomization
For each target, selfpatch must construct a randomized layout that is still valid for the original C object type. A valid randomized layout preserves the target's total size, satisfies every field's alignment requirement, avoids overlapping fields, and keeps fixed fields at their original offsets.
These constraints mean that selfpatch cannot simply shuffle the field list and lay the fields out again from scratch. A normal shuffle could increase the size of the target, violate alignment requirements, or move fields that must remain fixed. That would make existing allocation sizes and ABI-sensitive regions incompatible with the randomized layout.
Instead, selfpatch randomizes layouts through constrained field moves within the existing target size. A candidate move is accepted only if all layout invariants remain satisfied afterwards. Since relocating one field may require other fields to be repacked into the vacated space, selfpatch validates the resulting layout as a whole rather than treating field moves as independent operations.
The result is a randomized layout that changes the physical placement of eligible fields while preserving the original object size and all required layout invariants.
Patching
Once randomized layouts have been generated, selfpatch applies them to all metadata discovered in the subject image.
Instruction pins and data pins store compilation-unit-local target indices.
Before patching a compilation unit, selfpatch resolves these local indices to the
host's global randomized target state. The first part of the workspace buffer is
used as a temporary target map. For each entry in the unit's
target_refs array, selfpatch locates the corresponding global target
descriptor by comparing layout hashes and records the resulting global target index
in the map. While patching that unit, instruction pins and data pins use their
unit_target_idx field to access the resolved global target through
this temporary mapping.
Instruction pins are patched by evaluating their associated expressions against the randomized layout state. The resulting offset replaces the original immediate value stored at the instruction-pin address.
Data pins require a different strategy. Since fields may move to new offsets, rewriting an object in place could overwrite data that has not yet been copied. For this reason, the remainder of the workspace buffer is used as a temporary reorder buffer. For each data pin, the original object is first reordered into the workspace according to the randomized layout. Once all fields have been copied to their new positions, the reordered object is written back to the original storage location.
This approach allows arbitrary field reordering while avoiding overlap problems during the transformation.
Limitations
SPSLR is currently a research prototype and operates under explicit assumptions about compiler behavior, runtime patchability, and layout ownership. These assumptions define the correctness boundary of the implementation and document the cases that currently require exclusion or additional care.
Compiler and code-generation assumptions
- Field accesses must remain observable and transformable by pinpoint. Code generation patterns not recognized by pinpoint cannot be randomized safely.
- Inline assembly must not encode assumptions about randomized target layouts unless explicitly excluded from SPSLR or manually synchronized with runtime patching.
- The current implementation assumes predictable instruction pin lowering so that runtime patch sites remain identifiable and patchable.
Current pinpoint implementation limitations
The current pinpoint implementation intentionally preserves randomized field-offset dependencies until runtime patching. As a consequence, randomized field offsets cease to behave as compile-time knowledge in some contexts. Language constructs that require compile-time field offsets may therefore become invalid under SPSLR instrumentation.
In particular, static initialization involving pointers to randomized fields is currently unsupported:
struct target obj;
/* Fails to compile under SPSLR instrumentation (expression is non-const). */
int *field_ptr = &obj.field;
Fields that trigger compiler errors because of this restriction should currently
be marked with __attribute__((spslr_field_fixed)).
Pinpoint's separation transformations are not applied to fixed field references
which thus do not lose their compile-time evaluability.
Runtime and patching assumptions
- SPSLR assumes writable access can temporarily be obtained for executable and read-only mappings that contain instruction pins or static target instances.
- Dynamically allocated target instances must not survive across the patching boundary. Stack and heap target instances are not rewritten by selfpatch and must therefore either be destroyed before patching or only be created after selfpatching has completed.
- Pointers materialized from compile-time field offsets must not survive across the patching boundary. Addresses into target objects may become invalid after randomized layouts are applied.
Layout and ABI assumptions
- Layout-sensitive language constructs such as bitfields, compiler-specific packing behavior, or unusual layout rules may require exclusion from SPSLR.
- Externally visible binary interfaces that expose randomized target layouts require special care. Shared ABI structures may require fixed fields or exclusion from layout randomization.
These limitations are intentional constraints of the current prototype and document the assumptions under which SPSLR remains correct.