All of lore.kernel.org
 help / color / mirror / Atom feed
* [RFC 0/7] Bootpatch-SLR: Randomizing Linux Kernel Structure Layouts at Boot
@ 2026-06-05 20:25 York Jasper Niebuhr
  2026-06-05 20:25 ` [RFC 1/7] SPSLR pinpoint plugin source York Jasper Niebuhr
                   ` (8 more replies)
  0 siblings, 9 replies; 10+ messages in thread
From: York Jasper Niebuhr @ 2026-06-05 20:25 UTC (permalink / raw)
  To: linux-hardening; +Cc: kees, franzen

Hi,

my name is York Jasper Niebuhr. I am a Master's student in Informatics
at the Technical University of Munich.

For a while now, I have been developing Selfpatch-SLR (SPSLR), a system
to apply Structure Layout Randomization at runtime, and its Linux kernel
implementation, Bootpatch-SLR (BPSLR), as an independent research
project in my spare time. I am now beginning my Master's thesis, which
will build upon and further evaluate this work. This patch series
contains the current state of Bootpatch-SLR.

Several hardening mechanisms rely on compile-time diversification. While
these approaches can be highly effective, their benefits are reduced in
deployment scenarios where a single kernel build is distributed to
millions of systems. Once the layout and characteristics of that build
become known, they are known for every system running it. Selfpatch-SLR
investigates whether Structure Layout Randomization can be applied after
software has already been built, thereby allowing diversification to
occur at runtime rather than only at compile time. Bootpatch-SLR
explores that idea in the context of the Linux kernel. The goal of this
project is to investigate the feasibility of the approach and identify
assumptions and obstacles that would need to be addressed by a
production-quality implementation.

Additional information about the project can be found at:

Documentation:
https://spslr.yjn-systems.com

Selfpatch-SLR repository:
https://github.com/YJN-Systems/Selfpatch-SLR

Bootpatch-SLR kernel mirror:
https://github.com/YJN-Systems/Bootpatch-SLR

The documentation website contains an overview of the architecture and
design of the system. I also intend to maintain a list of known issues,
limitations, observations, and other findings there as development
continues.

Selfpatch-SLR consists of three main parts: pinpoint, patchcompile, and
selfpatch. During compilation, the pinpoint GCC plugin records the
structure accesses that may later need to be adjusted. The patchcompile
command line tool then aggregates this metadata from all compilation
units and generates the runtime information needed for patching. At
startup, selfpatch chooses the randomized layouts and patches the image
before the randomized structures are used. For more details on the
architecture, please refer to:

https://spslr.yjn-systems.com/architecture.html

The current Bootpatch-SLR prototype focuses on randomizing task_struct
in a v6.12 kernel, at early boot, with a small number of fields exempt
from randomization due to current SPSLR implementation details. At this
stage, SPSLR tooling is only available for x86_64.

Building the SPSLR or BPSLR currently requires a custom toolchain based
on GCC 16, because mainline GCC folds important offsetof-like
expressions before the earliest plugin hooks. This patch is not part of
the kernel series and is not intended for upstream GCC at this stage.
For reviewers interested in reproducing the build and test environment,
I will provide the GCC patch separately in this thread and in the
Selfpatch-SLR repository.

On my test system (QEMU on an i9-13900H), the additional boot time is
currently below one second. Most of this time is spent in the current,
largely unoptimized randomizer. The actual image patching phase takes
only a few milliseconds.

The design goal of SPSLR is to eliminate the need for runtime lookup
tables or similar indirection mechanisms. Structure access instructions
are patched directly to reflect the randomized field offsets before
normal kernel execution begins.

As a result, the implementation does not require additional memory
accesses when accessing randomized structures. The current prototype
does, however, introduce a single additional mov instruction per target
structure access. This overhead is an implementation artifact rather
than a fundamental requirement of the approach and may be removed by
future tooling improvements.

This RFC patch series is organized as follows:

 * Patches 1 and 2 add the SPSLR tooling sources: the pinpoint GCC
   plugin and the patchcompile command-line tool.

 * Patch 3 integrates these tools into the kernel build system.

 * Patch 4 adds the selfpatch runtime support used to randomize and
   apply structure layouts during boot.

 * Patch 5 adds the BPSLR task_struct integration.

 * Patch 6 adds a simple tasklist sample module for sanity checking.

 * Patch 7 ignores BPSLR-generated files.

The current baseline configuration for testing is defconfig plus
CONFIG_SPSLR. Additionally, CONFIG_SAMPLES, CONFIG_SAMPLES_SPSLR,
and CONFIG_SAMPLE_SPSLR_TASKLIST can be enabled to build a simple
sanity-check module. Among other things, this module prints the values
of offsetof(task_struct, ...). After BPSLR has been applied
successfully, these offsets differ between boots.

With this configuration, the modified kernel successfully boots and
passes simple task_struct-related userspace tests. In addition, I have
successfully started an Ubuntu 24.04 userspace on top of BPSLR.

I have also attempted to port BPSLR to v6.18 and later releases. While I
got the system to build, occasional runtime failures currently prevent
successful operation.

I assume some kernel behaviors, access patterns, or subsystem
interactions present in newer kernels are not yet handled correctly by
the prototype. I am aware of but have not yet addressed BTF and I
suspect there are several other mechanisms that conflict with current
BPSLR assumptions and need to still be addressed separately.

I would greatly appreciate feedback regarding kernel subsystems,
debugging and tracing infrastructure, metadata consumers, or other
mechanisms that may make assumptions about structure layouts, field
offsets, or object access patterns.

In particular, I would be interested in pointers to areas that are
likely to require special handling, as well as assumptions made by the
kernel that may not be obvious to someone approaching the problem from
outside the subsystem in question.

As I am only now beginning the thesis work associated with this project,
I expect to continue active development over the next approximately six
months. Early feedback on architectural concerns, missing subsystem
support, and problematic assumptions would therefore be especially
valuable.

I look forward to your feedback.

York Jasper Niebuhr

York Jasper Niebuhr (7):
  SPSLR pinpoint plugin source
  SPSLR patchcompile cli source
  SPSLR tool build integration
  SPSLR selfpatch
  BPSLR task_struct integration
  BPSLR tasklist sample module
  Ignore BPSLR generated files

 .gitignore                                    |   7 +
 Makefile                                      | 131 +++++
 arch/x86/boot/compressed/Makefile             |   2 +
 arch/x86/kernel/vmlinux.lds.S                 |   8 +
 drivers/firmware/efi/libstub/Makefile         |   2 +
 include/linux/compiler_types.h                |  12 +
 include/linux/sched.h                         |  50 +-
 include/linux/spslr.h                         |  48 ++
 init/Kconfig                                  |   7 +
 init/main.c                                   |  15 +
 kernel/Makefile                               |   2 +
 kernel/module/main.c                          |  62 ++
 kernel/spslr/Makefile                         |   5 +
 kernel/spslr/spslr.c                          | 267 +++++++++
 kernel/spslr/spslr_env.c                      |  74 +++
 kernel/spslr/spslr_env.h                      |  42 ++
 kernel/spslr/spslr_list.h                     |  63 +++
 kernel/spslr/spslr_list_link.h                |  21 +
 kernel/spslr/spslr_randomizer.c               | 382 +++++++++++++
 kernel/spslr/spslr_randomizer.h               |  26 +
 samples/Kconfig                               |   3 +
 samples/Makefile                              |   1 +
 samples/spslr/Kconfig                         |  17 +
 samples/spslr/Makefile                        |   1 +
 samples/spslr/tasklist/Makefile               |   1 +
 samples/spslr/tasklist/tasklist.c             |  68 +++
 scripts/Makefile.build                        |   9 +-
 scripts/Makefile.modfinal                     | 131 ++++-
 scripts/Makefile.vmlinux                      |   4 +
 scripts/Makefile.vmlinux_o                    |   1 +
 scripts/link-vmlinux.sh                       |   5 +
 tools/Makefile                                |  15 +-
 tools/spslr/Makefile                          |  78 +++
 tools/spslr/src/patchcompile/accumulation.cpp | 529 ++++++++++++++++++
 tools/spslr/src/patchcompile/accumulation.h   |  78 +++
 tools/spslr/src/patchcompile/emit.cpp         | 451 +++++++++++++++
 tools/spslr/src/patchcompile/emit.h           |   4 +
 tools/spslr/src/patchcompile/patchcompile.cpp | 167 ++++++
 .../src/patchcompile/patchcompile_error.h     |  41 ++
 tools/spslr/src/patchcompile/spslr_list.h     |  13 +
 tools/spslr/src/pinpoint/final/final.h        |  13 +
 .../src/pinpoint/final/on_finish_unit.cpp     | 190 +++++++
 tools/spslr/src/pinpoint/pinpoint.cpp         | 128 +++++
 tools/spslr/src/pinpoint/pinpoint_config.h    |   9 +
 tools/spslr/src/pinpoint/pinpoint_error.h     |  27 +
 .../spslr/src/pinpoint/safegcc/safe-attribs.h |   8 +
 .../src/pinpoint/safegcc/safe-diagnostic.h    |   9 +
 .../src/pinpoint/safegcc/safe-gcc-plugin.h    |   6 +
 .../spslr/src/pinpoint/safegcc/safe-gimple.h  |  13 +
 tools/spslr/src/pinpoint/safegcc/safe-input.h |   9 +
 .../src/pinpoint/safegcc/safe-langhooks.h     |   8 +
 tools/spslr/src/pinpoint/safegcc/safe-md5.h   |   8 +
 .../spslr/src/pinpoint/safegcc/safe-output.h  |   8 +
 .../pinpoint/safegcc/safe-plugin-version.h    |   8 +
 tools/spslr/src/pinpoint/safegcc/safe-rtl.h   |  17 +
 tools/spslr/src/pinpoint/safegcc/safe-tree.h  |   9 +
 .../src/pinpoint/stage0/on_finish_decl.cpp    | 216 +++++++
 .../src/pinpoint/stage0/on_finish_type.cpp    |  16 +
 .../stage0/on_preserve_component_ref.cpp      |  58 ++
 .../stage0/on_register_attributes.cpp         |  52 ++
 .../src/pinpoint/stage0/on_start_unit.cpp     |  19 +
 .../pinpoint/stage0/separate_offset_pass.cpp  | 315 +++++++++++
 tools/spslr/src/pinpoint/stage0/separator.cpp | 117 ++++
 tools/spslr/src/pinpoint/stage0/stage0.h      | 103 ++++
 tools/spslr/src/pinpoint/stage0/target.cpp    | 411 ++++++++++++++
 .../src/pinpoint/stage1/asm_offset_pass.cpp   | 133 +++++
 tools/spslr/src/pinpoint/stage1/stage1.h      |  11 +
 .../pinpoint/stage2/rtl_pin_lower_pass.cpp    | 283 ++++++++++
 tools/spslr/src/pinpoint/stage2/stage2.h      |  22 +
 69 files changed, 5041 insertions(+), 28 deletions(-)
 create mode 100644 include/linux/spslr.h
 create mode 100644 kernel/spslr/Makefile
 create mode 100644 kernel/spslr/spslr.c
 create mode 100644 kernel/spslr/spslr_env.c
 create mode 100644 kernel/spslr/spslr_env.h
 create mode 100644 kernel/spslr/spslr_list.h
 create mode 100644 kernel/spslr/spslr_list_link.h
 create mode 100644 kernel/spslr/spslr_randomizer.c
 create mode 100644 kernel/spslr/spslr_randomizer.h
 create mode 100644 samples/spslr/Kconfig
 create mode 100644 samples/spslr/Makefile
 create mode 100644 samples/spslr/tasklist/Makefile
 create mode 100644 samples/spslr/tasklist/tasklist.c
 create mode 100644 tools/spslr/Makefile
 create mode 100644 tools/spslr/src/patchcompile/accumulation.cpp
 create mode 100644 tools/spslr/src/patchcompile/accumulation.h
 create mode 100644 tools/spslr/src/patchcompile/emit.cpp
 create mode 100644 tools/spslr/src/patchcompile/emit.h
 create mode 100644 tools/spslr/src/patchcompile/patchcompile.cpp
 create mode 100644 tools/spslr/src/patchcompile/patchcompile_error.h
 create mode 100644 tools/spslr/src/patchcompile/spslr_list.h
 create mode 100644 tools/spslr/src/pinpoint/final/final.h
 create mode 100644 tools/spslr/src/pinpoint/final/on_finish_unit.cpp
 create mode 100644 tools/spslr/src/pinpoint/pinpoint.cpp
 create mode 100644 tools/spslr/src/pinpoint/pinpoint_config.h
 create mode 100644 tools/spslr/src/pinpoint/pinpoint_error.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-attribs.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-diagnostic.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-gcc-plugin.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-gimple.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-input.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-langhooks.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-md5.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-output.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-plugin-version.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-rtl.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-tree.h
 create mode 100644 tools/spslr/src/pinpoint/stage0/on_finish_decl.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage0/on_finish_type.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage0/on_preserve_component_ref.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage0/on_register_attributes.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage0/on_start_unit.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage0/separate_offset_pass.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage0/separator.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage0/stage0.h
 create mode 100644 tools/spslr/src/pinpoint/stage0/target.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage1/asm_offset_pass.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage1/stage1.h
 create mode 100644 tools/spslr/src/pinpoint/stage2/rtl_pin_lower_pass.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage2/stage2.h

-- 
2.43.0


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [RFC 1/7] SPSLR pinpoint plugin source
  2026-06-05 20:25 [RFC 0/7] Bootpatch-SLR: Randomizing Linux Kernel Structure Layouts at Boot York Jasper Niebuhr
@ 2026-06-05 20:25 ` York Jasper Niebuhr
  2026-06-05 20:25 ` [RFC 2/7] SPSLR patchcompile cli source York Jasper Niebuhr
                   ` (7 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: York Jasper Niebuhr @ 2026-06-05 20:25 UTC (permalink / raw)
  To: linux-hardening; +Cc: kees, franzen

Signed-off-by: York Jasper Niebuhr <yjn@yjn-systems.com>
---
 tools/spslr/src/pinpoint/final/final.h        |  13 +
 .../src/pinpoint/final/on_finish_unit.cpp     | 190 ++++++++
 tools/spslr/src/pinpoint/pinpoint.cpp         | 128 ++++++
 tools/spslr/src/pinpoint/pinpoint_config.h    |   9 +
 tools/spslr/src/pinpoint/pinpoint_error.h     |  27 ++
 .../spslr/src/pinpoint/safegcc/safe-attribs.h |   8 +
 .../src/pinpoint/safegcc/safe-diagnostic.h    |   9 +
 .../src/pinpoint/safegcc/safe-gcc-plugin.h    |   6 +
 .../spslr/src/pinpoint/safegcc/safe-gimple.h  |  13 +
 tools/spslr/src/pinpoint/safegcc/safe-input.h |   9 +
 .../src/pinpoint/safegcc/safe-langhooks.h     |   8 +
 tools/spslr/src/pinpoint/safegcc/safe-md5.h   |   8 +
 .../spslr/src/pinpoint/safegcc/safe-output.h  |   8 +
 .../pinpoint/safegcc/safe-plugin-version.h    |   8 +
 tools/spslr/src/pinpoint/safegcc/safe-rtl.h   |  17 +
 tools/spslr/src/pinpoint/safegcc/safe-tree.h  |   9 +
 .../src/pinpoint/stage0/on_finish_decl.cpp    | 216 +++++++++
 .../src/pinpoint/stage0/on_finish_type.cpp    |  16 +
 .../stage0/on_preserve_component_ref.cpp      |  58 +++
 .../stage0/on_register_attributes.cpp         |  52 +++
 .../src/pinpoint/stage0/on_start_unit.cpp     |  19 +
 .../pinpoint/stage0/separate_offset_pass.cpp  | 315 ++++++++++++++
 tools/spslr/src/pinpoint/stage0/separator.cpp | 117 +++++
 tools/spslr/src/pinpoint/stage0/stage0.h      | 103 +++++
 tools/spslr/src/pinpoint/stage0/target.cpp    | 411 ++++++++++++++++++
 .../src/pinpoint/stage1/asm_offset_pass.cpp   | 133 ++++++
 tools/spslr/src/pinpoint/stage1/stage1.h      |  11 +
 .../pinpoint/stage2/rtl_pin_lower_pass.cpp    | 283 ++++++++++++
 tools/spslr/src/pinpoint/stage2/stage2.h      |  22 +
 29 files changed, 2226 insertions(+)
 create mode 100644 tools/spslr/src/pinpoint/final/final.h
 create mode 100644 tools/spslr/src/pinpoint/final/on_finish_unit.cpp
 create mode 100644 tools/spslr/src/pinpoint/pinpoint.cpp
 create mode 100644 tools/spslr/src/pinpoint/pinpoint_config.h
 create mode 100644 tools/spslr/src/pinpoint/pinpoint_error.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-attribs.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-diagnostic.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-gcc-plugin.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-gimple.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-input.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-langhooks.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-md5.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-output.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-plugin-version.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-rtl.h
 create mode 100644 tools/spslr/src/pinpoint/safegcc/safe-tree.h
 create mode 100644 tools/spslr/src/pinpoint/stage0/on_finish_decl.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage0/on_finish_type.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage0/on_preserve_component_ref.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage0/on_register_attributes.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage0/on_start_unit.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage0/separate_offset_pass.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage0/separator.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage0/stage0.h
 create mode 100644 tools/spslr/src/pinpoint/stage0/target.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage1/asm_offset_pass.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage1/stage1.h
 create mode 100644 tools/spslr/src/pinpoint/stage2/rtl_pin_lower_pass.cpp
 create mode 100644 tools/spslr/src/pinpoint/stage2/stage2.h

diff --git a/tools/spslr/src/pinpoint/final/final.h b/tools/spslr/src/pinpoint/final/final.h
new file mode 100644
index 000000000000..11c8af5dd17d
--- /dev/null
+++ b/tools/spslr/src/pinpoint/final/final.h
@@ -0,0 +1,13 @@
+#pragma once
+
+void on_finish_unit(void *plugin_data, void *user_data);
+
+void set_output_file(const char *path);
+bool has_output_file();
+const char *get_output_file();
+
+bool init_src_file();
+const char *get_src_file();
+
+bool init_cu_hash();
+const char *get_cu_hash();
diff --git a/tools/spslr/src/pinpoint/final/on_finish_unit.cpp b/tools/spslr/src/pinpoint/final/on_finish_unit.cpp
new file mode 100644
index 000000000000..754921da9dc5
--- /dev/null
+++ b/tools/spslr/src/pinpoint/final/on_finish_unit.cpp
@@ -0,0 +1,190 @@
+#include <final.h>
+#include <stage0.h>
+#include <stage2.h>
+#include <string>
+#include <filesystem>
+#include <fstream>
+#include <unordered_set>
+
+#include <safe-input.h>
+#include <safe-output.h>
+#include <safe-md5.h>
+#include <pinpoint_config.h>
+#include <pinpoint_error.h>
+
+/*
+ * Finish-unit emits the per-compilation-unit `.spslr` metadata consumed by
+ * patchcompile.
+ *
+ * Only information that survived all earlier filtering should be dumped here:
+ * target layouts, data pins for static objects, and stage2 instruction pins
+ * whose immediates exist in the final object.
+ */
+
+static bool output_file_known = false;
+static std::string src_file, output_file, cu_hash;
+
+void set_output_file(const char *path)
+{
+	namespace fs = std::filesystem;
+
+	if (!path) {
+		output_file_known = false;
+		return;
+	}
+
+	output_file = fs::weakly_canonical(path).generic_string();
+	output_file_known = true;
+}
+
+bool has_output_file()
+{
+	return output_file_known;
+}
+
+const char *get_output_file()
+{
+	return output_file_known ? output_file.c_str() : nullptr;
+}
+
+bool init_src_file()
+{
+	namespace fs = std::filesystem;
+
+	if (!main_input_filename)
+		return false;
+
+	src_file = fs::weakly_canonical(fs::absolute(main_input_filename))
+			   .generic_string();
+	return true;
+}
+
+const char *get_src_file()
+{
+	return src_file.c_str();
+}
+
+bool init_cu_hash()
+{
+	if (!has_output_file())
+		return false;
+
+	unsigned char md5_digest[16];
+	const char *s = get_output_file();
+	md5_buffer(s, strlen(s), md5_digest);
+
+	char md5_digest_hex[33] = {};
+	for (int i = 0; i < 16; i++)
+		sprintf(md5_digest_hex + i * 2, "%02x", md5_digest[i]);
+
+	cu_hash = std::string{ md5_digest_hex };
+	return true;
+}
+
+const char *get_cu_hash()
+{
+	return cu_hash.c_str();
+}
+
+static void emit_dpin_alias_labels()
+{
+	for (const DataPin &dpin : DataPin::all()) {
+		if (dpin.pin_symbol.empty() || dpin.symbol.empty())
+			pinpoint_fatal(
+				"emit_dpin_alias_labels got incomplete data pin");
+
+		fprintf(asm_out_file, ".globl %s\n", dpin.pin_symbol.c_str());
+		fprintf(asm_out_file, ".set %s, %s\n", dpin.pin_symbol.c_str(),
+			dpin.symbol.c_str());
+	}
+}
+
+static std::ofstream open_spslr_output_file()
+{
+	namespace fs = std::filesystem;
+
+	fs::path meta_path{ get_output_file() };
+	fs::create_directories(meta_path.parent_path());
+
+	pinpoint_debug(
+		"dumping metadata for source file '%s' (CU hash: %s) to '%s'",
+		get_src_file(), get_cu_hash(), get_output_file());
+
+	std::ofstream out(meta_path);
+	if (!out)
+		pinpoint_fatal(
+			"open_spslr_output_file failed to open spslr dump file");
+
+	return std::move(out);
+}
+
+void on_finish_unit(void *plugin_data, void *user_data)
+{
+	std::string cu_uid = get_cu_hash();
+
+	// Emit globally unique data pin label for each static object to be randomized
+
+	emit_dpin_alias_labels();
+
+	// Dump all accumulated data to spslr file
+
+	std::ofstream out = open_spslr_output_file();
+
+	// Header associates data with compilation unit
+
+	out << "SPSLR " << get_src_file() << " " << cu_uid << std::endl;
+
+	// Construct set of all targets that are used by at least 1 ipin or dpin
+
+	std::unordered_set<UID> used_targets;
+
+	for (const DataPin &dpin : DataPin::all()) {
+		for (const DataPin::Component &c : dpin.components)
+			used_targets.insert(c.target);
+	}
+
+	for (const auto &[uid, ipin] : s2_pins())
+		used_targets.insert(ipin.target);
+
+	// Dump all USED target structs
+
+	for (const auto &[uid, target] : TargetType::all()) {
+		if (used_targets.find(uid) == used_targets.end())
+			continue;
+
+		// target <name> <local uid> <size> <field count>
+		out << "target " << target.name() << " " << uid << " "
+		    << target.size() << " " << target.fields().size()
+		    << std::endl;
+
+		if (!target.has_fields())
+			pinpoint_fatal(
+				"on_finish_unit encountered incomplete target type");
+
+		for (const auto &[off, field] : target.fields()) {
+			// f <offset> <size> <alignment> <flags>
+			out << "f " << field.offset << " " << field.size << " "
+			    << field.alignment << " " << field.flags
+			    << std::endl;
+		}
+	}
+
+	// Dump all data pins
+
+	for (const DataPin &dpin : DataPin::all()) {
+		for (const DataPin::Component &c : dpin.components) {
+			// dpin <symbol> <offset> <level> <target uid>
+			out << "dpin " << " " << dpin.pin_symbol << " "
+			    << c.offset << " " << c.level << " " << c.target
+			    << std::endl;
+		}
+	}
+
+	// Dump all instruction pins
+
+	for (const auto &[uid, ipin] : s2_pins()) {
+		// ipin <symbol> <target uid> <field offset> <immediate size>
+		out << "ipin " << ipin.symbol << " " << ipin.target << " "
+		    << ipin.offset << " " << ipin.imm_size << std::endl;
+	}
+}
diff --git a/tools/spslr/src/pinpoint/pinpoint.cpp b/tools/spslr/src/pinpoint/pinpoint.cpp
new file mode 100644
index 000000000000..13b77840c834
--- /dev/null
+++ b/tools/spslr/src/pinpoint/pinpoint.cpp
@@ -0,0 +1,128 @@
+#include <safe-gcc-plugin.h>
+#include <safe-plugin-version.h>
+#include <filesystem>
+#include <string>
+
+#include <stage0.h>
+#include <stage1.h>
+#include <stage2.h>
+#include <final.h>
+
+#include <pinpoint_error.h>
+
+int plugin_is_GPL_compatible;
+
+bool pinpoint_verbose_enabled;
+
+static const char *srcroot = nullptr;
+static const char *metadir = nullptr;
+static std::string computed_output_file; /* Must remain alive */
+
+static void set_legacy_output_file(void)
+{
+	if (has_output_file())
+		return;
+
+	if (!srcroot || !metadir)
+		return;
+
+	std::filesystem::path src = main_input_filename;
+	std::filesystem::path root = srcroot;
+
+	std::filesystem::path rel = std::filesystem::relative(src, root);
+	std::filesystem::path out = std::filesystem::path(metadir) / rel;
+
+	out += ".spslr";
+
+	computed_output_file = out.string();
+	set_output_file(computed_output_file.c_str());
+}
+
+int plugin_init(struct plugin_name_args *plugin_info,
+		struct plugin_gcc_version *version)
+{
+	if (!plugin_default_version_check(version, &gcc_version)) {
+		plugin_print_early_error(
+			"incompatible GCC/plugin versions: plugin built for GCC %s, loaded by GCC %s",
+			gcc_version.basever, version->basever);
+		return 1;
+	}
+
+	pinpoint_verbose_enabled = false;
+
+	for (int i = 0; i < plugin_info->argc; ++i) {
+		if (!strcmp(plugin_info->argv[i].key, "out")) {
+			set_output_file(plugin_info->argv[i].value);
+		} else if (!strcmp(plugin_info->argv[i].key, "metadir")) {
+			metadir = plugin_info->argv[i].value;
+		} else if (!strcmp(plugin_info->argv[i].key, "srcroot")) {
+			srcroot = plugin_info->argv[i].value;
+		} else if (!strcmp(plugin_info->argv[i].key, "verbose")) {
+			pinpoint_verbose_enabled = true;
+		}
+	}
+
+	set_legacy_output_file();
+
+	if (!has_output_file()) {
+		plugin_print_early_error(
+			"missing output file argument: use either out=<file> or metadir=<dir> plus srcroot=<dir>");
+		return 1;
+	}
+
+	/*
+	 * Pinpoint runs as a staged GCC plugin because no single GCC IR level has
+	 * all information SPSLR needs.
+	 *
+	 * Stage 0 runs while COMPONENT_REF trees are built or still available and records
+	 * which structure field offsets are randomization-sensitive.
+	 *
+	 * Stage 1 replaces synthetic separator calls with asm markers so GCC keeps
+	 * the offset value as a real data dependency through later optimization.
+	 *
+	 * Stage 2 runs on final RTL and lowers those markers into concrete architecture-specific
+	 * instructions whose immediate operands are labeled for runtime patching.
+	 *
+	 * The final callback emits the collected metadata for patchcompile.
+	 */
+
+	register_callback(plugin_info->base_name, PLUGIN_START_UNIT,
+			  on_start_unit, NULL);
+	register_callback(plugin_info->base_name, PLUGIN_ATTRIBUTES,
+			  on_register_attributes, NULL);
+	register_callback(plugin_info->base_name, PLUGIN_FINISH_TYPE,
+			  on_finish_type, NULL);
+	register_callback(plugin_info->base_name, PLUGIN_BUILD_COMPONENT_REF,
+			  on_preserve_component_ref, NULL);
+	register_callback(plugin_info->base_name, PLUGIN_FINISH_DECL,
+			  on_finish_decl, NULL);
+
+	struct register_pass_info separate_offset_pass_info;
+	separate_offset_pass_info.pass = new separate_offset_pass(nullptr);
+	separate_offset_pass_info.ref_pass_instance_number = 1;
+	separate_offset_pass_info.reference_pass_name = "cfg";
+	separate_offset_pass_info.pos_op = PASS_POS_INSERT_AFTER;
+	register_callback(plugin_info->base_name, PLUGIN_PASS_MANAGER_SETUP,
+			  nullptr, &separate_offset_pass_info);
+
+	struct register_pass_info asm_offset_pass_info;
+	asm_offset_pass_info.pass = new asm_offset_pass(nullptr);
+	asm_offset_pass_info.ref_pass_instance_number = 1;
+	asm_offset_pass_info.reference_pass_name = "separate_offset";
+	asm_offset_pass_info.pos_op = PASS_POS_INSERT_AFTER;
+	register_callback(plugin_info->base_name, PLUGIN_PASS_MANAGER_SETUP,
+			  nullptr, &asm_offset_pass_info);
+
+	struct register_pass_info rtl_pin_lower_pass_info;
+	rtl_pin_lower_pass_info.pass = new rtl_pin_lower_pass(nullptr);
+	rtl_pin_lower_pass_info.ref_pass_instance_number = 1;
+	rtl_pin_lower_pass_info.reference_pass_name = "final";
+	rtl_pin_lower_pass_info.pos_op = PASS_POS_INSERT_BEFORE;
+	register_callback(plugin_info->base_name, PLUGIN_PASS_MANAGER_SETUP,
+			  nullptr, &rtl_pin_lower_pass_info);
+
+	register_callback(plugin_info->base_name, PLUGIN_FINISH_UNIT,
+			  on_finish_unit, NULL);
+
+	return 0;
+}
diff --git a/tools/spslr/src/pinpoint/pinpoint_config.h b/tools/spslr/src/pinpoint/pinpoint_config.h
new file mode 100644
index 000000000000..0d590e30e21d
--- /dev/null
+++ b/tools/spslr/src/pinpoint/pinpoint_config.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#define SPSLR_ATTRIBUTE "spslr"
+#define SPSLR_FIELD_FIXED_ATTRIBUTE "spslr_field_fixed"
+#define SPSLR_PINPOINT_STAGE0_SEPARATOR "__spslr_offsetof"
+#define SPSLR_PINFILE_EXTENSION ".spslr"
+#define SPSLR_PINPOINT_STAGE2_PIN \
+	"__spslr_ipin_" /* suffixed with "<cuhash>_<uid>" */
+#define SPSLR_PINPOINT_DPIN "__spslr_dpin_" /* suffixed with "<cuhash>_<uid>" */
diff --git a/tools/spslr/src/pinpoint/pinpoint_error.h b/tools/spslr/src/pinpoint/pinpoint_error.h
new file mode 100644
index 000000000000..3c8c994effc2
--- /dev/null
+++ b/tools/spslr/src/pinpoint/pinpoint_error.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <cstdio>
+#include <safe-diagnostic.h>
+
+extern bool pinpoint_verbose_enabled;
+
+#define plugin_print_early_error(fmt, ...)                                \
+	do {                                                              \
+		std::fprintf(stderr, "[spslr::pinpoint] error: " fmt "\n", \
+			     ##__VA_ARGS__);                              \
+	} while (0)
+
+#define pinpoint_debug_loc(loc, fmt, ...)                                      \
+	do {                                                                   \
+		if (pinpoint_verbose_enabled)                                  \
+			inform((loc), "[spslr::pinpoint] " fmt, ##__VA_ARGS__); \
+	} while (0)
+
+#define pinpoint_debug(fmt, ...) \
+	pinpoint_debug_loc(UNKNOWN_LOCATION, fmt, ##__VA_ARGS__)
+
+#define pinpoint_fatal_loc(loc, fmt, ...) \
+	fatal_error((loc), "[spslr::pinpoint] " fmt, ##__VA_ARGS__)
+
+#define pinpoint_fatal(fmt, ...) \
+	pinpoint_fatal_loc(UNKNOWN_LOCATION, fmt, ##__VA_ARGS__)
diff --git a/tools/spslr/src/pinpoint/safegcc/safe-attribs.h b/tools/spslr/src/pinpoint/safegcc/safe-attribs.h
new file mode 100644
index 000000000000..0881abc2598d
--- /dev/null
+++ b/tools/spslr/src/pinpoint/safegcc/safe-attribs.h
@@ -0,0 +1,8 @@
+#include <safe-gcc-plugin.h>
+
+#ifndef SAFEGCC_ATTRIBS_H
+#define SAFEGCC_ATTRIBS_H
+
+#include <attribs.h>
+
+#endif
diff --git a/tools/spslr/src/pinpoint/safegcc/safe-diagnostic.h b/tools/spslr/src/pinpoint/safegcc/safe-diagnostic.h
new file mode 100644
index 000000000000..3577020c880b
--- /dev/null
+++ b/tools/spslr/src/pinpoint/safegcc/safe-diagnostic.h
@@ -0,0 +1,9 @@
+#include <safe-gcc-plugin.h>
+
+#ifndef SAFEGCC_DIAGNOSTIC_H
+#define SAFEGCC_DIAGNOSTIC_H
+
+#include <diagnostic.h>
+#include <diagnostic-core.h>
+
+#endif
diff --git a/tools/spslr/src/pinpoint/safegcc/safe-gcc-plugin.h b/tools/spslr/src/pinpoint/safegcc/safe-gcc-plugin.h
new file mode 100644
index 000000000000..fcd111636a1c
--- /dev/null
+++ b/tools/spslr/src/pinpoint/safegcc/safe-gcc-plugin.h
@@ -0,0 +1,6 @@
+#ifndef SAFEGCC_GCC_PLUGIN_H
+#define SAFEGCC_GCC_PLUGIN_H
+
+#include <gcc-plugin.h>
+
+#endif
diff --git a/tools/spslr/src/pinpoint/safegcc/safe-gimple.h b/tools/spslr/src/pinpoint/safegcc/safe-gimple.h
new file mode 100644
index 000000000000..20d7edd8eb5c
--- /dev/null
+++ b/tools/spslr/src/pinpoint/safegcc/safe-gimple.h
@@ -0,0 +1,13 @@
+#include <safe-gcc-plugin.h>
+#include <safe-tree.h>
+
+#ifndef SAFEGCC_GIMPLE_H
+#define SAFEGCC_GIMPLE_H
+
+#include <gimple.h>
+#include <gimplify.h>
+#include <gimple-iterator.h>
+#include <gimple-pretty-print.h>
+#include <ssa.h>
+
+#endif
diff --git a/tools/spslr/src/pinpoint/safegcc/safe-input.h b/tools/spslr/src/pinpoint/safegcc/safe-input.h
new file mode 100644
index 000000000000..fe24435830fe
--- /dev/null
+++ b/tools/spslr/src/pinpoint/safegcc/safe-input.h
@@ -0,0 +1,9 @@
+#include <safe-gcc-plugin.h>
+
+#ifndef SAFEGCC_INPUT_H
+#define SAFEGCC_INPUT_H
+
+#include <input.h>
+#include <libiberty.h>
+
+#endif
diff --git a/tools/spslr/src/pinpoint/safegcc/safe-langhooks.h b/tools/spslr/src/pinpoint/safegcc/safe-langhooks.h
new file mode 100644
index 000000000000..3fbea6ccb579
--- /dev/null
+++ b/tools/spslr/src/pinpoint/safegcc/safe-langhooks.h
@@ -0,0 +1,8 @@
+#include <safe-gcc-plugin.h>
+
+#ifndef SAFEGCC_LANGHOOKS_H
+#define SAFEGCC_LANGHOOKS_H
+
+#include <langhooks.h>
+
+#endif
diff --git a/tools/spslr/src/pinpoint/safegcc/safe-md5.h b/tools/spslr/src/pinpoint/safegcc/safe-md5.h
new file mode 100644
index 000000000000..8341cb193467
--- /dev/null
+++ b/tools/spslr/src/pinpoint/safegcc/safe-md5.h
@@ -0,0 +1,8 @@
+#include <safe-gcc-plugin.h>
+
+#ifndef SAFEGCC_MD5_H
+#define SAFEGCC_MD5_H
+
+#include <md5.h>
+
+#endif
diff --git a/tools/spslr/src/pinpoint/safegcc/safe-output.h b/tools/spslr/src/pinpoint/safegcc/safe-output.h
new file mode 100644
index 000000000000..5da7fec494be
--- /dev/null
+++ b/tools/spslr/src/pinpoint/safegcc/safe-output.h
@@ -0,0 +1,8 @@
+#include <safe-gcc-plugin.h>
+
+#ifndef SAFEGCC_OUTPUT_H
+#define SAFEGCC_OUTPUT_H
+
+#include <output.h>
+
+#endif
diff --git a/tools/spslr/src/pinpoint/safegcc/safe-plugin-version.h b/tools/spslr/src/pinpoint/safegcc/safe-plugin-version.h
new file mode 100644
index 000000000000..e43a1689da8c
--- /dev/null
+++ b/tools/spslr/src/pinpoint/safegcc/safe-plugin-version.h
@@ -0,0 +1,8 @@
+#include <safe-gcc-plugin.h>
+
+#ifndef SAFEGCC_PLUGIN_VERSION_H
+#define SAFEGCC_PLUGIN_VERSION_H
+
+#include <plugin-version.h>
+
+#endif
diff --git a/tools/spslr/src/pinpoint/safegcc/safe-rtl.h b/tools/spslr/src/pinpoint/safegcc/safe-rtl.h
new file mode 100644
index 000000000000..f89decdb1d18
--- /dev/null
+++ b/tools/spslr/src/pinpoint/safegcc/safe-rtl.h
@@ -0,0 +1,17 @@
+#include <safe-gcc-plugin.h>
+
+#ifndef SAFEGCC_RTL_H
+#define SAFEGCC_RTL_H
+
+#include <rtl.h>
+#include <rtl-iter.h>
+#include <memmodel.h>
+#include <emit-rtl.h>
+#include <function.h>
+#include <expr.h>
+#include <hard-reg-set.h>
+
+#undef toupper
+#undef tolower
+
+#endif
diff --git a/tools/spslr/src/pinpoint/safegcc/safe-tree.h b/tools/spslr/src/pinpoint/safegcc/safe-tree.h
new file mode 100644
index 000000000000..b2c0625bb1aa
--- /dev/null
+++ b/tools/spslr/src/pinpoint/safegcc/safe-tree.h
@@ -0,0 +1,9 @@
+#include <safe-gcc-plugin.h>
+
+#ifndef SAFEGCC_TREE_H
+#define SAFEGCC_TREE_H
+
+#include <tree.h>
+#include <tree-pass.h>
+
+#endif
diff --git a/tools/spslr/src/pinpoint/stage0/on_finish_decl.cpp b/tools/spslr/src/pinpoint/stage0/on_finish_decl.cpp
new file mode 100644
index 000000000000..871ba5da070d
--- /dev/null
+++ b/tools/spslr/src/pinpoint/stage0/on_finish_decl.cpp
@@ -0,0 +1,216 @@
+#include <stage0.h>
+#include <final.h>
+
+#include <unordered_set>
+#include <string>
+
+#include <pinpoint_error.h>
+#include <pinpoint_config.h>
+
+static UID next_dpin_uid = 0;
+static std::list<DataPin> pins;
+static std::unordered_set<std::string> seen_dpin_symbols;
+
+void DataPin::reset()
+{
+	pins.clear();
+	next_dpin_uid = 0;
+}
+
+const std::list<DataPin> &DataPin::all()
+{
+	return pins;
+}
+
+static std::string make_dpin_symbol()
+{
+	return std::string(SPSLR_PINPOINT_DPIN) + std::string(get_cu_hash()) +
+	       "_" + std::to_string(next_dpin_uid++);
+}
+
+static std::list<DataPin::Component>
+compile_datapin_record_components(tree type);
+static std::list<DataPin::Component>
+compile_datapin_array_components(tree type);
+
+/*
+ * Build the list of randomized objects contained in a static object.
+ *
+ * A dpin may describe the object itself, nested target structs, arrays of
+ * targets, or targets embedded inside non-target containers. The level field
+ * records nesting depth so patchcompile/runtime can patch inner objects before
+ * their containing objects.
+ */
+
+static std::list<DataPin::Component> compile_datapin_components(tree type)
+{
+	std::list<DataPin::Component> components;
+
+	const TargetType *relevant = TargetType::find(type);
+	if (relevant) {
+		components.push_back(DataPin::Component{
+			.offset = 0, .level = 0, .target = relevant->uid() });
+	}
+
+	std::list<DataPin::Component> sub_components;
+	if (TREE_CODE(type) == RECORD_TYPE) {
+		sub_components = compile_datapin_record_components(type);
+	} else if (TREE_CODE(type) == ARRAY_TYPE) {
+		sub_components = compile_datapin_array_components(type);
+	}
+
+	for (const DataPin::Component &sc : sub_components) {
+		components.push_back(DataPin::Component{ .offset = sc.offset,
+							 .level = sc.level + 1,
+							 .target = sc.target });
+	}
+
+	// Note -> should probably make sure that randomized structs are never used in unions!
+	return components;
+}
+
+std::list<DataPin::Component> compile_datapin_record_components(tree type)
+{
+	std::list<DataPin::Component> components;
+
+	if (TREE_CODE(type) != RECORD_TYPE)
+		return components;
+
+	if (!COMPLETE_TYPE_P(type))
+		return components;
+
+	for (tree field = TYPE_FIELDS(type); field; field = TREE_CHAIN(field)) {
+		if (TREE_CODE(field) != FIELD_DECL)
+			continue;
+
+		std::size_t field_offset;
+		bool field_bitfield;
+
+		if (!field_info(field, &field_offset, nullptr, nullptr,
+				&field_bitfield)) {
+			// pinpoint_fatal("compile_datapin_record_components failed to get field info");
+			continue; // NOTE -> Happens e.g. on trailing arrays of dynamic size (allowed only in non-target structs)
+		}
+
+		if (field_bitfield)
+			continue;
+
+		tree field_type = TREE_TYPE(field);
+
+		std::list<DataPin::Component> field_components =
+			compile_datapin_components(field_type);
+		for (const DataPin::Component &fc : field_components) {
+			components.push_back(DataPin::Component{
+				.offset = field_offset + fc.offset,
+				.level = fc.level,
+				.target = fc.target });
+		}
+	}
+
+	return components;
+}
+
+std::list<DataPin::Component> compile_datapin_array_components(tree type)
+{
+	std::list<DataPin::Component> components;
+
+	if (TREE_CODE(type) != ARRAY_TYPE)
+		return components;
+
+	tree elem_type = TREE_TYPE(type);
+	if (!elem_type)
+		return components;
+
+	std::list<DataPin::Component> elem_components =
+		compile_datapin_components(elem_type);
+	if (elem_components.empty())
+		return components;
+
+	tree domain = TYPE_DOMAIN(type);
+	if (!domain)
+		pinpoint_fatal(
+			"compile_datapin_array_components failed to get domain for relevant element type");
+
+	tree min_t = TYPE_MIN_VALUE(domain);
+	tree max_t = TYPE_MAX_VALUE(domain);
+	if (!min_t || !max_t || TREE_CODE(min_t) != INTEGER_CST ||
+	    TREE_CODE(max_t) != INTEGER_CST)
+		pinpoint_fatal(
+			"compile_datapin_array_components failed to get array dimensions for relevant element type");
+
+	HOST_WIDE_INT min_i = tree_to_shwi(min_t);
+	HOST_WIDE_INT max_i = tree_to_shwi(max_t);
+
+	tree elem_size_t = TYPE_SIZE_UNIT(elem_type);
+	if (!elem_size_t || TREE_CODE(elem_size_t) != INTEGER_CST)
+		pinpoint_fatal(
+			"compile_datapin_array_components failed to get constant element size for relevant element type");
+
+	std::size_t elem_size = tree_to_uhwi(elem_size_t);
+
+	for (HOST_WIDE_INT i = min_i; i <= max_i; ++i) {
+		std::size_t element_offset =
+			(std::size_t)(i - min_i) * elem_size;
+
+		for (const DataPin::Component &ec : elem_components) {
+			components.push_back(DataPin::Component{
+				.offset = element_offset + ec.offset,
+				.level = ec.level,
+				.target = ec.target });
+		}
+	}
+
+	return components;
+}
+
+static bool compile_datapin(tree type, DataPin &pin)
+{
+	pin.components = compile_datapin_components(type);
+	return !pin.components.empty();
+}
+
+static void on_static_var(tree var)
+{
+	tree type = TREE_TYPE(var);
+	if (!type)
+		return;
+
+	tree symbol_tree = DECL_ASSEMBLER_NAME(var);
+	const char *symbol;
+	if (!symbol_tree || !(symbol = IDENTIFIER_POINTER(symbol_tree)))
+		pinpoint_fatal(
+			"on_static_var failed to get symbol of static variable");
+
+	std::string sym{ symbol };
+
+	/*
+	 * Multiple VAR_DECLs can name the same emitted object, for example through
+	 * export or alias machinery. Emit at most one dpin per assembler symbol.
+	 */
+	if (!seen_dpin_symbols.insert(sym).second)
+		return;
+
+	DataPin pin;
+	if (!compile_datapin(type, pin))
+		return;
+
+	DECL_PRESERVE_P(var) = 1;
+	// pin.global = static_cast<bool>(TREE_PUBLIC(var));
+
+	pin.symbol = sym;
+	pin.pin_symbol = make_dpin_symbol();
+	pins.emplace_back(std::move(pin));
+}
+
+void on_finish_decl(void *plugin_data, void *user_data)
+{
+	tree decl = (tree)plugin_data;
+
+	if (TREE_CODE(decl) != VAR_DECL)
+		return;
+
+	if (!TREE_STATIC(decl) || DECL_EXTERNAL(decl))
+		return;
+
+	on_static_var(decl);
+}
diff --git a/tools/spslr/src/pinpoint/stage0/on_finish_type.cpp b/tools/spslr/src/pinpoint/stage0/on_finish_type.cpp
new file mode 100644
index 000000000000..f2459be7242c
--- /dev/null
+++ b/tools/spslr/src/pinpoint/stage0/on_finish_type.cpp
@@ -0,0 +1,16 @@
+#include <stage0.h>
+#include <pinpoint_error.h>
+
+void on_finish_type(void *plugin_data, void *user_data)
+{
+	tree t = (tree)plugin_data;
+
+	TargetType *type = TargetType::find_mutable(t);
+	if (!type)
+		return;
+
+	if (!type->fetch_fields())
+		pinpoint_fatal(
+			"on_finish_type failed to fetch fields of target \"%s\"",
+			type->name().c_str());
+}
diff --git a/tools/spslr/src/pinpoint/stage0/on_preserve_component_ref.cpp b/tools/spslr/src/pinpoint/stage0/on_preserve_component_ref.cpp
new file mode 100644
index 000000000000..f02d49fbe218
--- /dev/null
+++ b/tools/spslr/src/pinpoint/stage0/on_preserve_component_ref.cpp
@@ -0,0 +1,58 @@
+#include <stage0.h>
+#include <pinpoint_error.h>
+
+/*
+ * Preserve an early COMPONENT_REF before GCC folds it into a plain constant
+ * offset.
+ *
+ * The custom GCC hook calls this while the frontend still knows that an
+ * expression is "base.field". We rewrite it into pointer arithmetic whose
+ * offset comes from a synthetic separator call:
+ *
+ *     base.field  ->  *(typeof(field) *)((char *)&base + separator(target, off))
+ *
+ * Later passes replace the separator with an instruction pin.
+ */
+
+static tree ast_separate_offset(tree ref, UID target, std::size_t offset)
+{
+	tree separator = make_stage0_ast_separator(target, offset);
+	if (!separator)
+		pinpoint_fatal(
+			"ast_separate_offset failed to generate AST separator for target %u at offset %u",
+			(unsigned)target, (unsigned)offset);
+
+	tree base = TREE_OPERAND(ref, 0);
+
+	// ADDR_EXPR can never be a valid base, but such trees can happen during parsing before checks
+	if (TREE_CODE(base) == ADDR_EXPR)
+		pinpoint_fatal(
+			"ast_separate_offset encountered ADDR_EXPR as COMPONENT_REF base");
+
+	tree base_ptr = build_fold_addr_expr(base);
+
+	tree field_type = TREE_TYPE(
+		ref); // Type of COMPONENT_REF is type of the accessed field
+	tree field_ptr_type = build_pointer_type(field_type);
+	tree field_ptr =
+		build2(POINTER_PLUS_EXPR, field_ptr_type, base_ptr, separator);
+
+	tree field_ref = build1(INDIRECT_REF, field_type, field_ptr);
+	return field_ref;
+}
+
+void on_preserve_component_ref(void *plugin_data, void *user_data)
+{
+	tree *ref = (tree *)plugin_data;
+	if (!ref)
+		return;
+
+	UID target;
+	std::size_t offset;
+	if (!TargetType::reference(*ref, target, offset))
+		return;
+
+	tree separated = ast_separate_offset(*ref, target, offset);
+	if (separated)
+		*ref = separated;
+}
diff --git a/tools/spslr/src/pinpoint/stage0/on_register_attributes.cpp b/tools/spslr/src/pinpoint/stage0/on_register_attributes.cpp
new file mode 100644
index 000000000000..7720d18022e3
--- /dev/null
+++ b/tools/spslr/src/pinpoint/stage0/on_register_attributes.cpp
@@ -0,0 +1,52 @@
+#include <stage0.h>
+#include <pinpoint_config.h>
+#include <cstdio>
+
+static tree log_new_target(tree *node, tree name, tree args, int flags,
+			   bool *no_add_attrs)
+{
+	if (node)
+		TargetType::add(*node);
+
+	return NULL_TREE;
+}
+
+static tree check_field_fixed_attribute(tree *node, tree name, tree args,
+					int flags, bool *no_add_attrs)
+{
+	if (!node || !*node || TREE_CODE(*node) != FIELD_DECL) {
+		*no_add_attrs = true;
+		fprintf(stderr,
+			"%qs attribute only applies to struct/union fields",
+			SPSLR_FIELD_FIXED_ATTRIBUTE);
+	}
+	return NULL_TREE;
+}
+
+/*
+ * __attribute__((spslr)) marks a record type as a randomization target.
+ * The attribute only registers the type here; fields are fetched later once
+ * GCC has completed the type.
+ */
+
+static struct attribute_spec spslr_attribute = {
+	SPSLR_ATTRIBUTE, 0, 0, false, false, false, false, log_new_target, NULL
+};
+
+/*
+ * __attribute__((spslr_field_fixed)) marks a field as layout-sensitive.
+ * Fixed fields remain part of the target, but are treated as dangerous so
+ * instruction/data pins are not generated for offsets that would become
+ * ambiguous after randomization.
+ */
+
+static struct attribute_spec spslr_fixed_field_attribute = {
+	SPSLR_FIELD_FIXED_ATTRIBUTE, 0,	  0, false, false, false, false,
+	check_field_fixed_attribute, NULL
+};
+
+void on_register_attributes(void *plugin_data, void *user_data)
+{
+	register_attribute(&spslr_attribute);
+	register_attribute(&spslr_fixed_field_attribute);
+}
diff --git a/tools/spslr/src/pinpoint/stage0/on_start_unit.cpp b/tools/spslr/src/pinpoint/stage0/on_start_unit.cpp
new file mode 100644
index 000000000000..a677557b330d
--- /dev/null
+++ b/tools/spslr/src/pinpoint/stage0/on_start_unit.cpp
@@ -0,0 +1,19 @@
+#include <stage0.h>
+#include <stage1.h>
+#include <stage2.h>
+#include <final.h>
+
+#include <pinpoint_error.h>
+
+void on_start_unit(void *plugin_data, void *user_data)
+{
+	TargetType::reset();
+	DataPin::reset();
+	s2_pins_reset();
+
+	if (!init_src_file())
+		pinpoint_fatal("failed to get source file name");
+
+	if (!init_cu_hash())
+		pinpoint_fatal("failed to initialize CU hash");
+}
diff --git a/tools/spslr/src/pinpoint/stage0/separate_offset_pass.cpp b/tools/spslr/src/pinpoint/stage0/separate_offset_pass.cpp
new file mode 100644
index 000000000000..6c9236705a6b
--- /dev/null
+++ b/tools/spslr/src/pinpoint/stage0/separate_offset_pass.cpp
@@ -0,0 +1,315 @@
+#include <stage0.h>
+#include <functional>
+#include <list>
+
+#include <pinpoint_error.h>
+
+/*
+ * AccessChain flattens nested COMPONENT_REF / ARRAY_REF expressions from
+ * outer base to inner access.
+ *
+ * This lets the pass rebuild the expression one step at a time while replacing
+ * only SPSLR-relevant field offsets with separator calls. Non-target offsets
+ * stay as ordinary constant pointer arithmetic.
+ */
+
+struct AccessChain {
+	struct Step {
+		enum Kind { STEP_COMPONENT, STEP_ARRAY } kind = STEP_COMPONENT;
+
+		tree t = NULL_TREE;
+
+		/* COMPONENT_REF data */
+		bool relevant = false;
+		UID target = UID_INVALID;
+		std::size_t offset = 0;
+	};
+
+	bool relevant = false;
+	std::list<Step> steps;
+	tree base = NULL_TREE;
+};
+
+static tree walk_tree_contains_component_ref(tree *tp, int *walk_subtrees,
+					     void *data)
+{
+	int *found_flag = (int *)data;
+
+	if (!tp || !*tp)
+		return NULL_TREE;
+
+	if (TREE_CODE(*tp) == COMPONENT_REF)
+		*found_flag = 1;
+
+	return NULL_TREE;
+}
+
+static bool tree_contains_component_ref(tree ref)
+{
+	int found_flag = 0;
+	walk_tree(&ref, walk_tree_contains_component_ref, &found_flag, NULL);
+	return found_flag != 0;
+}
+
+static bool access_chain(tree ref, AccessChain &chain)
+{
+	if (!ref)
+		return false;
+
+	switch (TREE_CODE(ref)) {
+	case COMPONENT_REF: {
+		AccessChain::Step step;
+		step.kind = AccessChain::Step::STEP_COMPONENT;
+		step.t = ref;
+		step.relevant =
+			TargetType::reference(ref, step.target, step.offset);
+		if (step.relevant)
+			chain.relevant = true;
+		chain.steps.push_front(step);
+		return access_chain(TREE_OPERAND(ref, 0), chain);
+	}
+
+	case ARRAY_REF:
+	case ARRAY_RANGE_REF: {
+		AccessChain::Step step;
+		step.kind = AccessChain::Step::STEP_ARRAY;
+		step.t = ref;
+		chain.steps.push_front(step);
+		return access_chain(TREE_OPERAND(ref, 0), chain);
+	}
+
+	default:
+		if (tree_contains_component_ref(ref))
+			return false;
+		chain.base = ref;
+		return true;
+	}
+}
+
+/*
+ * Rewrite a relevant field-access chain into explicit pointer arithmetic.
+ *
+ * The resulting MEM_REF has offset zero; all interesting byte offsets have
+ * either become separator calls or fixed constants. This makes the later asm
+ * marker pass independent of GCC's original COMPONENT_REF tree shape.
+ */
+
+static tree separate_offset_chain_maybe(tree ref, gimple_stmt_iterator *gsi)
+{
+	AccessChain chain;
+	if (!access_chain(ref, chain)) {
+		pinpoint_fatal(
+			"separate_offset_chain_maybe encountered invalid access chain: top=%s base=%s",
+			get_tree_code_name(TREE_CODE(ref)),
+			TREE_OPERAND(ref, 0) ? get_tree_code_name(TREE_CODE(
+						       TREE_OPERAND(ref, 0))) :
+					       "<null>");
+	}
+
+	if (!chain.relevant)
+		return NULL_TREE;
+
+	tree cur_expr = chain.base;
+
+	// NOTE -> Could fold into single call here (needs to track what offsets contribute, +irrelevant combined)
+
+	for (const AccessChain::Step &step : chain.steps) {
+		if (step.kind == AccessChain::Step::STEP_COMPONENT) {
+			if (TREE_CODE(cur_expr) == ADDR_EXPR)
+				pinpoint_fatal(
+					"separate_offset_chain_maybe encountered ADDR_EXPR as base of COMPONENT_REF");
+
+			tree cur_ptr = build_fold_addr_expr(cur_expr);
+
+			tree field_ptr_type =
+				build_pointer_type(TREE_TYPE(step.t));
+			tree field_ptr;
+
+			if (step.relevant) {
+				tree return_tmp =
+					create_tmp_var(size_type_node, NULL);
+				gimple *call_stmt = make_stage0_gimple_separator(
+					return_tmp, step.target, step.offset);
+				if (!call_stmt)
+					pinpoint_fatal(
+						"separate_offset_chain_maybe failed to make gimple separator");
+
+				gsi_insert_before(gsi, call_stmt,
+						  GSI_SAME_STMT);
+				field_ptr = build2(POINTER_PLUS_EXPR,
+						   field_ptr_type, cur_ptr,
+						   return_tmp);
+			} else {
+				tree field_decl = TREE_OPERAND(step.t, 1);
+
+				std::size_t field_offset;
+				bool field_bitfield;
+				if (!field_info(field_decl, &field_offset,
+						nullptr, nullptr,
+						&field_bitfield))
+					pinpoint_fatal(
+						"separate_offset_chain_maybe failed to get field info of non-target access");
+				if (field_bitfield)
+					pinpoint_fatal(
+						"separate_offset_chain_maybe encountered bitfield access in relevant COMPONENT_REF chain");
+
+				field_ptr = build2(POINTER_PLUS_EXPR,
+						   field_ptr_type, cur_ptr,
+						   build_int_cst(sizetype,
+								 field_offset));
+			}
+
+			tree ptr_tmp = create_tmp_var(field_ptr_type, NULL);
+			gimple *ptr_assign =
+				gimple_build_assign(ptr_tmp, field_ptr);
+			gsi_insert_before(gsi, ptr_assign, GSI_SAME_STMT);
+
+			// Dereference constructed pointer expression
+
+			tree offset0 = fold_convert(TREE_TYPE(ptr_tmp),
+						    build_int_cst(sizetype, 0));
+			cur_expr = build2(MEM_REF, TREE_TYPE(step.t), ptr_tmp,
+					  offset0);
+			continue;
+		}
+
+		if (step.kind == AccessChain::Step::STEP_ARRAY) {
+			tree idx = TREE_OPERAND(step.t, 1);
+			tree low = TREE_OPERAND(step.t, 2);
+			tree elts = TREE_OPERAND(step.t, 3);
+
+			cur_expr = build4(TREE_CODE(step.t), TREE_TYPE(step.t),
+					  cur_expr, idx, low, elts);
+			continue;
+		}
+	}
+
+	return cur_expr;
+}
+
+static void dispatch_separation_maybe(const std::list<tree *> &path,
+				      gimple_stmt_iterator *gsi,
+				      unsigned &cancel_levels)
+{
+	if (path.empty() || !gsi)
+		return;
+
+	tree ref = *path.back();
+	if (!ref || TREE_CODE(ref) != COMPONENT_REF)
+		return;
+
+	cancel_levels = 1;
+
+	tree instrumented_ref = separate_offset_chain_maybe(ref, gsi);
+	if (!instrumented_ref)
+		return;
+
+	gimple_set_modified(gsi_stmt(*gsi), true);
+	*path.back() = instrumented_ref;
+
+	// At this point, instrumented_ref is a MEM_REF node (off=0). A wrapping ADDR_EXPR cancels it out.
+
+	if (path.size() < 2)
+		return;
+
+	tree *parent = *(++path.rbegin());
+
+	if (TREE_CODE(*parent) == ADDR_EXPR) {
+		// Note -> the base of the MEM_REF is expected to have the same type as the ADDR_EXPR
+		*parent = TREE_OPERAND(instrumented_ref, 0);
+		cancel_levels++;
+	}
+}
+
+static const pass_data separate_offset_pass_data = {
+	GIMPLE_PASS, "separate_offset", OPTGROUP_NONE, TV_NONE, 0, 0, 0,
+	0,	     TODO_update_ssa
+};
+
+separate_offset_pass::separate_offset_pass(gcc::context *ctxt)
+	: gimple_opt_pass(separate_offset_pass_data, ctxt)
+{
+}
+
+struct TreeWalkData {
+	std::list<tree *> path;
+	gimple_stmt_iterator *gsi;
+	unsigned cancel_levels;
+	std::function<void(const std::list<tree *> &, gimple_stmt_iterator *,
+			   unsigned &)>
+		callback;
+};
+
+static tree walk_tree_level(tree *tp, int *walk_subtrees, void *data)
+{
+	TreeWalkData *twd = (TreeWalkData *)data;
+	if (!twd)
+		return NULL_TREE;
+
+	if (!twd->path.empty() && twd->path.back() == tp)
+		return NULL_TREE; // root of this level
+
+	if (walk_subtrees)
+		*walk_subtrees = 0;
+
+	twd->cancel_levels = 0;
+	twd->path.push_back(tp);
+
+	twd->callback(twd->path, twd->gsi, twd->cancel_levels);
+
+	if (twd->cancel_levels == 0)
+		walk_tree(tp, walk_tree_level, data, NULL);
+
+	twd->path.pop_back();
+
+	if (twd->cancel_levels > 0)
+		twd->cancel_levels--;
+
+	// Cancel current level if there are still cancel_levels due
+	return twd->cancel_levels == 0 ? NULL_TREE : *tp;
+}
+
+static bool
+walk_gimple_stmt(gimple_stmt_iterator *gsi,
+		 std::function<void(const std::list<tree *> &,
+				    gimple_stmt_iterator *, unsigned &)>
+			 callback)
+{
+	if (!gsi || gsi_end_p(*gsi) || !callback)
+		return false;
+
+	gimple *stmt = gsi_stmt(*gsi);
+
+	for (int i = 0; i < gimple_num_ops(stmt); i++) {
+		tree *op = gimple_op_ptr(stmt, i);
+		if (!op || !*op)
+			continue;
+
+		TreeWalkData twd;
+		twd.gsi = gsi;
+		twd.callback = callback;
+
+		walk_tree_level(op, NULL, &twd);
+	}
+
+	return true;
+}
+
+unsigned int separate_offset_pass::execute(function *fn)
+{
+	if (!fn)
+		return 0;
+
+	basic_block bb;
+	FOR_EACH_BB_FN(bb, fn)
+	{
+		for (gimple_stmt_iterator gsi = gsi_start_bb(bb);
+		     !gsi_end_p(gsi); gsi_next(&gsi)) {
+			if (!walk_gimple_stmt(&gsi, dispatch_separation_maybe))
+				pinpoint_fatal(
+					"separate_offset pass failed to walk gimple statement");
+		}
+	}
+
+	return 0;
+}
diff --git a/tools/spslr/src/pinpoint/stage0/separator.cpp b/tools/spslr/src/pinpoint/stage0/separator.cpp
new file mode 100644
index 000000000000..91f7838cf4e8
--- /dev/null
+++ b/tools/spslr/src/pinpoint/stage0/separator.cpp
@@ -0,0 +1,117 @@
+#include <stage0.h>
+#include <pinpoint_config.h>
+#include <pinpoint_error.h>
+
+static tree separator_decl = NULL_TREE;
+
+/*
+ * Stage0 separators are compiler-internal marker calls, not runtime calls.
+ *
+ * They carry two constants: target UID and original field offset. The call is
+ * declared pure/no-vops so GCC treats it as having no memory side effects; a
+ * later pinpoint pass must remove every separator before code generation.
+ */
+
+static tree make_separator_decl()
+{
+	if (separator_decl)
+		return separator_decl;
+
+	tree args = tree_cons(NULL_TREE, sizetype,
+			      tree_cons(NULL_TREE, sizetype, NULL_TREE));
+	tree type = build_function_type(sizetype, args);
+
+	tree tmp_decl = build_fn_decl(SPSLR_PINPOINT_STAGE0_SEPARATOR, type);
+	if (!tmp_decl)
+		return NULL_TREE;
+
+	DECL_EXTERNAL(tmp_decl) = 1;
+	TREE_PUBLIC(tmp_decl) = 1;
+	DECL_ARTIFICIAL(tmp_decl) = 1;
+
+	/* Prevent VOP problems later when removing calls (VOPs mark memory
+	   side-effects, which these calls have none of anyways */
+	DECL_PURE_P(tmp_decl) = 1;
+	DECL_IS_NOVOPS(tmp_decl) = 1;
+
+	return (separator_decl = tmp_decl);
+}
+
+tree make_stage0_ast_separator(UID target, std::size_t offset)
+{
+	tree decl = make_separator_decl();
+	if (!decl)
+		return NULL_TREE;
+
+	tree arg0 = size_int(target);
+	tree arg1 = size_int(offset);
+
+	if (!arg0 || !arg1)
+		return NULL_TREE;
+
+	return build_call_expr(decl, 2, arg0, arg1);
+}
+
+gimple *make_stage0_gimple_separator(tree lhs, UID target, std::size_t offset)
+{
+	if (!lhs)
+		return nullptr;
+
+	tree decl = make_separator_decl();
+	if (!decl)
+		return nullptr;
+
+	tree arg0 = size_int(target);
+	tree arg1 = size_int(offset);
+
+	if (!arg0 || !arg1)
+		return nullptr;
+
+	gimple *call = gimple_build_call(decl, 2, arg0, arg1);
+	if (!call)
+		return nullptr;
+
+	gimple_call_set_lhs(call, lhs);
+	return call;
+}
+
+static bool decl_is_separator(tree fndecl)
+{
+	if (!fndecl)
+		return false;
+
+	tree name_tree = DECL_NAME(fndecl);
+	if (!name_tree)
+		return false;
+
+	const char *name = IDENTIFIER_POINTER(name_tree);
+	if (!name)
+		return false;
+
+	return strcmp(name, SPSLR_PINPOINT_STAGE0_SEPARATOR) == 0;
+}
+
+bool is_stage0_separator(gimple *stmt, UID &target, std::size_t &offset)
+{
+	if (!stmt || !is_gimple_call(stmt))
+		return false;
+
+	tree fndecl = gimple_call_fndecl(stmt);
+	if (!decl_is_separator(fndecl))
+		return false;
+
+	tree arg0 = gimple_call_arg(stmt, 0);
+	tree arg1 = gimple_call_arg(stmt, 1);
+
+	if (!arg0 || TREE_CODE(arg0) != INTEGER_CST)
+		pinpoint_fatal(
+			"is_state0_separator failed to get target UID from separator");
+
+	if (!arg1 || TREE_CODE(arg1) != INTEGER_CST)
+		pinpoint_fatal(
+			"is_state0_separator failed to get field offset from separator");
+
+	target = static_cast<UID>(tree_to_uhwi(arg0));
+	offset = static_cast<std::size_t>(tree_to_uhwi(arg1));
+	return true;
+}
diff --git a/tools/spslr/src/pinpoint/stage0/stage0.h b/tools/spslr/src/pinpoint/stage0/stage0.h
new file mode 100644
index 000000000000..e9fb9447b4d0
--- /dev/null
+++ b/tools/spslr/src/pinpoint/stage0/stage0.h
@@ -0,0 +1,103 @@
+#pragma once
+#include <cstddef>
+#include <map>
+#include <unordered_map>
+#include <string>
+#include <list>
+#include <limits>
+
+#include <safe-tree.h>
+#include <safe-gimple.h>
+
+using UID = std::size_t;
+constexpr UID UID_INVALID = std::numeric_limits<UID>::max();
+
+class TargetType {
+    public:
+	struct Field {
+		static constexpr std::size_t FLAG_DANGEROUS = 1;
+
+		std::size_t offset;
+		std::size_t size;
+		std::size_t alignment;
+		std::size_t flags;
+	};
+
+    private:
+	static constexpr std::size_t FLAG_MAIN_VARIANT = 1;
+	static constexpr std::size_t FLAG_FIELDS = 2;
+
+	UID m_uid;
+	std::size_t m_flags;
+	tree m_main_variant;
+	std::size_t m_size;
+
+	// Fields are identified by their offsets
+	std::map<std::size_t, Field> m_fields;
+
+    public:
+	TargetType(const TargetType &other) = default;
+	TargetType &operator=(const TargetType &other) = default;
+	TargetType(TargetType &&other) = default;
+	TargetType &operator=(TargetType &&other) = default;
+
+	TargetType(tree t); // Does NOT fetch fields
+	~TargetType();
+
+	bool valid() const;
+	bool has_fields() const;
+	const std::map<std::size_t, Field> &fields() const;
+	std::string name() const;
+	const Field *field(std::size_t off, bool exact = true) const;
+	UID uid() const;
+	std::size_t size() const;
+
+	static void add(tree t);
+	static std::size_t count();
+	static const TargetType *find(tree t); // O(n)
+	static const TargetType *find(UID uid); // O(1)
+	static bool reference(tree ref, UID &target, std::size_t &offset);
+	static const std::unordered_map<UID, TargetType> &all();
+	static void reset();
+
+    private:
+	friend void on_finish_type(void *, void *);
+	bool fetch_fields(bool redo = false);
+	static TargetType *find_mutable(tree t);
+};
+
+bool field_info(tree field_decl, std::size_t *offset, std::size_t *size,
+		std::size_t *alignment, bool *bitfield);
+
+/* Stage 0 offsetof separators are function calls, such as:
+   SPSLR_PINPOINT_STAGE0_SEPARATOR(target, member offset) */
+
+tree make_stage0_ast_separator(UID target, std::size_t offset);
+gimple *make_stage0_gimple_separator(tree lhs, UID target, std::size_t offset);
+bool is_stage0_separator(gimple *stmt, UID &target, std::size_t &offset);
+
+struct DataPin {
+	struct Component {
+		std::size_t offset;
+		std::size_t level;
+		UID target;
+	};
+
+	std::string symbol; // potentially local object symbol
+	std::string pin_symbol; // global alias symbol
+	std::list<Component> components;
+
+	static void reset();
+	static const std::list<DataPin> &all();
+};
+
+void on_register_attributes(void *plugin_data, void *user_data);
+void on_finish_type(void *plugin_data, void *user_data);
+void on_preserve_component_ref(void *plugin_data, void *user_data);
+void on_finish_decl(void *plugin_data, void *user_data);
+void on_start_unit(void *plugin_data, void *user_data);
+
+struct separate_offset_pass : gimple_opt_pass {
+	separate_offset_pass(gcc::context *ctxt);
+	unsigned int execute(function *fn) override;
+};
diff --git a/tools/spslr/src/pinpoint/stage0/target.cpp b/tools/spslr/src/pinpoint/stage0/target.cpp
new file mode 100644
index 000000000000..7e40abf184f7
--- /dev/null
+++ b/tools/spslr/src/pinpoint/stage0/target.cpp
@@ -0,0 +1,411 @@
+#include <stage0.h>
+#include <functional>
+
+#include <safe-langhooks.h>
+#include <safe-attribs.h>
+
+#include <pinpoint_config.h>
+
+/*
+ * TargetType is the compiler-side model of an SPSLR-randomized record type.
+ *
+ * A target is keyed by GCC's main record variant, not by spelling alone, so
+ * typedefs and compatible variants resolve to the same SPSLR target. The UID
+ * is local to this compilation unit; patchcompile later merges compatible
+ * targets across objects.
+ */
+
+static UID next_uid = 0;
+static std::unordered_map<UID, TargetType> targets;
+
+static tree get_record_main_variant(tree t)
+{
+	if (!t || TREE_CODE(t) != RECORD_TYPE)
+		return NULL_TREE;
+
+	return TYPE_MAIN_VARIANT(t);
+}
+
+TargetType::TargetType(tree t)
+	: m_uid{ UID_INVALID }
+	, m_flags{ 0 }
+	, m_size{ 0 }
+{
+	if (!(m_main_variant = get_record_main_variant(t)))
+		return;
+
+	m_flags |= FLAG_MAIN_VARIANT;
+}
+
+TargetType::~TargetType()
+{
+}
+
+bool TargetType::valid() const
+{
+	return (m_flags & FLAG_MAIN_VARIANT) != 0;
+}
+
+bool TargetType::has_fields() const
+{
+	return (m_flags & FLAG_FIELDS) != 0;
+}
+
+const std::map<std::size_t, TargetType::Field> &TargetType::fields() const
+{
+	return m_fields;
+}
+
+std::string TargetType::name() const
+{
+	const char *error_name = "<error>";
+	const char *anonymous_name = "<anonymous>";
+
+	if (!valid())
+		return { error_name };
+
+	tree name_tree = TYPE_NAME(m_main_variant);
+	if (!name_tree)
+		return { anonymous_name };
+
+	if (TREE_CODE(name_tree) == TYPE_DECL && DECL_NAME(name_tree))
+		return { IDENTIFIER_POINTER(DECL_NAME(name_tree)) };
+	else if (TREE_CODE(name_tree) == IDENTIFIER_NODE)
+		return { IDENTIFIER_POINTER(name_tree) };
+
+	return { anonymous_name };
+}
+
+const TargetType::Field *TargetType::field(std::size_t off, bool exact) const
+{
+	if (!valid() || !(m_flags & FLAG_FIELDS))
+		return nullptr;
+
+	auto it = m_fields.upper_bound(off); // Next element
+	if (it == m_fields.begin())
+		return nullptr;
+
+	--it; // Element of interest
+
+	const TargetType::Field &maybe = it->second;
+
+	if (off >= maybe.offset + maybe.size)
+		return nullptr;
+
+	if (exact && maybe.offset != off)
+		return nullptr;
+
+	return &maybe;
+}
+
+UID TargetType::uid() const
+{
+	return m_uid;
+}
+
+std::size_t TargetType::size() const
+{
+	if (!valid() || !(m_flags & FLAG_FIELDS))
+		return 0;
+
+	return m_size;
+}
+
+void TargetType::add(tree t)
+{
+	if (find(t) != nullptr)
+		return;
+
+	TargetType tmp{ t };
+	if (!tmp.valid())
+		return;
+
+	tmp.m_uid = next_uid++;
+	targets.emplace(tmp.m_uid, tmp);
+}
+
+std::size_t TargetType::count()
+{
+	return targets.size();
+}
+
+const TargetType *TargetType::find(tree t)
+{
+	tree main_variant = get_record_main_variant(t);
+	if (!main_variant)
+		return nullptr;
+
+	for (const auto &[uid, target] : targets) {
+		if (lang_hooks.types_compatible_p(main_variant,
+						  target.m_main_variant))
+			return &target;
+	}
+
+	return nullptr;
+}
+
+TargetType *TargetType::find_mutable(tree t)
+{
+	tree main_variant = get_record_main_variant(t);
+	if (!main_variant)
+		return nullptr;
+
+	for (auto &[uid, target] : targets) {
+		if (lang_hooks.types_compatible_p(main_variant,
+						  target.m_main_variant))
+			return &target;
+	}
+
+	return nullptr;
+}
+
+const TargetType *TargetType::find(UID uid)
+{
+	auto it = targets.find(uid);
+	if (it == targets.end())
+		return nullptr;
+
+	return &it->second;
+}
+
+/*
+ * Convert GCC's byte + bit field layout into the byte-oriented layout model
+ * used by SPSLR metadata.
+ *
+ * Bitfields and sub-byte/overlapping storage are marked dangerous because the
+ * current runtime randomizer can only move independently addressable byte ranges.
+ */
+
+bool field_info(tree field_decl, std::size_t *offset, std::size_t *size,
+		std::size_t *alignment, bool *bitfield)
+{
+	if (!field_decl || TREE_CODE(field_decl) != FIELD_DECL)
+		return false;
+
+	tree field_off_t = DECL_FIELD_OFFSET(field_decl);
+	if (!field_off_t || TREE_CODE(field_off_t) != INTEGER_CST)
+		return false;
+
+	tree field_bit_off_t = DECL_FIELD_BIT_OFFSET(field_decl);
+	if (!field_bit_off_t || TREE_CODE(field_bit_off_t) != INTEGER_CST)
+		return false;
+
+	tree field_size_t = DECL_SIZE(field_decl);
+	if (!field_size_t || TREE_CODE(field_size_t) != INTEGER_CST)
+		return false;
+
+	HOST_WIDE_INT tmp_byte_offset = tree_to_uhwi(field_off_t);
+	HOST_WIDE_INT tmp_bit_offset = tree_to_uhwi(field_bit_off_t);
+	HOST_WIDE_INT tmp_bit_size = tree_to_uhwi(field_size_t);
+
+	HOST_WIDE_INT bit_offset_bytes = tmp_bit_offset / 8;
+	tmp_byte_offset += bit_offset_bytes;
+	tmp_bit_offset -= bit_offset_bytes * 8;
+
+	bool tmp_bitfield = (DECL_BIT_FIELD_TYPE(field_decl) != NULL_TREE);
+	tmp_bitfield |= !(tmp_bit_size % 8 == 0 && tmp_bit_offset == 0);
+
+	tmp_bit_size += tmp_bit_offset;
+
+	HOST_WIDE_INT tmp_bit_overhang = tmp_bit_size % 8;
+	if (tmp_bit_overhang != 0)
+		tmp_bit_size += (8 - tmp_bit_overhang);
+
+	HOST_WIDE_INT tmp_align_bits = DECL_ALIGN(field_decl);
+	if (tmp_align_bits <= 0 && TREE_TYPE(field_decl))
+		tmp_align_bits = TYPE_ALIGN(TREE_TYPE(field_decl));
+	if (tmp_align_bits <= 0)
+		tmp_align_bits = BITS_PER_UNIT;
+
+	std::size_t tmp_alignment = static_cast<std::size_t>(
+		(tmp_align_bits + BITS_PER_UNIT - 1) / BITS_PER_UNIT);
+	if (tmp_alignment == 0)
+		tmp_alignment = 1;
+
+	if (offset)
+		*offset = static_cast<std::size_t>(tmp_byte_offset);
+
+	if (size)
+		*size = static_cast<std::size_t>(tmp_bit_size / 8);
+
+	if (alignment)
+		*alignment = tmp_alignment;
+
+	if (bitfield)
+		*bitfield = tmp_bitfield;
+
+	return true;
+}
+
+bool TargetType::reference(tree ref, UID &target, std::size_t &offset)
+{
+	if (!ref || TREE_CODE(ref) != COMPONENT_REF)
+		return false;
+
+	tree base = TREE_OPERAND(ref, 0);
+	if (!base)
+		return false;
+
+	tree base_type = TREE_TYPE(base);
+	if (!base_type)
+		return false;
+
+	const TargetType *base_target = TargetType::find(base_type);
+	if (!base_target)
+		return false;
+
+	target = base_target->uid();
+
+	tree field_decl = TREE_OPERAND(ref, 1);
+
+	if (!field_info(field_decl, &offset, nullptr, nullptr, nullptr))
+		return false;
+
+	const Field *f = base_target->field(offset, false);
+	if (!f || (f->flags & Field::FLAG_DANGEROUS))
+		return false;
+
+	return true;
+}
+
+const std::unordered_map<UID, TargetType> &TargetType::all()
+{
+	return targets;
+}
+
+void TargetType::reset()
+{
+	targets.clear();
+	next_uid = 0;
+}
+
+static bool
+foreach_record_field(tree t,
+		     std::function<bool(const TargetType::Field &)> callback)
+{
+	if (!t || TREE_CODE(t) != RECORD_TYPE)
+		return false;
+
+	if (!COMPLETE_TYPE_P(t))
+		return false;
+
+	for (tree field_decl = TYPE_FIELDS(t); field_decl;
+	     field_decl = DECL_CHAIN(field_decl)) {
+		if (TREE_CODE(field_decl) != FIELD_DECL)
+			continue;
+
+		TargetType::Field field;
+		bool is_bitfield;
+
+		if (!field_info(field_decl, &field.offset, &field.size,
+				&field.alignment, &is_bitfield))
+			return false;
+
+		tree attrs = DECL_ATTRIBUTES(field_decl);
+		bool is_fixed = lookup_attribute(SPSLR_FIELD_FIXED_ATTRIBUTE,
+						 attrs) != NULL_TREE;
+
+		field.flags = ((is_fixed || is_bitfield) ?
+				       TargetType::Field::FLAG_DANGEROUS :
+				       0);
+
+		if (!callback(field))
+			return false;
+	}
+
+	return true;
+}
+
+/*
+ * Merge overlapping fields into one dangerous region.
+ *
+ * GCC can expose fields that overlap through bitfields, zero-sized members,
+ * padding-sensitive constructs, or frontend-specific layout artifacts. SPSLR
+ * keeps the containing byte range fixed rather than pretending those pieces
+ * can be randomized independently.
+ */
+
+static bool field_map_add(std::map<std::size_t, TargetType::Field> &map,
+			  const TargetType::Field &field)
+{
+	TargetType::Field tmp_field;
+	tmp_field.offset = field.offset;
+	tmp_field.size = (field.size == 0 ? 1 : field.size);
+	tmp_field.alignment = (field.alignment == 0 ? 1 : field.alignment);
+	tmp_field.flags =
+		(field.size == 0 ? TargetType::Field::FLAG_DANGEROUS : 0) |
+		field.flags;
+
+	// Overlaps are dangerous -> remove and integrate into member
+	auto overlap_end = map.lower_bound(tmp_field.offset + tmp_field.size);
+	for (auto it = std::make_reverse_iterator(overlap_end);
+	     it != map.rend();) {
+		const TargetType::Field &existing_field = it->second;
+
+		if (existing_field.offset + existing_field.size <=
+		    tmp_field.offset)
+			break;
+
+		auto combined_end = std::max<decltype(tmp_field.offset)>(
+			tmp_field.offset + tmp_field.size,
+			existing_field.offset + existing_field.size);
+
+		auto combined_offset = std::min<decltype(tmp_field.offset)>(
+			tmp_field.offset, existing_field.offset);
+		auto combined_size = combined_end - combined_offset;
+
+		tmp_field.flags |= (existing_field.flags |
+				    TargetType::Field::FLAG_DANGEROUS);
+		tmp_field.offset = combined_offset;
+		tmp_field.alignment =
+			std::min(tmp_field.alignment, existing_field.alignment);
+		tmp_field.size = combined_size;
+
+		// Erase overlapping member
+		auto tmp_forward = std::prev(it.base());
+		tmp_forward = map.erase(tmp_forward);
+		it = std::make_reverse_iterator(tmp_forward);
+	}
+
+	map.emplace(tmp_field.offset, tmp_field);
+	return true;
+}
+
+bool TargetType::fetch_fields(bool redo)
+{
+	if (!valid())
+		return false;
+
+	if ((m_flags & FLAG_FIELDS) != 0 && !redo)
+		return true;
+
+	m_flags &= ~FLAG_FIELDS;
+	m_fields.clear();
+
+	std::map<std::size_t, Field> tmp_fields;
+
+	auto per_field_callback = [&tmp_fields](const Field &field) -> bool {
+		return field_map_add(tmp_fields, field);
+	};
+
+	if (!foreach_record_field(m_main_variant, per_field_callback))
+		return false;
+
+	// Get struct size
+
+	tree size_tree = TYPE_SIZE(m_main_variant);
+	if (!size_tree || TREE_CODE(size_tree) != INTEGER_CST)
+		return false;
+
+	HOST_WIDE_INT size_bits = tree_to_uhwi(size_tree);
+	if (size_bits < 0 || size_bits % 8 != 0)
+		return false;
+
+	m_size = static_cast<std::size_t>(size_bits / 8);
+
+	// Everything done
+
+	m_fields = std::move(tmp_fields);
+	m_flags |= FLAG_FIELDS;
+	return true;
+}
diff --git a/tools/spslr/src/pinpoint/stage1/asm_offset_pass.cpp b/tools/spslr/src/pinpoint/stage1/asm_offset_pass.cpp
new file mode 100644
index 000000000000..e11f377ffb7d
--- /dev/null
+++ b/tools/spslr/src/pinpoint/stage1/asm_offset_pass.cpp
@@ -0,0 +1,133 @@
+#include <stage1.h>
+#include <unordered_map>
+
+#include <pinpoint_error.h>
+#include <pinpoint_config.h>
+
+/*
+ * Stage1 marker asm is a carrier for target/offset constants.
+ *
+ * It intentionally does not contain final machine code yet. GCC still needs
+ * freedom to allocate the destination register, and stage2 later observes the
+ * final RTL register choice before emitting bytes with a labeled immediate.
+ */
+
+const char *s1_ipin_marker = "/*spslr_s1_ipin_marker*/";
+
+static tree make_asm_operand(const char *constraint_text, tree operand_tree)
+{
+	tree constraint_str =
+		build_string(strlen(constraint_text) + 1, constraint_text);
+	tree inner_list = build_tree_list(integer_zero_node, constraint_str);
+	tree outer_list = build_tree_list(inner_list, operand_tree);
+	return outer_list;
+}
+
+static gimple *make_stage1_pin(tree lhs, UID target, std::size_t offset)
+{
+	if (!lhs)
+		return nullptr;
+
+	const char *asm_str =
+		s1_ipin_marker; // Asm is inserted at a later stage
+
+	tree arg0 = build_int_cst(size_type_node, target);
+	tree arg1 = build_int_cst(size_type_node, offset);
+
+	vec<tree, va_gc> *outputs = NULL;
+	vec<tree, va_gc> *inputs = NULL;
+
+	vec_safe_push(outputs, make_asm_operand("=r", lhs));
+	vec_safe_push(inputs, make_asm_operand("i", arg0));
+	vec_safe_push(inputs, make_asm_operand("i", arg1));
+
+	gasm *new_gasm = gimple_build_asm_vec(ggc_strdup(asm_str), inputs,
+					      outputs, NULL, NULL);
+	if (!new_gasm)
+		return nullptr;
+
+	/*
+	 * Non-volatile is intentional: unused field-offset computations should die
+	 * normally. Only offsets that survive optimization become instruction pins.
+	 */
+	gimple_asm_set_volatile(new_gasm, false);
+
+	return new_gasm;
+}
+
+/*
+ * gsi_replace() changes the statement, but SSA names that were defined by the
+ * separator call still point at the old def statement. Retarget those SSA defs
+ * so later GCC passes see the asm marker as the producer of the offset value.
+ */
+
+static void pin_update_ssa_def(function *fn, gimple *old_def, gimple *new_def)
+{
+	if (!fn || !old_def)
+		return;
+
+	// Stage 0 separator call was definition statement of temporary variable
+
+	unsigned i;
+	tree name;
+	FOR_EACH_SSA_NAME(i, name, fn)
+	{
+		if (!name)
+			continue;
+
+		if (SSA_NAME_DEF_STMT(name) != old_def)
+			continue;
+
+		SSA_NAME_DEF_STMT(name) = new_def;
+	}
+}
+
+static void pin_assemble_maybe(function *fn, gimple_stmt_iterator *gsi)
+{
+	if (!gsi)
+		return;
+
+	gimple *stmt = gsi_stmt(*gsi);
+	if (!stmt)
+		return;
+
+	UID target;
+	std::size_t offset;
+
+	if (!is_stage0_separator(stmt, target, offset))
+		return;
+
+	gimple *replacement =
+		make_stage1_pin(gimple_call_lhs(stmt), target, offset);
+	if (!replacement)
+		pinpoint_fatal();
+
+	gsi_replace(gsi, replacement, true);
+	pin_update_ssa_def(fn, stmt, replacement);
+}
+
+static const pass_data asm_offset_pass_data = {
+	GIMPLE_PASS, "asm_offset",   OPTGROUP_NONE, TV_NONE, 0, 0, 0,
+	0,	     TODO_update_ssa
+};
+
+asm_offset_pass::asm_offset_pass(gcc::context *ctxt)
+	: gimple_opt_pass(asm_offset_pass_data, ctxt)
+{
+}
+
+unsigned int asm_offset_pass::execute(function *fn)
+{
+	if (!fn)
+		return 0;
+
+	basic_block bb;
+	FOR_EACH_BB_FN(bb, fn)
+	{
+		for (gimple_stmt_iterator gsi = gsi_start_bb(bb);
+		     !gsi_end_p(gsi); gsi_next(&gsi))
+			pin_assemble_maybe(fn, &gsi);
+	}
+
+	return 0;
+}
diff --git a/tools/spslr/src/pinpoint/stage1/stage1.h b/tools/spslr/src/pinpoint/stage1/stage1.h
new file mode 100644
index 000000000000..976bf8b36918
--- /dev/null
+++ b/tools/spslr/src/pinpoint/stage1/stage1.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include <stage0.h>
+#include <safe-gimple.h>
+
+extern const char *s1_ipin_marker;
+
+struct asm_offset_pass : gimple_opt_pass {
+	asm_offset_pass(gcc::context *ctxt);
+	unsigned int execute(function *fn) override;
+};
diff --git a/tools/spslr/src/pinpoint/stage2/rtl_pin_lower_pass.cpp b/tools/spslr/src/pinpoint/stage2/rtl_pin_lower_pass.cpp
new file mode 100644
index 000000000000..e7e92c8ef514
--- /dev/null
+++ b/tools/spslr/src/pinpoint/stage2/rtl_pin_lower_pass.cpp
@@ -0,0 +1,283 @@
+#include <stage2.h>
+#include <stage1.h>
+#include <final.h>
+#include <pinpoint_config.h>
+#include <pinpoint_error.h>
+
+#include <safe-rtl.h>
+
+#include <unordered_map>
+#include <string>
+#include <cstring>
+#include <cstdio>
+
+static UID next_stage2_pin_uid = 0;
+static std::unordered_map<UID, S2InstructionPin> pins;
+
+const std::unordered_map<UID, S2InstructionPin> &s2_pins()
+{
+	return pins;
+}
+
+void s2_pins_reset()
+{
+	pins.clear();
+	next_stage2_pin_uid = 0;
+}
+
+/*
+ * Stage2 pins are the bridge from compiler metadata to runtime patching:
+ * target + original offset describe what to compute, while symbol points at
+ * the immediate bytes that must be rewritten.
+ */
+
+UID s2_pin_allocate(const S2InstructionPin &pin)
+{
+	UID uid = next_stage2_pin_uid++;
+	pins.emplace(uid, pin);
+	return uid;
+}
+
+static bool read_marker_inputs(rtx src, UID &target, std::size_t &offset)
+{
+	rtx in0 = ASM_OPERANDS_INPUT(src, 0);
+	rtx in1 = ASM_OPERANDS_INPUT(src, 1);
+
+	if (!CONST_INT_P(in0) || !CONST_INT_P(in1))
+		return false;
+
+	target = (UID)INTVAL(in0);
+	offset = (std::size_t)INTVAL(in1);
+	return true;
+}
+
+static bool extract_set_from_pat(rtx pat, rtx &set_out)
+{
+	if (!pat)
+		return false;
+
+	if (GET_CODE(pat) == SET) {
+		set_out = pat;
+		return true;
+	}
+
+	if (GET_CODE(pat) == PARALLEL) {
+		int len = XVECLEN(pat, 0);
+		for (int i = 0; i < len; ++i) {
+			rtx elem = XVECEXP(pat, 0, i);
+			if (elem && GET_CODE(elem) == SET) {
+				set_out = elem;
+				return true;
+			}
+		}
+	}
+
+	return false;
+}
+
+static bool get_regno_from_dest(rtx dest, unsigned &regno)
+{
+	if (!dest)
+		return false;
+
+	if (REG_P(dest)) {
+		regno = REGNO(dest);
+		return true;
+	}
+
+	if (GET_CODE(dest) == SUBREG) {
+		rtx inner = SUBREG_REG(dest);
+		if (inner && REG_P(inner)) {
+			regno = REGNO(inner);
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * Recognize the RTL form of a stage1 marker.
+ *
+ * At this point GCC has selected the destination register. That register
+ * choice determines the exact x86_64 encoding we must emit so the immediate
+ * operand can be labeled and patched later.
+ */
+
+static bool is_stage1_marker_insn(rtx_insn *insn, rtx &set_rtx, rtx &asm_src,
+				  UID &target, std::size_t &offset,
+				  unsigned &regno)
+{
+	if (!insn || !INSN_P(insn))
+		return false;
+
+	rtx pat = PATTERN(insn);
+	if (!extract_set_from_pat(pat, set_rtx))
+		return false;
+
+	rtx dest = SET_DEST(set_rtx);
+	rtx src = SET_SRC(set_rtx);
+
+	if (!src || GET_CODE(src) != ASM_OPERANDS)
+		return false;
+
+	const char *templ = ASM_OPERANDS_TEMPLATE(src);
+	if (!templ)
+		return false;
+
+	/* safer than exact strcmp */
+	if (!std::strstr(templ, s1_ipin_marker))
+		return false;
+
+	if (!read_marker_inputs(src, target, offset))
+		return false;
+
+	if (!get_regno_from_dest(dest, regno))
+		return false;
+
+	asm_src = src;
+	return true;
+}
+
+static const pass_data rtl_pin_lower_pass_data = {
+	RTL_PASS, /* type */
+	"spslr_rtl_pin_lower", /* name */
+	OPTGROUP_NONE, /* optinfo_flags */
+	TV_NONE, /* tv_id */
+	PROP_rtl, /* properties_required */
+	0, /* properties_provided */
+	0, /* properties_destroyed */
+	0, /* todo_flags_start */
+	0 /* todo_flags_finish */
+};
+
+struct EncodedReg {
+	unsigned rex;
+	unsigned modrm;
+};
+
+/*
+ * Encode "mov imm32, r64" for the hard register selected by GCC.
+ *
+ * The immediate is deliberately four bytes: the final asm labels exactly that
+ * immediate field, and runtime selfpatch overwrites those bytes with the
+ * randomized offset value.
+ */
+static bool x86_64_encode_mov_imm32_to_reg(unsigned regno, EncodedReg &out)
+{
+	if (!HARD_REGISTER_NUM_P(regno))
+		return false;
+
+	const char *name = reg_names[regno];
+	if (!name)
+		return false;
+
+	struct RegMapEntry {
+		const char *name;
+		unsigned rex;
+		unsigned rm;
+	};
+
+	static const RegMapEntry regmap[] = {
+		{ "ax", 0x48, 0 },  { "cx", 0x48, 1 },	{ "dx", 0x48, 2 },
+		{ "bx", 0x48, 3 },  { "sp", 0x48, 4 },	{ "bp", 0x48, 5 },
+		{ "si", 0x48, 6 },  { "di", 0x48, 7 },
+
+		{ "r8", 0x49, 0 },  { "r9", 0x49, 1 },	{ "r10", 0x49, 2 },
+		{ "r11", 0x49, 3 }, { "r12", 0x49, 4 }, { "r13", 0x49, 5 },
+		{ "r14", 0x49, 6 }, { "r15", 0x49, 7 },
+	};
+
+	for (const auto &e : regmap) {
+		if (std::strcmp(name, e.name) == 0) {
+			out.rex = e.rex;
+			out.modrm = 0xC0 | e.rm; /* mod=11, /0, rm=e.rm */
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * Emit the final instruction bytes manually so the immediate operand can be
+ * exposed as an object symbol.
+ *
+ * The symbol does not denote executable code as a callable function; it denotes
+ * the four-byte immediate field inside the instruction stream.
+ */
+
+static std::string make_final_x86_64_asm(const std::string &sym,
+					 const EncodedReg &enc, std::size_t imm)
+{
+	char buf[512];
+	std::snprintf(buf, sizeof(buf),
+		      ".globl %s\n"
+		      ".hidden %s\n"
+		      ".byte 0x%02x, 0xC7, 0x%02x\n"
+		      "%s:\n"
+		      ".type %s, @object\n"
+		      ".size %s, 4\n"
+		      ".long %zu",
+		      sym.c_str(), sym.c_str(), enc.rex, enc.modrm, sym.c_str(),
+		      sym.c_str(), sym.c_str(), imm);
+	return std::string(buf);
+}
+
+static bool lower_stage1_marker_insn(rtx_insn *insn)
+{
+	rtx set_rtx = nullptr;
+	rtx asm_src = nullptr;
+	UID target = 0;
+	std::size_t offset = 0;
+	unsigned regno = 0;
+
+	if (!is_stage1_marker_insn(insn, set_rtx, asm_src, target, offset,
+				   regno))
+		return false;
+
+	EncodedReg enc{};
+	if (!x86_64_encode_mov_imm32_to_reg(regno, enc)) {
+		pinpoint_fatal(
+			"stage2: unsupported hard register for ipin lowering: regno=%u",
+			(unsigned)regno);
+		return false;
+	}
+
+	S2InstructionPin pin;
+	pin.target = target;
+	pin.offset = offset;
+	pin.imm_size = 4;
+
+	UID pin_uid = s2_pin_allocate(pin);
+
+	auto it = pins.find(pin_uid);
+	if (it == pins.end())
+		pinpoint_fatal("stage2: internal error after s2_pin_allocate");
+
+	it->second.symbol = std::string(SPSLR_PINPOINT_STAGE2_PIN) +
+			    std::string(get_cu_hash()) + "_" +
+			    std::to_string(pin_uid);
+
+	std::string final_asm =
+		make_final_x86_64_asm(it->second.symbol, enc, offset);
+
+	ASM_OPERANDS_TEMPLATE(asm_src) = ggc_strdup(final_asm.c_str());
+	return true;
+}
+
+rtl_pin_lower_pass::rtl_pin_lower_pass(gcc::context *ctxt)
+	: rtl_opt_pass(rtl_pin_lower_pass_data, ctxt)
+{
+}
+
+unsigned int rtl_pin_lower_pass::execute(function *fn)
+{
+	(void)fn;
+
+	for (rtx_insn *insn = get_insns(); insn; insn = NEXT_INSN(insn)) {
+		(void)lower_stage1_marker_insn(insn);
+	}
+
+	return 0;
+}
diff --git a/tools/spslr/src/pinpoint/stage2/stage2.h b/tools/spslr/src/pinpoint/stage2/stage2.h
new file mode 100644
index 000000000000..5ae1c2b5626f
--- /dev/null
+++ b/tools/spslr/src/pinpoint/stage2/stage2.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <stage0.h>
+#include <safe-rtl.h>
+#include <unordered_map>
+#include <string>
+
+struct S2InstructionPin {
+	UID target;
+	std::size_t offset;
+	std::size_t imm_size;
+	std::string symbol;
+};
+
+const std::unordered_map<UID, S2InstructionPin> &s2_pins();
+void s2_pins_reset();
+UID s2_pin_allocate(const S2InstructionPin &pin);
+
+struct rtl_pin_lower_pass : rtl_opt_pass {
+	rtl_pin_lower_pass(gcc::context *ctxt);
+	unsigned int execute(function *fn) override;
+};
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [RFC 2/7] SPSLR patchcompile cli source
  2026-06-05 20:25 [RFC 0/7] Bootpatch-SLR: Randomizing Linux Kernel Structure Layouts at Boot York Jasper Niebuhr
  2026-06-05 20:25 ` [RFC 1/7] SPSLR pinpoint plugin source York Jasper Niebuhr
@ 2026-06-05 20:25 ` York Jasper Niebuhr
  2026-06-05 20:25 ` [RFC 3/7] SPSLR tool build integration York Jasper Niebuhr
                   ` (6 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: York Jasper Niebuhr @ 2026-06-05 20:25 UTC (permalink / raw)
  To: linux-hardening; +Cc: kees, franzen

Signed-off-by: York Jasper Niebuhr <yjn@yjn-systems.com>
---
 tools/spslr/src/patchcompile/accumulation.cpp | 529 ++++++++++++++++++
 tools/spslr/src/patchcompile/accumulation.h   |  78 +++
 tools/spslr/src/patchcompile/emit.cpp         | 451 +++++++++++++++
 tools/spslr/src/patchcompile/emit.h           |   4 +
 tools/spslr/src/patchcompile/patchcompile.cpp | 167 ++++++
 .../src/patchcompile/patchcompile_error.h     |  41 ++
 tools/spslr/src/patchcompile/spslr_list.h     |  13 +
 7 files changed, 1283 insertions(+)
 create mode 100644 tools/spslr/src/patchcompile/accumulation.cpp
 create mode 100644 tools/spslr/src/patchcompile/accumulation.h
 create mode 100644 tools/spslr/src/patchcompile/emit.cpp
 create mode 100644 tools/spslr/src/patchcompile/emit.h
 create mode 100644 tools/spslr/src/patchcompile/patchcompile.cpp
 create mode 100644 tools/spslr/src/patchcompile/patchcompile_error.h
 create mode 100644 tools/spslr/src/patchcompile/spslr_list.h

diff --git a/tools/spslr/src/patchcompile/accumulation.cpp b/tools/spslr/src/patchcompile/accumulation.cpp
new file mode 100644
index 000000000000..300c610a5286
--- /dev/null
+++ b/tools/spslr/src/patchcompile/accumulation.cpp
@@ -0,0 +1,529 @@
+#include "accumulation.h"
+
+#include <filesystem>
+#include <fstream>
+
+#include "patchcompile_error.h"
+
+namespace fs = std::filesystem;
+
+/*
+ * Accumulated metadata model.
+ *
+ * targets is indexed by global target UID. units is indexed by the per-CU UID
+ * symbol emitted by pinpoint and preserves each CU's local-to-global target map.
+ */
+static std::size_t next_global_target_uid = 0;
+std::unordered_map<std::size_t, TARGET> targets;
+std::unordered_map<std::string, CU> units;
+
+/*
+ * Decide whether two target layouts can share one runtime randomization.
+ *
+ * Same-named types from different compilation units are merged only if their
+ * byte layout is identical. If the layouts differ, they must get detached
+ * randomization state because one permutation could not safely describe both.
+ */
+static bool global_target_field_cmp(const TARGET &a, const TARGET &b)
+{
+	if (a.fields.size() != b.fields.size())
+		return false;
+
+	auto ita = a.fields.begin();
+	auto itb = b.fields.begin();
+
+	for (; ita != a.fields.end() && itb != b.fields.end(); ++ita, ++itb) {
+		const FIELD &fa = ita->second;
+		const FIELD &fb = itb->second;
+
+		if (fa.offset != fb.offset)
+			return false;
+		if (fa.size != fb.size)
+			return false;
+		if (fa.alignment != fb.alignment)
+			return false;
+		if (fa.flags != fb.flags)
+			return false;
+	}
+
+	return true;
+}
+
+static bool global_target_cmp(const TARGET &a, const TARGET &b)
+{
+	if (a.name != b.name)
+		return false;
+
+	if (a.size != b.size || !global_target_field_cmp(a, b)) {
+		patchcompile_warn(
+			"got different definitions of '%s' -> detached randomization",
+			a.name.c_str());
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Intern a CU-local target into the global target table.
+ *
+ * The returned UID is stable for the rest of this patchcompile invocation and
+ * becomes the runtime target index used by emitted ipin/dpin descriptors.
+ */
+static std::size_t accumulate_global_target(TARGET &&target, bool &was_new)
+{
+	was_new = false;
+	for (const auto &[guid, gtarget] : targets) {
+		if (global_target_cmp(gtarget, target))
+			return guid;
+	}
+
+	was_new = true;
+
+	std::size_t guid = next_global_target_uid++;
+	targets.emplace(guid, std::move(target));
+	return guid;
+}
+
+/*
+ * pinpoint emits one textual `.spslr` file per compilation unit:
+ *
+ *   SPSLR <source-file> <cu-uid-symbol>
+ *
+ *   target <name> <local-target-uid> <sizeof(target)> <field-count>
+ *   f      <offset> <size> <alignment> <flags>
+ *
+ *   ipin   <immediate-symbol> <local-target-uid> <field-offset> <imm-size>
+ *   dpin   <object-symbol> <object-offset> <nesting-level> <local-target-uid>
+ *
+ * Target UIDs in this file are local to the originating CU. This parser keeps
+ * that local namespace inside CU::local_targets and resolves it to global UIDs
+ * only after all compatible targets have been merged.
+ */
+static bool accumulate_file(const fs::path &path, bool no_new_targets)
+{
+	std::ifstream infile(path);
+	if (!infile)
+		return false;
+
+	std::string err_file_cppstr = path.generic_string();
+	const char *err_file = err_file_cppstr.c_str();
+	std::size_t err_line = 1; // One header line
+
+	std::string hdr_line;
+	if (!std::getline(infile, hdr_line))
+		return false;
+
+	std::istringstream hdr_iss(hdr_line);
+
+	std::string hdr_magic, hdr_cu_file, hdr_cu_uid;
+
+	if (!(hdr_iss >> hdr_magic) || hdr_magic != "SPSLR")
+		return false;
+
+	if (!(hdr_iss >> hdr_cu_file) || !(hdr_iss >> hdr_cu_uid))
+		return false;
+
+	if (units.contains(hdr_cu_uid)) {
+		patchcompile_file_error(err_file, err_line,
+					"duplicate compilation unit UID '%s'",
+					hdr_cu_uid.c_str());
+		return false;
+	}
+
+	patchcompile_debug("parsing meta data from %s ...", err_file);
+
+	units.emplace(hdr_cu_uid, CU{});
+	CU &cu = units.at(hdr_cu_uid);
+
+	std::string line;
+	while (std::getline(infile, line)) {
+		err_line++;
+
+		if (line.empty())
+			continue;
+
+		std::istringstream iss(line);
+		std::string type;
+
+		if (!(iss >> type))
+			return false;
+
+		if (type == "target") {
+			std::size_t err_line_target_base = err_line;
+
+			TARGET target;
+
+			std::size_t local_uid, field_count;
+
+			if (!(iss >> target.name) || !(iss >> local_uid) ||
+			    !(iss >> target.size) || !(iss >> field_count)) {
+				patchcompile_file_error(
+					err_file, err_line,
+					"invalid target line: \"%s\"",
+					line.c_str());
+				return false;
+			}
+
+			if (cu.local_targets.contains(local_uid)) {
+				patchcompile_file_error(
+					err_file, err_line_target_base,
+					"local target uid %llu has already been claimed",
+					local_uid);
+				return false;
+			}
+
+			for (std::size_t i = 0; i < field_count; i++) {
+				err_line++;
+
+				std::string fline;
+				if (!std::getline(infile, fline)) {
+					patchcompile_file_error(
+						err_file, err_line,
+						"missing field declaration, expected %llu fields",
+						field_count);
+					return false;
+				}
+
+				std::istringstream fiss(fline);
+
+				std::string ftype;
+				if (!(fiss >> ftype) || ftype != "f") {
+					patchcompile_file_error(
+						err_file, err_line,
+						"invalid field declaration line: \"%s\"",
+						fline.c_str());
+					return false;
+				}
+
+				FIELD field;
+				if (!(fiss >> field.offset) ||
+				    !(fiss >> field.size) ||
+				    !(fiss >> field.alignment) ||
+				    !(fiss >> field.flags)) {
+					patchcompile_file_error(
+						err_file, err_line,
+						"invalid field declaration line: \"%s\"",
+						fline.c_str());
+					return false;
+				}
+
+				// Note -> could do sanity checks here
+
+				if (target.fields.contains(field.offset)) {
+					patchcompile_file_error(
+						err_file, err_line,
+						"duplicate field offset %llu",
+						field.offset);
+					return false;
+				}
+
+				target.fields.emplace(field.offset, field);
+			}
+
+			/*
+			 * Runtime metadata addresses fields by compact field index, not by byte offset.
+			 * The map is ordered by offset, so this assigns deterministic original-layout
+			 * indices.
+			 */
+			auto fit = target.fields.begin();
+			for (std::size_t i = 0; i < field_count; i++) {
+				fit->second.idx = i;
+				fit++;
+			}
+
+			bool was_new = false;
+			std::size_t global_target_uid =
+				accumulate_global_target(std::move(target),
+							 was_new);
+
+			if (no_new_targets && was_new) {
+				patchcompile_file_error(
+					err_file, err_line_target_base,
+					"encountered new target but --no-new-targets is set");
+				return false;
+			}
+
+			cu.local_targets.emplace(local_uid, global_target_uid);
+			continue;
+		} else if (type == "ipin") {
+			IPIN ipin;
+			if (!(iss >> ipin.symbol) ||
+			    !(iss >> ipin.local_target) ||
+			    !(iss >> ipin.field_offset) ||
+			    !(iss >> ipin.imm_size)) {
+				patchcompile_file_error(
+					err_file, err_line,
+					"invalid ipin declaration: \"%s\"",
+					line.c_str());
+				return false;
+			}
+
+			if (!cu.local_targets.contains(ipin.local_target)) {
+				patchcompile_file_error(
+					err_file, err_line,
+					"ipin references local target %llu which has not yet been parsed",
+					ipin.local_target);
+				return false;
+			}
+
+			/*
+			 * An instruction immediate has exactly one patch descriptor. Duplicate symbols
+			 * would mean two metadata records are trying to patch the same bytes.
+			 */
+			if (cu.ipins.contains(ipin.symbol)) {
+				patchcompile_file_error(
+					err_file, err_line,
+					"duplicate ipin for symbol '%s'",
+					ipin.symbol.c_str());
+				return false;
+			}
+
+			cu.ipins.emplace(ipin.symbol, ipin);
+			continue;
+		} else if (type == "dpin") {
+			std::string symbol;
+			DPIN::COMPONENT comp;
+
+			if (!(iss >> symbol) || !(iss >> comp.offset) ||
+			    !(iss >> comp.level) || !(iss >> comp.target)) {
+				patchcompile_file_error(
+					err_file, err_line,
+					"invalid dpin declaration: \"%s\"",
+					line.c_str());
+				return false;
+			}
+
+			/*
+			 * Dpins are grouped by linker symbol because one static object can contain
+			 * several randomized target instances. Emission later expands the components
+			 * into individual runtime dpin records in patch order.
+			 */
+			if (!cu.dpins.contains(symbol))
+				cu.dpins.emplace(symbol,
+						 DPIN{ .symbol = symbol });
+
+			DPIN &dpin = cu.dpins.at(symbol);
+
+			if (!cu.local_targets.contains(comp.target)) {
+				patchcompile_file_error(
+					err_file, err_line,
+					"dpin references local target %llu which has not yet been parsed",
+					comp.target);
+				return false;
+			}
+
+			dpin.components.push_back(comp);
+			continue;
+		} else {
+			patchcompile_file_error(
+				err_file, err_line,
+				"invalid meta data file line of type '%s': \"%s\"",
+				type.c_str(), line.c_str());
+			return false;
+		}
+	}
+
+	patchcompile_debug(
+		"metadata summary for CU %s (source %s): %zu targets, %zu ipins, %zu dpins",
+		hdr_cu_uid.c_str(), hdr_cu_file.c_str(),
+		cu.local_targets.size(), cu.ipins.size(), cu.dpins.size());
+
+	for (const auto &[local_uid, global_uid] : cu.local_targets) {
+		const TARGET &target = targets.at(global_uid);
+
+		patchcompile_debug(
+			"  target local=%zu global=%zu name=%s size=%zu fields=%zu",
+			local_uid, global_uid, target.name.c_str(), target.size,
+			target.fields.size());
+	}
+
+	return true;
+}
+
+bool accumulate(const std::vector<std::string> &spslr_files,
+		bool no_new_targets)
+{
+	for (const std::string &spslr_file : spslr_files) {
+		fs::path p{ spslr_file };
+
+		if (!fs::exists(p) || !fs::is_regular_file(p)) {
+			patchcompile_error(
+				"failed to open meta data file at %s",
+				spslr_file.c_str());
+			return false;
+		}
+
+		if (!accumulate_file(p, no_new_targets)) {
+			patchcompile_error(
+				"failed to parse meta data file at %s",
+				spslr_file.c_str());
+			return false;
+		}
+	}
+
+	return true;
+}
+
+/*
+ * Persist the executable's global target namespace.
+ *
+ * Module patchcompile loads this map so module metadata can refer to exactly
+ * the same target IDs and randomization state as the main executable.
+ */
+bool dump_target_map(const std::string &path)
+{
+	std::filesystem::path p{ path };
+	if (p.has_parent_path()) {
+		std::error_code ec;
+		std::filesystem::create_directories(p.parent_path(), ec);
+		if (ec) {
+			patchcompile_error(
+				"failed to create target map directory at %s: %s",
+				p.parent_path().generic_string().c_str(),
+				ec.message().c_str());
+			return false;
+		}
+	}
+
+	std::ofstream out(p);
+	if (!out)
+		return false;
+
+	out << "SPSLR_TARGETS 1\n";
+
+	for (const auto &[uid, t] : targets) {
+		out << "target " << t.name << " " << uid << " " << t.size << " "
+		    << t.fields.size() << "\n";
+
+		for (const auto &[off, f] : t.fields) {
+			(void)off;
+			out << "f " << f.offset << " " << f.size << " "
+			    << f.alignment << " " << f.flags << "\n";
+		}
+	}
+
+	return !!out;
+}
+
+/*
+ * Restore a previously emitted global target namespace.
+ *
+ * This is used for modules: they may reference existing randomized types, but
+ * --no-new-targets prevents them from silently introducing layouts unknown to
+ * the already-running executable.
+ */
+bool load_target_map(const std::string &path)
+{
+	std::ifstream in(path);
+	if (!in)
+		return false;
+
+	std::string err_file_cppstr = path;
+	const char *err_file = err_file_cppstr.c_str();
+	std::size_t err_line = 1; // Header is one line
+
+	std::string magic;
+	std::size_t version = 0;
+	if (!(in >> magic >> version) || magic != "SPSLR_TARGETS" ||
+	    version != 1) {
+		patchcompile_file_error(err_file, err_line,
+					"invalid target map header");
+		return false;
+	}
+
+	std::string line;
+	std::getline(in, line); // consume rest of header line
+
+	std::size_t max_uid = 0;
+	bool have_any = false;
+
+	while (std::getline(in, line)) {
+		err_line++;
+
+		if (line.empty())
+			continue;
+
+		std::istringstream iss(line);
+		std::string tag;
+		iss >> tag;
+
+		if (tag != "target") {
+			patchcompile_file_error(
+				err_file, err_line,
+				"target map file can only contain target and field entries");
+			return false;
+		}
+
+		TARGET t{};
+		std::size_t uid = 0;
+		std::size_t field_count = 0;
+
+		if (!(iss >> t.name >> uid >> t.size >> field_count)) {
+			patchcompile_file_error(
+				err_file, err_line,
+				"invalid target declaration: \"%s\"",
+				line.c_str());
+			return false;
+		}
+
+		std::size_t err_line_target_base = err_line;
+
+		for (std::size_t i = 0; i < field_count; ++i) {
+			err_line++;
+
+			std::string fline;
+			if (!std::getline(in, fline)) {
+				patchcompile_file_error(
+					err_file, err_line,
+					"missing field entry, expected %llu fields",
+					field_count);
+				return false;
+			}
+
+			std::istringstream fiss(fline);
+			std::string ftag;
+			FIELD f{};
+
+			if (!(fiss >> ftag) || ftag != "f" ||
+			    !(fiss >> f.offset >> f.size >> f.alignment >>
+			      f.flags)) {
+				patchcompile_file_error(
+					err_file, err_line,
+					"invalid field declaration: \"%s\"",
+					fline.c_str());
+				return false;
+			}
+
+			f.idx = i;
+			if (t.fields.contains(f.offset)) {
+				patchcompile_file_error(
+					err_file, err_line,
+					"duplicate field offset %llu",
+					f.offset);
+				return false;
+			}
+			t.fields.emplace(f.offset, f);
+		}
+
+		if (targets.contains(uid)) {
+			patchcompile_file_error(
+				err_file, err_line_target_base,
+				"target uid %llu has already been claimed",
+				uid);
+			return false;
+		}
+
+		targets.emplace(uid, std::move(t));
+
+		if (!have_any || uid > max_uid) {
+			max_uid = uid;
+			have_any = true;
+		}
+	}
+
+	if (have_any)
+		next_global_target_uid = (max_uid + 1);
+
+	return true;
+}
diff --git a/tools/spslr/src/patchcompile/accumulation.h b/tools/spslr/src/patchcompile/accumulation.h
new file mode 100644
index 000000000000..caa2615c628d
--- /dev/null
+++ b/tools/spslr/src/patchcompile/accumulation.h
@@ -0,0 +1,78 @@
+#pragma once
+#include <string>
+#include <cstddef>
+#include <unordered_map>
+#include <map>
+#include <list>
+#include <optional>
+#include <cstdint>
+#include <vector>
+
+/*
+ * Per-CU instruction pin parsed from pinpoint metadata.
+ *
+ * local_target is the target UID as seen by the originating compilation unit.
+ * patchcompile resolves it to a global target UID before emission.
+ */
+struct IPIN {
+	std::string symbol;
+	std::size_t local_target;
+	std::size_t field_offset;
+	std::size_t imm_size;
+};
+
+/*
+ * Per-CU data pin for one linker-visible object symbol.
+ *
+ * A single symbol may contain multiple randomized target instances, for example
+ * nested structs or array elements. Each component describes one such instance.
+ */
+struct DPIN {
+	struct COMPONENT {
+		std::size_t offset;
+
+		/*
+		 * Nesting depth within the containing object. Larger values are patched first
+		 * so embedded target objects are rewritten before their containers.
+		 */
+		std::size_t level;
+		std::size_t target; // local pin -> local target
+	};
+
+	std::string symbol;
+	std::list<COMPONENT> components;
+};
+
+struct FIELD {
+	std::size_t offset;
+	std::size_t size;
+	std::size_t alignment;
+	std::size_t flags;
+	std::size_t idx;
+};
+
+struct TARGET {
+	std::string name;
+	std::size_t size;
+	std::map<std::size_t, FIELD> fields;
+};
+
+/*
+ * Metadata collected from one `.spslr` file.
+ *
+ * local_targets maps the CU-local target namespace emitted by pinpoint to the
+ * global target namespace constructed by patchcompile.
+ */
+struct CU {
+	std::unordered_map<std::size_t, std::size_t> local_targets;
+	std::unordered_map<std::string, IPIN> ipins;
+	std::unordered_map<std::string, DPIN> dpins;
+};
+
+extern std::unordered_map<std::size_t, TARGET> targets;
+extern std::unordered_map<std::string, CU> units;
+
+bool accumulate(const std::vector<std::string> &spslr_files,
+		bool no_new_targets);
+bool dump_target_map(const std::string &path);
+bool load_target_map(const std::string &path);
diff --git a/tools/spslr/src/patchcompile/emit.cpp b/tools/spslr/src/patchcompile/emit.cpp
new file mode 100644
index 000000000000..1b6017c5dd0b
--- /dev/null
+++ b/tools/spslr/src/patchcompile/emit.cpp
@@ -0,0 +1,451 @@
+#include "emit.h"
+#include "accumulation.h"
+
+#include <spslr_list.h>
+
+#include <algorithm>
+#include <cstdint>
+#include <ostream>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+namespace
+{
+
+/*
+ * These records mirror the C runtime descriptor layout emitted as assembly.
+ * Keep them in sync with selfpatch's .spslr reader structures and
+ * spslr_list.h opcode definitions.
+ */
+
+struct TARGET_REC {
+	uint32_t size;
+	uint32_t fieldcnt;
+	uint32_t fieldoff;
+};
+
+struct TARGET_FIELD_REC {
+	uint32_t offset;
+	uint32_t size;
+	uint32_t alignment;
+	uint32_t flags;
+};
+
+struct IPIN_REC {
+	std::string addr_sym;
+	uint32_t size;
+	uint32_t program;
+};
+
+struct IPIN_OP_REC {
+	uint32_t code;
+	uint32_t op0;
+	uint32_t op1;
+};
+
+struct DPIN_REC {
+	std::string addr_sym;
+	uint32_t target;
+};
+
+/*
+ * Identical "target field -> randomized offset" computations can share one
+ * ipin program. The runtime program is immutable descriptor data, so multiple
+ * instruction pins may point at the same program offset.
+ */
+
+struct PROGRAM_KEY {
+	uint32_t target;
+	uint32_t field;
+
+	bool operator==(const PROGRAM_KEY &other) const
+	{
+		return target == other.target && field == other.field;
+	}
+};
+
+struct PROGRAM_KEY_HASH {
+	std::size_t operator()(const PROGRAM_KEY &k) const
+	{
+		return (static_cast<std::size_t>(k.target) << 32) ^ k.field;
+	}
+};
+
+static bool emit_header(std::ostream &out);
+static bool emit_u32_object(std::ostream &out, const char *name,
+			    uint32_t value);
+static bool emit_targets(std::ostream &out,
+			 const std::vector<TARGET_REC> &targets);
+static bool emit_target_fields(std::ostream &out,
+			       const std::vector<TARGET_FIELD_REC> &fields);
+static bool emit_ipins(std::ostream &out, const std::vector<IPIN_REC> &ipins);
+static bool emit_ipin_ops(std::ostream &out,
+			  const std::vector<IPIN_OP_REC> &ops);
+static bool emit_dpins(std::ostream &out, const std::vector<DPIN_REC> &dpins);
+
+/*
+ * Build or reuse the tiny runtime program for one field-offset patch.
+ *
+ * Current ipin programs are simple:
+ *   1. add the randomized offset of field <field> in target <target>
+ *   2. patch the instruction immediate with the accumulated value
+ *
+ * The returned value is an index into spslr_ipin_ops[], not a byte address.
+ */
+
+static uint32_t intern_simple_ipin_program(
+	std::vector<IPIN_OP_REC> &ops,
+	std::unordered_map<PROGRAM_KEY, uint32_t, PROGRAM_KEY_HASH> &memo,
+	uint32_t target, uint32_t field)
+{
+	PROGRAM_KEY key{ target, field };
+
+	auto it = memo.find(key);
+	if (it != memo.end())
+		return it->second;
+
+	const uint32_t start = static_cast<uint32_t>(ops.size());
+
+	ops.push_back(IPIN_OP_REC{
+		.code = SPSLR_IPIN_OP_ADD_OFFSET,
+		.op0 = target,
+		.op1 = field,
+	});
+
+	ops.push_back(IPIN_OP_REC{
+		.code = SPSLR_IPIN_OP_PATCH,
+		.op0 = 0,
+		.op1 = 0,
+	});
+
+	memo.emplace(key, start);
+	return start;
+}
+
+/*
+ * Main executables carry complete SPSLR metadata in a read-only .spslr section.
+ * The runtime locates these exported symbols during selfpatch initialization.
+ */
+
+static bool emit_header(std::ostream &out)
+{
+	out << ".section .spslr,\"a\",@progbits\n";
+	out << ".balign 8\n";
+	return !!out;
+}
+
+/*
+ * Module metadata may contain relocations against module-local symbols.
+ * Put it in writable relocation-friendly storage so the loader can resolve
+ * those addresses without text/rodata relocation warnings.
+ */
+
+static bool emit_module_header(std::ostream &out)
+{
+	out << ".section .data.rel.ro.spslr,\"aw\",@progbits\n";
+	out << ".balign 8\n";
+	return !!out;
+}
+
+static bool emit_u32_object(std::ostream &out, const char *name, uint32_t value)
+{
+	out << ".globl " << name << "\n";
+	out << ".type " << name << ", @object\n";
+	out << ".balign 4\n";
+	out << name << ":\n";
+	out << "\t.long " << value << "\n";
+	out << ".size " << name << ", 4\n";
+	return !!out;
+}
+
+/*
+ * Emit the global target table. The target UID is the array index, so callers
+ * must provide a dense vector ordered by global UID.
+ */
+
+static bool emit_targets(std::ostream &out,
+			 const std::vector<TARGET_REC> &targets)
+{
+	if (!emit_u32_object(out, "spslr_target_cnt",
+			     static_cast<uint32_t>(targets.size())))
+		return false;
+
+	out << ".globl spslr_targets\n";
+	out << ".type spslr_targets, @object\n";
+	out << ".balign 4\n";
+	out << "spslr_targets:\n";
+
+	for (const TARGET_REC &t : targets) {
+		out << "\t.long " << t.size << "\n";
+		out << "\t.long " << t.fieldcnt << "\n";
+		out << "\t.long " << t.fieldoff << "\n";
+	}
+
+	out << ".size spslr_targets, .-spslr_targets\n";
+	return !!out;
+}
+
+/*
+ * Emit the flattened field table referenced by TARGET_REC::fieldoff.
+ * Fields are stored in original-layout order and later permuted by runtime.
+ */
+
+static bool emit_target_fields(std::ostream &out,
+			       const std::vector<TARGET_FIELD_REC> &fields)
+{
+	if (!emit_u32_object(out, "spslr_target_field_cnt",
+			     static_cast<uint32_t>(fields.size())))
+		return false;
+
+	out << ".globl spslr_target_fields\n";
+	out << ".type spslr_target_fields, @object\n";
+	out << ".balign 4\n";
+	out << "spslr_target_fields:\n";
+
+	for (const TARGET_FIELD_REC &f : fields) {
+		out << "\t.long " << f.offset << "\n";
+		out << "\t.long " << f.size << "\n";
+		out << "\t.long " << f.alignment << "\n";
+		out << "\t.long " << f.flags << "\n";
+	}
+
+	out << ".size spslr_target_fields, .-spslr_target_fields\n";
+	return !!out;
+}
+
+/*
+ * Emit instruction pins. addr_sym names the immediate bytes inside the final
+ * instruction stream; program indexes the spslr_ipin_ops interpreter program.
+ */
+
+static bool emit_ipins(std::ostream &out, const std::vector<IPIN_REC> &ipins)
+{
+	if (!emit_u32_object(out, "spslr_ipin_cnt",
+			     static_cast<uint32_t>(ipins.size())))
+		return false;
+
+	out << ".globl spslr_ipins\n";
+	out << ".type spslr_ipins, @object\n";
+	out << ".balign 8\n";
+	out << "spslr_ipins:\n";
+
+	for (const IPIN_REC &ip : ipins) {
+		out << "\t.quad " << ip.addr_sym << "\n";
+		out << "\t.long " << ip.size << "\n";
+		out << "\t.long " << ip.program << "\n";
+	}
+
+	out << ".size spslr_ipins, .-spslr_ipins\n";
+	return !!out;
+}
+
+/*
+ * Emit the bytecode-like ipin operation stream interpreted by selfpatch.
+ * Programs terminate with SPSLR_IPIN_OP_PATCH.
+ */
+
+static bool emit_ipin_ops(std::ostream &out,
+			  const std::vector<IPIN_OP_REC> &ops)
+{
+	if (!emit_u32_object(out, "spslr_ipin_op_cnt",
+			     static_cast<uint32_t>(ops.size())))
+		return false;
+
+	out << ".globl spslr_ipin_ops\n";
+	out << ".type spslr_ipin_ops, @object\n";
+	out << ".balign 4\n";
+	out << "spslr_ipin_ops:\n";
+
+	for (const IPIN_OP_REC &op : ops) {
+		out << "\t.long " << op.code << "\n";
+		out << "\t.long " << op.op0 << "\n";
+		out << "\t.long " << op.op1 << "\n";
+	}
+
+	out << ".size spslr_ipin_ops, .-spslr_ipin_ops\n";
+	return !!out;
+}
+
+/*
+ * Emit data pins. Each record names an already-existing object address that
+ * must be rewritten from original layout into randomized layout at startup.
+ */
+
+static bool emit_dpins(std::ostream &out, const std::vector<DPIN_REC> &dpins)
+{
+	if (!emit_u32_object(out, "spslr_dpin_cnt",
+			     static_cast<uint32_t>(dpins.size())))
+		return false;
+
+	out << ".globl spslr_dpins\n";
+	out << ".type spslr_dpins, @object\n";
+	out << ".balign 8\n";
+	out << "spslr_dpins:\n";
+
+	for (const DPIN_REC &dp : dpins) {
+		out << "\t.quad " << dp.addr_sym << "\n";
+		out << "\t.long " << dp.target << "\n";
+	}
+
+	out << ".size spslr_dpins, .-spslr_dpins\n";
+	return !!out;
+}
+
+}
+
+bool emit_patcher_program_asm(std::ostream &out, bool is_module)
+{
+	if (!is_module) {
+		if (!emit_header(out))
+			return false;
+	} else {
+		if (!emit_module_header(out))
+			return false;
+	}
+
+	/*
+	 * Runtime descriptors use dense array indexing, not maps. Therefore every
+	 * global target UID from 0 to targets.size()-1 must exist before emission.
+	 */
+	std::vector<TARGET_REC> target_recs(targets.size());
+	std::vector<TARGET_FIELD_REC> field_recs;
+
+	/*
+	 * Only the main executable emits the target layout table. Modules reuse the
+	 * executable's target map and contribute only their own ipins/dpins.
+	 */
+	if (!is_module) {
+		field_recs.reserve(64);
+
+		for (uint32_t uid = 0;
+		     uid < static_cast<uint32_t>(targets.size()); ++uid) {
+			if (!targets.contains(uid))
+				return false;
+
+			const TARGET &target = targets.at(uid);
+
+			TARGET_REC trec{};
+			trec.size = static_cast<uint32_t>(target.size);
+			trec.fieldoff =
+				static_cast<uint32_t>(field_recs.size());
+			trec.fieldcnt =
+				static_cast<uint32_t>(target.fields.size());
+
+			for (const auto &[off, field] : target.fields) {
+				(void)off;
+				field_recs.push_back(TARGET_FIELD_REC{
+					.offset = static_cast<uint32_t>(
+						field.offset),
+					.size = static_cast<uint32_t>(
+						field.size),
+					.alignment = static_cast<uint32_t>(
+						field.alignment),
+					.flags = static_cast<uint32_t>(
+						field.flags),
+				});
+			}
+
+			target_recs[uid] = trec;
+		}
+	}
+
+	std::vector<IPIN_REC> ipin_recs;
+	std::vector<IPIN_OP_REC> ipin_ops;
+	std::unordered_map<PROGRAM_KEY, uint32_t, PROGRAM_KEY_HASH> program_memo;
+
+	std::vector<DPIN_REC> dpin_recs;
+
+	for (const auto &[cu_uid, cu] : units) {
+		(void)cu_uid;
+
+		/*
+		 * Resolve each CU-local ipin to a global target and field index. Field offsets
+		 * from pinpoint are accepted only if they survived global target merging.
+		 */
+		for (const auto &[sym, ipin] : cu.ipins) {
+			const uint32_t global_target = static_cast<uint32_t>(
+				cu.local_targets.at(ipin.local_target));
+
+			if (!targets.contains(global_target))
+				return false;
+
+			const TARGET &target = targets.at(global_target);
+
+			if (!target.fields.contains(ipin.field_offset))
+				return false;
+
+			const FIELD &field =
+				target.fields.at(ipin.field_offset);
+
+			const uint32_t program = intern_simple_ipin_program(
+				ipin_ops, program_memo, global_target,
+				static_cast<uint32_t>(field.idx));
+
+			ipin_recs.push_back(IPIN_REC{
+				.addr_sym = ipin.symbol,
+				.size = static_cast<uint32_t>(ipin.imm_size),
+				.program = program,
+			});
+		}
+
+		for (const auto &[sym, dpin] : cu.dpins) {
+			std::vector<DPIN::COMPONENT> sorted_components(
+				dpin.components.begin(), dpin.components.end());
+
+			/*
+			 * Patch nested data from inside to outside.
+			 *
+			 * Rewriting an outer object may move the bytes containing an inner object.
+			 * Sorting by descending nesting level ensures inner target instances are
+			 * converted while their original addresses are still meaningful.
+			 */
+			std::sort(sorted_components.begin(),
+				  sorted_components.end(),
+				  [](const DPIN::COMPONENT &a,
+				     const DPIN::COMPONENT &b) {
+					  return a.level > b.level;
+				  });
+
+			for (const DPIN::COMPONENT &component :
+			     sorted_components) {
+				const uint32_t global_target =
+					static_cast<uint32_t>(
+						cu.local_targets.at(
+							component.target));
+
+				/*
+				 * A component offset turns one linker symbol into the address of an embedded
+				 * target instance inside that object.
+				 */
+				std::string addr = dpin.symbol;
+				if (component.offset != 0)
+					addr += " + " +
+						std::to_string(
+							component.offset);
+
+				dpin_recs.push_back(DPIN_REC{
+					.addr_sym = std::move(addr),
+					.target = global_target,
+				});
+			}
+		}
+	}
+
+	if (!is_module) {
+		if (!emit_targets(out, target_recs))
+			return false;
+		if (!emit_target_fields(out, field_recs))
+			return false;
+	}
+
+	if (!emit_ipins(out, ipin_recs))
+		return false;
+	if (!emit_ipin_ops(out, ipin_ops))
+		return false;
+	if (!emit_dpins(out, dpin_recs))
+		return false;
+
+	out << ".section .note.GNU-stack,\"\",@progbits\n";
+	return !!out;
+}
diff --git a/tools/spslr/src/patchcompile/emit.h b/tools/spslr/src/patchcompile/emit.h
new file mode 100644
index 000000000000..28f937852f5e
--- /dev/null
+++ b/tools/spslr/src/patchcompile/emit.h
@@ -0,0 +1,4 @@
+#pragma once
+#include <fstream>
+
+bool emit_patcher_program_asm(std::ostream &out, bool is_module);
diff --git a/tools/spslr/src/patchcompile/patchcompile.cpp b/tools/spslr/src/patchcompile/patchcompile.cpp
new file mode 100644
index 000000000000..6f5161206d87
--- /dev/null
+++ b/tools/spslr/src/patchcompile/patchcompile.cpp
@@ -0,0 +1,167 @@
+#include <string>
+#include <vector>
+#include <cstdint>
+#include <getopt.h>
+#include <filesystem>
+
+#include "accumulation.h"
+#include "emit.h"
+
+#include "patchcompile_error.h"
+
+bool patchcompile_verbose_enabled;
+
+/*
+ * spslr_patchcompile is the bridge between compiler metadata and runtime
+ * selfpatch descriptors.
+ *
+ * It consumes textual `.spslr` files emitted by pinpoint, merges compatible
+ * targets across compilation units, resolves CU-local target IDs to global
+ * runtime target IDs, and emits assembly defining the descriptor symbols read
+ * by selfpatch.
+ *
+ * For module builds, --load-targets imports the executable's target namespace
+ * and --no-new-targets ensures the module does not introduce randomization
+ * targets unknown to the already-running executable.
+ */
+
+struct OPTIONS {
+	std::string out_file;
+	std::string load_targets_file;
+	std::string dump_targets_file;
+	std::vector<std::string> spslr_files;
+	bool no_new_targets = false;
+	bool is_module = false;
+};
+
+int main(int argc, char **argv)
+{
+	static option long_options[] = {
+		{ "help", no_argument, 0, 0 },
+		{ "verbose", no_argument, 0, 0 },
+		{ "out", required_argument, 0, 0 },
+		{ "load-targets", required_argument, 0, 0 },
+		{ "dump-targets", required_argument, 0, 0 },
+		{ "no-new-targets", no_argument, 0, 0 },
+		{ "module", no_argument, 0, 0 },
+		{ 0, 0, 0, 0 }
+	};
+
+	OPTIONS opts{};
+	int option_index = 0;
+	int c;
+
+	patchcompile_verbose_enabled = false;
+
+	while ((c = getopt_long(argc, argv, "h:o:", long_options,
+				&option_index)) == 0) {
+		const option &opt = long_options[option_index];
+		std::string optname{ opt.name };
+
+		if (optname == "help") {
+			patchcompile_info(
+				"\nUsage:\n"
+				"  spslr_patchcompile --out=<file> [options] <file> ...\n\n"
+				"Options:\n"
+				"  --help\n"
+				"  --verbose\n"
+				"  --read-targets=<file>\n"
+				"  --emit-targets=<file>\n"
+				"  --no-new-targets\n"
+				"  --module\n");
+			return 0;
+		} else if (optname == "verbose") {
+			patchcompile_verbose_enabled = true;
+		} else if (optname == "out") {
+			opts.out_file = optarg;
+		} else if (optname == "load-targets") {
+			opts.load_targets_file = optarg;
+		} else if (optname == "dump-targets") {
+			opts.dump_targets_file = optarg;
+		} else if (optname == "no-new-targets") {
+			opts.no_new_targets = true;
+		} else if (optname == "module") {
+			opts.is_module = true;
+		} else {
+			patchcompile_error("invalid option '%s', try '--help'",
+					   optname.c_str());
+			return 1;
+		}
+	}
+
+	for (int i = optind; i < argc; ++i)
+		opts.spslr_files.emplace_back(argv[i]);
+
+	if (opts.out_file.empty()) {
+		patchcompile_error(
+			"missing output file path, supply it via '--out=<file>'");
+		return 1;
+	}
+
+	if (opts.spslr_files.empty()) {
+		patchcompile_error(
+			"missing spslr meta data files, pass them as positional arguments");
+		return 1;
+	}
+
+	/*
+	 * --no-new-targets only makes sense with a preloaded target map; otherwise
+	 * there is no existing namespace to validate against.
+	 */
+	if (opts.no_new_targets && opts.load_targets_file.empty()) {
+		patchcompile_error("--no-new-targets requires --load-targets");
+		return 1;
+	}
+
+	if (!opts.load_targets_file.empty()) {
+		if (!load_target_map(opts.load_targets_file)) {
+			patchcompile_error("failed to load target map from %s",
+					   opts.load_targets_file.c_str());
+			return 1;
+		}
+	}
+
+	if (!accumulate(opts.spslr_files, opts.no_new_targets)) {
+		patchcompile_error("failed to accumulate meta data");
+		return 1;
+	}
+
+	std::filesystem::path out_path{ opts.out_file };
+	if (out_path.has_parent_path()) {
+		std::error_code ec;
+		std::filesystem::create_directories(out_path.parent_path(), ec);
+		if (ec) {
+			patchcompile_error(
+				"failed to create output directory '%s' with error '%s'",
+				out_path.parent_path().c_str(),
+				ec.message().c_str());
+			return 1;
+		}
+	}
+
+	std::ofstream out(out_path);
+	if (!out) {
+		patchcompile_error("failed to open output file '%s'",
+				   opts.out_file.c_str());
+		return 1;
+	}
+
+	/*
+	 * The output is assembly rather than binary data so the normal assembler/linker
+	 * can resolve symbols referenced by ipin and dpin records.
+	 */
+	if (!emit_patcher_program_asm(out, opts.is_module)) {
+		patchcompile_error("failed to write spslr section");
+		return 1;
+	}
+
+	if (!opts.dump_targets_file.empty()) {
+		if (!dump_target_map(opts.dump_targets_file)) {
+			patchcompile_error("failed to write target map to '%s'",
+					   opts.dump_targets_file.c_str());
+			return 1;
+		}
+	}
+
+	return 0;
+}
diff --git a/tools/spslr/src/patchcompile/patchcompile_error.h b/tools/spslr/src/patchcompile/patchcompile_error.h
new file mode 100644
index 000000000000..93e2fc66055c
--- /dev/null
+++ b/tools/spslr/src/patchcompile/patchcompile_error.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include <cstdio>
+
+extern bool patchcompile_verbose_enabled;
+
+#define patchcompile_error(fmt, ...)                                          \
+	do {                                                                  \
+		std::fprintf(stderr, "[spslr::patchcompile] error: " fmt "\n", \
+			     ##__VA_ARGS__);                                  \
+	} while (0)
+
+#define patchcompile_file_error(f, l, fmt, ...)                               \
+	do {                                                                  \
+		std::fprintf(                                                 \
+			stderr,                                               \
+			"[spslr::patchcompile] error in %s at line %llu: " fmt \
+			"\n",                                                 \
+			f, l, ##__VA_ARGS__);                                 \
+	} while (0)
+
+#define patchcompile_warn(fmt, ...)                                     \
+	do {                                                            \
+		std::fprintf(stderr,                                    \
+			     "[spslr::patchcompile] warning: " fmt "\n", \
+			     ##__VA_ARGS__);                            \
+	} while (0)
+
+#define patchcompile_info(fmt, ...)                                          \
+	do {                                                                 \
+		std::fprintf(stderr, "[spslr::patchcompile] info: " fmt "\n", \
+			     ##__VA_ARGS__);                                 \
+	} while (0)
+
+#define patchcompile_debug(fmt, ...)                                          \
+	do {                                                                  \
+		if (patchcompile_verbose_enabled)                             \
+			std::fprintf(stderr,                                  \
+				     "[spslr::patchcompile] debug: " fmt "\n", \
+				     ##__VA_ARGS__);                          \
+	} while (0)
diff --git a/tools/spslr/src/patchcompile/spslr_list.h b/tools/spslr/src/patchcompile/spslr_list.h
new file mode 100644
index 000000000000..a8946b2fd0a1
--- /dev/null
+++ b/tools/spslr/src/patchcompile/spslr_list.h
@@ -0,0 +1,13 @@
+#ifndef SPSLR_LIST_H
+#define SPSLR_LIST_H
+
+#define SPSLR_IPIN_OP_PATCH 1
+#define SPSLR_IPIN_OP_ADD_INITIAL_OFFSET 2
+#define SPSLR_IPIN_OP_SUB_INITIAL_OFFSET 3
+#define SPSLR_IPIN_OP_ADD_OFFSET 4
+#define SPSLR_IPIN_OP_SUB_OFFSET 5
+#define SPSLR_IPIN_OP_ADD_CONST 6
+
+#define SPSLR_FLAG_FIELD_FIXED 1
+
+#endif
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [RFC 3/7] SPSLR tool build integration
  2026-06-05 20:25 [RFC 0/7] Bootpatch-SLR: Randomizing Linux Kernel Structure Layouts at Boot York Jasper Niebuhr
  2026-06-05 20:25 ` [RFC 1/7] SPSLR pinpoint plugin source York Jasper Niebuhr
  2026-06-05 20:25 ` [RFC 2/7] SPSLR patchcompile cli source York Jasper Niebuhr
@ 2026-06-05 20:25 ` York Jasper Niebuhr
  2026-06-05 20:25 ` [RFC 4/7] SPSLR selfpatch York Jasper Niebuhr
                   ` (5 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: York Jasper Niebuhr @ 2026-06-05 20:25 UTC (permalink / raw)
  To: linux-hardening; +Cc: kees, franzen

Signed-off-by: York Jasper Niebuhr <yjn@yjn-systems.com>
---
 tools/Makefile       | 15 +++++++--
 tools/spslr/Makefile | 78 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 91 insertions(+), 2 deletions(-)
 create mode 100644 tools/spslr/Makefile

diff --git a/tools/Makefile b/tools/Makefile
index 278d24723b74..55e393a0a63d 100644
--- a/tools/Makefile
+++ b/tools/Makefile
@@ -42,6 +42,7 @@ help:
 	@echo '  mm                     - misc mm tools'
 	@echo '  wmi			- WMI interface examples'
 	@echo '  x86_energy_perf_policy - Intel energy policy tool'
+	@echo '  spslr                  - Selfpatch-SLR build tools'
 	@echo ''
 	@echo 'You can do:'
 	@echo ' $$ make -C tools/ <tool>_install'
@@ -119,11 +120,18 @@ freefall: FORCE
 kvm_stat: FORCE
 	$(call descend,kvm/$@)
 
+spslr: FORCE
+	$(call descend,spslr,)
+
+spslr/%: FORCE
+	$(call descend,spslr,$*)
+
 all: acpi counter cpupower gpio hv firewire \
 		perf selftests bootconfig spi turbostat usb \
 		virtio mm bpf x86_energy_perf_policy \
 		tmon freefall iio objtool kvm_stat wmi \
-		pci debugging tracing thermal thermometer thermal-engine
+		pci debugging tracing thermal thermometer thermal-engine \
+		spslr
 
 acpi_install:
 	$(call descend,power/$(@:_install=),install)
@@ -215,12 +223,15 @@ freefall_clean:
 build_clean:
 	$(call descend,build,clean)
 
+spslr_clean:
+	$(call descend,spslr,clean)
+
 clean: acpi_clean counter_clean cpupower_clean hv_clean firewire_clean \
 		perf_clean selftests_clean turbostat_clean bootconfig_clean spi_clean usb_clean virtio_clean \
 		mm_clean bpf_clean iio_clean x86_energy_perf_policy_clean tmon_clean \
 		freefall_clean build_clean libbpf_clean libsubcmd_clean \
 		gpio_clean objtool_clean leds_clean wmi_clean pci_clean firmware_clean debugging_clean \
 		intel-speed-select_clean tracing_clean thermal_clean thermometer_clean thermal-engine_clean \
-		sched_ext_clean
+		sched_ext_clean spslr_clean
 
 .PHONY: FORCE
diff --git a/tools/spslr/Makefile b/tools/spslr/Makefile
new file mode 100644
index 000000000000..400b86edf5ec
--- /dev/null
+++ b/tools/spslr/Makefile
@@ -0,0 +1,78 @@
+# SPDX-License-Identifier: GPL-2.0
+
+CC ?= gcc
+
+# If CXX was not passed, derive it from CC.
+ifeq ($(origin CXX),default)
+override CXX := $(subst gcc,g++,$(CC))
+endif
+
+OUTPUT ?= .
+
+PINPOINT := $(OUTPUT)/pinpoint.so
+PATCHCOMPILE := $(OUTPUT)/patchcompile
+
+GCC_PLUGIN_DIR := $(shell $(CXX) -print-file-name=plugin)
+
+COMMON_CXXFLAGS := -std=c++20
+
+PINPOINT_CXXFLAGS := \
+	$(COMMON_CXXFLAGS) \
+	-fPIC -fno-rtti -fno-exceptions \
+	-D_GNU_SOURCE \
+	-Isrc/pinpoint \
+	-Isrc/pinpoint/safegcc \
+	-Isrc/pinpoint/stage0 \
+	-Isrc/pinpoint/stage1 \
+	-Isrc/pinpoint/stage2 \
+	-Isrc/pinpoint/final \
+	-I$(GCC_PLUGIN_DIR)/include
+
+PATCHCOMPILE_CXXFLAGS := \
+	$(COMMON_CXXFLAGS) \
+	-Isrc/patchcompile
+
+PINPOINT_SRCS := \
+	src/pinpoint/pinpoint.cpp \
+	src/pinpoint/stage0/on_register_attributes.cpp \
+	src/pinpoint/stage0/on_finish_type.cpp \
+	src/pinpoint/stage0/on_preserve_component_ref.cpp \
+	src/pinpoint/stage0/on_finish_decl.cpp \
+	src/pinpoint/stage0/on_start_unit.cpp \
+	src/pinpoint/stage0/target.cpp \
+	src/pinpoint/stage0/separator.cpp \
+	src/pinpoint/stage0/separate_offset_pass.cpp \
+	src/pinpoint/stage1/asm_offset_pass.cpp \
+	src/pinpoint/stage2/rtl_pin_lower_pass.cpp \
+	src/pinpoint/final/on_finish_unit.cpp
+
+PATCHCOMPILE_SRCS := \
+	src/patchcompile/patchcompile.cpp \
+	src/patchcompile/accumulation.cpp \
+	src/patchcompile/emit.cpp
+
+PINPOINT_OBJS := $(PINPOINT_SRCS:.cpp=.o)
+PATCHCOMPILE_OBJS := $(PATCHCOMPILE_SRCS:.cpp=.o)
+
+.PHONY: all clean
+all: $(PINPOINT) $(PATCHCOMPILE)
+
+$(PINPOINT): $(PINPOINT_OBJS)
+	$(CXX) -shared -o $@ $^
+
+$(PATCHCOMPILE): $(PATCHCOMPILE_OBJS)
+	$(CXX) -o $@ $^
+
+src/pinpoint/%.o: src/pinpoint/%.cpp
+	$(CXX) $(PINPOINT_CXXFLAGS) -c -o $@ $<
+
+src/patchcompile/%.o: src/patchcompile/%.cpp
+	$(CXX) $(PATCHCOMPILE_CXXFLAGS) -c -o $@ $<
+
+.PHONY: clean
+
+clean:
+	@echo '  CLEAN   tools/spslr'
+	@rm -f ./pinpoint.so ./patchcompile \
+		$(PINPOINT_OBJS) \
+		$(PATCHCOMPILE_OBJS)
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [RFC 4/7] SPSLR selfpatch
  2026-06-05 20:25 [RFC 0/7] Bootpatch-SLR: Randomizing Linux Kernel Structure Layouts at Boot York Jasper Niebuhr
                   ` (2 preceding siblings ...)
  2026-06-05 20:25 ` [RFC 3/7] SPSLR tool build integration York Jasper Niebuhr
@ 2026-06-05 20:25 ` York Jasper Niebuhr
  2026-06-05 20:25 ` [RFC 5/7] BPSLR task_struct integration York Jasper Niebuhr
                   ` (4 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: York Jasper Niebuhr @ 2026-06-05 20:25 UTC (permalink / raw)
  To: linux-hardening; +Cc: kees, franzen

Signed-off-by: York Jasper Niebuhr <yjn@yjn-systems.com>
---
 include/linux/spslr.h           |  48 ++++
 kernel/Makefile                 |   2 +
 kernel/spslr/Makefile           |   5 +
 kernel/spslr/spslr.c            | 267 ++++++++++++++++++++++
 kernel/spslr/spslr_env.c        |  74 +++++++
 kernel/spslr/spslr_env.h        |  42 ++++
 kernel/spslr/spslr_list.h       |  63 ++++++
 kernel/spslr/spslr_list_link.h  |  21 ++
 kernel/spslr/spslr_randomizer.c | 382 ++++++++++++++++++++++++++++++++
 kernel/spslr/spslr_randomizer.h |  26 +++
 10 files changed, 930 insertions(+)
 create mode 100644 include/linux/spslr.h
 create mode 100644 kernel/spslr/Makefile
 create mode 100644 kernel/spslr/spslr.c
 create mode 100644 kernel/spslr/spslr_env.c
 create mode 100644 kernel/spslr/spslr_env.h
 create mode 100644 kernel/spslr/spslr_list.h
 create mode 100644 kernel/spslr/spslr_list_link.h
 create mode 100644 kernel/spslr/spslr_randomizer.c
 create mode 100644 kernel/spslr/spslr_randomizer.h

diff --git a/include/linux/spslr.h b/include/linux/spslr.h
new file mode 100644
index 000000000000..2609bd06aae4
--- /dev/null
+++ b/include/linux/spslr.h
@@ -0,0 +1,48 @@
+#ifndef SPSLR_SELFPATCH_H
+#define SPSLR_SELFPATCH_H
+
+#define SPSLR_MODULE_SYM_IPIN_CNT "spslr_ipin_cnt"
+#define SPSLR_MODULE_SYM_IPINS "spslr_ipins"
+#define SPSLR_MODULE_SYM_IPIN_OP_CNT "spslr_ipin_op_cnt"
+#define SPSLR_MODULE_SYM_IPIN_OPS "spslr_ipin_ops"
+#define SPSLR_MODULE_SYM_DPIN_CNT "spslr_dpin_cnt"
+#define SPSLR_MODULE_SYM_DPINS "spslr_dpins"
+
+enum spslr_viability {
+	SPSLR_VIABLE,
+	SPSLR_NONVIABLE
+};
+
+enum spslr_error {
+	SPSLR_OK,
+	SPSLR_ERROR_RANDOMIZER_INIT,
+	SPSLR_ERROR_INITIAL_TARGET_LAYOUT,
+	SPSLR_ERROR_RANDOMIZED_TARGET_LAYOUT,
+	SPSLR_ERROR_RANDOMIZE,
+	SPSLR_ERROR_REORDER_BUFFER,
+	SPSLR_ERROR_UNINITIALIZED,
+	SPSLR_ERROR_ALREADY_PATCHED,
+	SPSLR_ERROR_PATCH_DPINS,
+	SPSLR_ERROR_PATCH_IPINS,
+	SPSLR_ERROR_META_INCOMPLETE
+};
+
+struct spslr_status {
+	enum spslr_viability viability;
+	enum spslr_error error;
+};
+
+struct spslr_module {
+	const void* ipin_cnt;
+	const void* ipins;
+	const void* ipin_op_cnt;
+	const void* ipin_ops;
+	const void* dpin_cnt;
+	const void* dpins;
+};
+
+struct spslr_status spslr_init(void);
+struct spslr_status spslr_selfpatch(void);
+struct spslr_status spslr_patch_module(const struct spslr_module* m);
+
+#endif
diff --git a/kernel/Makefile b/kernel/Makefile
index 87866b037fbe..9014e9573c31 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -132,6 +132,8 @@ obj-$(CONFIG_WATCH_QUEUE) += watch_queue.o
 obj-$(CONFIG_RESOURCE_KUNIT_TEST) += resource_kunit.o
 obj-$(CONFIG_SYSCTL_KUNIT_TEST) += sysctl-test.o
 
+obj-$(CONFIG_SPSLR) += spslr/
+
 CFLAGS_stackleak.o += $(DISABLE_STACKLEAK_PLUGIN)
 obj-$(CONFIG_GCC_PLUGIN_STACKLEAK) += stackleak.o
 KASAN_SANITIZE_stackleak.o := n
diff --git a/kernel/spslr/Makefile b/kernel/spslr/Makefile
new file mode 100644
index 000000000000..c67c7b1295ca
--- /dev/null
+++ b/kernel/spslr/Makefile
@@ -0,0 +1,5 @@
+obj-$(CONFIG_SPSLR) += spslr.o spslr_env.o spslr_randomizer.o
+
+CFLAGS_REMOVE_spslr.o += $(SPSLR_PLUGIN_FLAGS)
+CFLAGS_REMOVE_spslr_env.o += $(SPSLR_PLUGIN_FLAGS)
+CFLAGS_REMOVE_spslr_randomizer.o += $(SPSLR_PLUGIN_FLAGS)
diff --git a/kernel/spslr/spslr.c b/kernel/spslr/spslr.c
new file mode 100644
index 000000000000..89c3c79f3aa1
--- /dev/null
+++ b/kernel/spslr/spslr.c
@@ -0,0 +1,267 @@
+#include <linux/spslr.h>
+
+#include "spslr_randomizer.h"
+#include "spslr_env.h"
+#include "spslr_list_link.h"
+
+#define SPSLR_SANITY_CHECK
+
+static int spslr_patch_dpins(const struct spslr_dpin* dpins, spslr_u32 cnt);
+static int spslr_patch_dpin(void* addr, spslr_u32 target);
+static int spslr_patch_ipins(const struct spslr_ipin* ipins, spslr_u32 cnt,
+	const struct spslr_ipin_op* ipin_ops, spslr_u32 op_cnt);
+
+static int reorder_object(void* dst, const void* src, spslr_u32 target);
+static int spslr_calculate_ipin_value(const struct spslr_ipin_op* ipin_ops, spslr_u32 op_cnt, spslr_u32 start, spslr_s64* res);
+
+static void* reorder_buffer = NULL;
+
+static int allocate_reorder_buffer(void);
+
+static int initialized = 0, patched = 0;
+static enum spslr_viability viable = SPSLR_VIABLE;
+
+struct spslr_status __init spslr_init(void) {
+	if (initialized)
+		return (struct spslr_status) { .viability = viable, .error = SPSLR_OK };
+
+	if (spslr_randomizer_init() < 0)
+		return (struct spslr_status) { .viability = viable, .error = SPSLR_ERROR_RANDOMIZER_INIT };
+
+#ifdef SPSLR_SANITY_CHECK
+	for (spslr_u32 tidx = 0; tidx < spslr_target_cnt; tidx++) {
+		if (spslr_randomizer_validate_target(tidx) < 0)
+			return (struct spslr_status) { .viability = viable, .error = SPSLR_ERROR_INITIAL_TARGET_LAYOUT };
+	}
+#endif
+
+	if (spslr_randomize() < 0)
+		return (struct spslr_status) { .viability = viable, .error = SPSLR_ERROR_RANDOMIZE };
+
+#ifdef SPSLR_SANITY_CHECK
+	for (spslr_u32 tidx = 0; tidx < spslr_target_cnt; tidx++) {
+		if (spslr_randomizer_validate_target(tidx) < 0)
+			return (struct spslr_status) { .viability = viable, .error = SPSLR_ERROR_RANDOMIZED_TARGET_LAYOUT };
+	}
+#endif
+
+	if (allocate_reorder_buffer() < 0)
+		return (struct spslr_status) { .viability = viable, .error = SPSLR_ERROR_REORDER_BUFFER };
+
+	initialized = 1;
+	return (struct spslr_status) { .viability = viable, .error = SPSLR_OK };
+}
+
+struct spslr_status __init spslr_selfpatch(void) {
+	if (patched)
+		return (struct spslr_status) { .viability = viable, .error = SPSLR_ERROR_ALREADY_PATCHED };
+
+	if (!initialized)
+		return (struct spslr_status) { .viability = viable, .error = SPSLR_ERROR_UNINITIALIZED };
+
+	viable = SPSLR_NONVIABLE;
+
+	if (spslr_patch_dpins(spslr_dpins, spslr_dpin_cnt) < 0)
+		return (struct spslr_status) { .viability = viable, .error = SPSLR_ERROR_PATCH_DPINS };
+
+	if (spslr_patch_ipins(spslr_ipins, spslr_ipin_cnt, spslr_ipin_ops, spslr_ipin_op_cnt) < 0)
+		return (struct spslr_status) { .viability = viable, .error = SPSLR_ERROR_PATCH_IPINS };
+
+	viable = SPSLR_VIABLE;
+
+	patched = 1;
+	return (struct spslr_status) { .viability = viable, .error = SPSLR_OK };
+}
+
+struct spslr_status spslr_patch_module(const struct spslr_module* m) {
+	if (!initialized)
+		return (struct spslr_status) { .viability = SPSLR_VIABLE, .error = SPSLR_ERROR_UNINITIALIZED };
+
+	if (!m || !m->ipin_cnt || !m->ipins || !m->ipin_op_cnt || !m->ipin_ops || !m->dpin_cnt || !m->dpins)
+		return (struct spslr_status) { .viability = SPSLR_VIABLE, .error = SPSLR_ERROR_META_INCOMPLETE };
+
+	spslr_u32 module_ipin_cnt = *(const spslr_u32*)m->ipin_cnt;
+	const struct spslr_ipin* module_ipins = (const struct spslr_ipin*)m->ipins;
+	spslr_u32 module_ipin_op_cnt = *(const spslr_u32*)m->ipin_op_cnt;
+	const struct spslr_ipin_op* module_ipin_ops = (const struct spslr_ipin_op*)m->ipin_ops;
+	spslr_u32 module_dpin_cnt = *(const spslr_u32*)m->dpin_cnt;
+	const struct spslr_dpin* module_dpins = (const struct spslr_dpin*)m->dpins;
+
+	if (spslr_patch_dpins(module_dpins, module_dpin_cnt) < 0)
+		return (struct spslr_status) { .viability = SPSLR_NONVIABLE, .error = SPSLR_ERROR_PATCH_DPINS };
+
+	if (spslr_patch_ipins(module_ipins, module_ipin_cnt, module_ipin_ops, module_ipin_op_cnt) < 0)
+		return (struct spslr_status) { .viability = SPSLR_NONVIABLE, .error = SPSLR_ERROR_PATCH_IPINS };
+
+	return (struct spslr_status) { .viability = SPSLR_VIABLE, .error = SPSLR_OK };
+}
+
+static int __init allocate_reorder_buffer(void) {
+	if (reorder_buffer)
+		return 0;
+
+	spslr_u32 max_target_size = 0;
+	for (spslr_u32 i = 0; i < spslr_target_cnt; i++) {
+		if (spslr_targets[i].size > max_target_size)
+			max_target_size = spslr_targets[i].size;
+	}
+
+	reorder_buffer = spslr_env_malloc(max_target_size);
+	if (!reorder_buffer)
+		return -1;
+
+	return 0;
+}
+
+static int spslr_patch_dpins(const struct spslr_dpin* dpins, spslr_u32 cnt) {
+	for (spslr_u32 dpidx = 0; dpidx < cnt; dpidx++)  {
+		const struct spslr_dpin* dp = &dpins[dpidx];
+		if (spslr_patch_dpin((void*)dp->addr, dp->target) < 0)
+			return -1;
+	}
+
+	return 0;
+}
+
+static int reorder_object(void* dst, const void* src, spslr_u32 target) {
+	spslr_u32 field_count;
+	if (spslr_randomizer_get_target(target, NULL, &field_count))
+		return -1;
+
+	const spslr_u8* src_countable = (const spslr_u8*)src;
+	spslr_u8* dst_countable = (spslr_u8*)dst;
+
+	for (spslr_u32 i = 0; i < field_count; i++) {
+		struct spslr_randomizer_field_info finfo;
+		if (spslr_randomizer_get_field(target, i, SPSLR_RANDOMIZER_FIELD_IDX_MODE_FINAL, &finfo))
+			return -1;
+
+		spslr_env_memcpy(dst_countable + finfo.offset, src_countable + finfo.initial_offset, finfo.size);
+	}
+
+	return 0;
+}
+
+static int spslr_patch_dpin(void* addr, spslr_u32 target) {
+	if (target >= spslr_target_cnt)
+		return -1;
+
+	const struct spslr_target* t = &spslr_targets[target];
+
+	spslr_env_memset(reorder_buffer, 0, t->size);
+
+	if (reorder_object(reorder_buffer, addr, target) < 0)
+		return -1;
+
+	if (spslr_env_poke_data(addr, reorder_buffer, t->size) < 0)
+		return -1;
+
+	return 0;
+}
+
+static int spslr_patch_ipins(const struct spslr_ipin* ipins, spslr_u32 cnt,
+		const struct spslr_ipin_op* ipin_ops, spslr_u32 op_cnt) {
+	for (spslr_u32 ipidx = 0; ipidx < cnt; ipidx++) {
+		const struct spslr_ipin* ip = &ipins[ipidx];
+
+		spslr_s64 value;
+		if (spslr_calculate_ipin_value(ipin_ops, op_cnt, ip->program, &value) < 0)
+			return -1;
+
+		switch (ip->size) {
+			case 1:
+				if (spslr_env_poke_text_8((void*)ip->addr, (spslr_u8)value) < 0)
+					return -1;
+				break;
+			case 2:
+				if (spslr_env_poke_text_16((void*)ip->addr, (spslr_u16)value) < 0)
+					return -1;
+				break;
+			case 4:
+				if (spslr_env_poke_text_32((void*)ip->addr, (spslr_u32)value) < 0)
+					return -1;
+				break;
+			case 8:
+				if (spslr_env_poke_text_64((void*)ip->addr, (spslr_u64)value) < 0)
+					return -1;
+				break;
+			default:
+				return -1;
+		}
+	}
+
+	return 0;
+}
+
+static int spslr_calculate_ipin_value(const struct spslr_ipin_op* ipin_ops, spslr_u32 op_cnt, spslr_u32 start, spslr_s64* res) {
+	if (!res)
+		return -1;
+
+	*res = 0;
+
+	spslr_u32 pc = start;
+	while (1) {
+		if (pc >= op_cnt)
+			return -1;
+
+		int end_flag = 0;
+
+		const struct spslr_ipin_op* op = &ipin_ops[pc++];
+		switch (op->code) {
+			case SPSLR_IPIN_OP_PATCH:
+				end_flag = 1;
+				break;
+			case SPSLR_IPIN_OP_ADD_INITIAL_OFFSET:
+				{
+					struct spslr_randomizer_field_info finfo;
+					if (spslr_randomizer_get_field(op->op0.add_initial_offset_target,
+							op->op1.add_initial_offset_field, SPSLR_RANDOMIZER_FIELD_IDX_MODE_ORIGINAL, &finfo))
+						return -1;
+
+					*res += finfo.initial_offset;
+				}
+				break;
+			case SPSLR_IPIN_OP_ADD_OFFSET:
+				{
+					struct spslr_randomizer_field_info finfo;
+					if (spslr_randomizer_get_field(op->op0.add_offset_target,
+							op->op1.add_offset_field, SPSLR_RANDOMIZER_FIELD_IDX_MODE_ORIGINAL, &finfo))
+						return -1;
+
+					*res += finfo.offset;
+				}
+				break;
+			case SPSLR_IPIN_OP_SUB_INITIAL_OFFSET:
+				{
+					struct spslr_randomizer_field_info finfo;
+					if (spslr_randomizer_get_field(op->op0.sub_initial_offset_target,
+							op->op1.sub_initial_offset_field, SPSLR_RANDOMIZER_FIELD_IDX_MODE_ORIGINAL, &finfo))
+						return -1;
+
+					*res -= finfo.initial_offset;
+				}
+				break;
+			case SPSLR_IPIN_OP_SUB_OFFSET:
+				{
+					struct spslr_randomizer_field_info finfo;
+					if (spslr_randomizer_get_field(op->op0.sub_offset_target,
+							op->op1.sub_offset_field, SPSLR_RANDOMIZER_FIELD_IDX_MODE_ORIGINAL, &finfo))
+						return -1;
+
+					*res -= finfo.offset;
+				}
+				break;
+			case SPSLR_IPIN_OP_ADD_CONST:
+				*res += op->op0.add_const_value;
+				break;
+			default:
+				return -1;
+		}
+
+		if (end_flag)
+			break;
+	}
+
+	return 0;
+}
+
diff --git a/kernel/spslr/spslr_env.c b/kernel/spslr/spslr_env.c
new file mode 100644
index 000000000000..d4a79d532c06
--- /dev/null
+++ b/kernel/spslr/spslr_env.c
@@ -0,0 +1,74 @@
+#include "spslr_env.h"
+
+#include <linux/kernel.h>
+#include <linux/string.h>
+#include <linux/random.h>
+#include <linux/memblock.h>
+
+#ifdef CONFIG_X86
+
+#include <asm/text-patching.h>
+
+static __always_inline int spslr_env_poke_text(void *dst, const void *src, size_t n)
+{
+	text_poke_early(dst, src, n);
+	return 0;
+}
+
+#endif
+
+int spslr_env_poke_text_8(void *dst, u8 value)
+{
+	return spslr_env_poke_text(dst, &value, sizeof(value));
+}
+
+int spslr_env_poke_text_16(void *dst, u16 value)
+{
+	return spslr_env_poke_text(dst, &value, sizeof(value));
+}
+
+int spslr_env_poke_text_32(void *dst, u32 value)
+{
+	return spslr_env_poke_text(dst, &value, sizeof(value));
+}
+
+int spslr_env_poke_text_64(void *dst, u64 value)
+{
+	return spslr_env_poke_text(dst, &value, sizeof(value));
+}
+
+void* __init spslr_env_malloc(spslr_u32 n) {
+	if (!n)
+		n = 1;
+
+	/*
+	 * Hook runs before slab allocators are available.
+	 * memblock_alloc() is the correct early-boot allocator.
+	 * Reserve the memory to ensure it is not freed when handed to later allocator.
+	 */
+	void* res = memblock_alloc(n, SMP_CACHE_BYTES);
+
+	if (!res)
+		return res;
+
+	memblock_reserve(__pa(res), n);
+	return res;
+}
+
+int spslr_env_poke_data(void* dst, const void* src, spslr_u32 n) {
+	memcpy(dst, src, n);
+	return 0;
+}
+
+void spslr_env_memset(void* dst, int v, spslr_u32 n) {
+	memset(dst, v, n);
+}
+
+void spslr_env_memcpy(void* dst, const void* src, spslr_u32 n) {
+	memcpy(dst, src, n);
+}
+
+spslr_u32 __init spslr_env_random_u32(void) {
+	return get_random_u32(); // Hook runs after random_init_early() 
+}
+
diff --git a/kernel/spslr/spslr_env.h b/kernel/spslr/spslr_env.h
new file mode 100644
index 000000000000..09260dcb2192
--- /dev/null
+++ b/kernel/spslr/spslr_env.h
@@ -0,0 +1,42 @@
+#ifndef SPSLR_ENV_H
+#define SPSLR_ENV_H
+
+#include <linux/types.h>
+#include <linux/stddef.h>
+#include <linux/init.h>
+
+#ifndef __packed
+#define __packed __attribute__((packed))
+#endif
+
+#ifndef __init
+#define __init /* only required in kernel */
+#endif
+
+#ifndef __printf
+#define __printf(fmt_pos, arg_pos) __attribute__((format(printf, fmt_pos, arg_pos)))
+#endif
+
+#ifndef NULL
+#define NULL ((void*)0)
+#endif
+
+typedef uint8_t spslr_u8;
+typedef uint16_t spslr_u16;
+typedef uint32_t spslr_u32;
+typedef uint64_t spslr_u64;
+typedef int32_t spslr_s32;
+typedef int64_t spslr_s64;
+typedef uintptr_t spslr_uintptr;
+
+int spslr_env_poke_text_8(void* dst, spslr_u8 value);
+int spslr_env_poke_text_16(void* dst, spslr_u16 value);
+int spslr_env_poke_text_32(void* dst, spslr_u32 value);
+int spslr_env_poke_text_64(void* dst, spslr_u64 value);
+int spslr_env_poke_data(void* dst, const void* src, spslr_u32 n);
+void* spslr_env_malloc(spslr_u32 n);
+void spslr_env_memset(void* dst, int v, spslr_u32 n);
+void spslr_env_memcpy(void* dst, const void* src, spslr_u32 n);
+spslr_u32 spslr_env_random_u32(void);
+
+#endif
diff --git a/kernel/spslr/spslr_list.h b/kernel/spslr/spslr_list.h
new file mode 100644
index 000000000000..baf713e8e893
--- /dev/null
+++ b/kernel/spslr/spslr_list.h
@@ -0,0 +1,63 @@
+#ifndef SPSLR_LIST_H
+#define SPSLR_LIST_H
+
+#include "spslr_env.h"
+
+#define SPSLR_IPIN_OP_PATCH 1
+#define SPSLR_IPIN_OP_ADD_INITIAL_OFFSET 2
+#define SPSLR_IPIN_OP_SUB_INITIAL_OFFSET 3
+#define SPSLR_IPIN_OP_ADD_OFFSET 4
+#define SPSLR_IPIN_OP_SUB_OFFSET 5
+#define SPSLR_IPIN_OP_ADD_CONST 6
+
+#define SPSLR_FLAG_FIELD_FIXED 1
+
+struct spslr_target {
+	spslr_u32 size;
+	spslr_u32 fieldcnt;
+	spslr_u32 fieldoff; // Offset into spslr_target_field array
+} __packed;
+
+struct spslr_target_field {
+	spslr_u32 offset;
+	spslr_u32 size;
+	spslr_u32 alignment;
+	spslr_u32 flags;
+} __packed;
+
+struct spslr_ipin {
+	spslr_u64 addr;
+	spslr_u32 size;
+	spslr_u32 program; // Index in spslr_ipin_op array
+} __packed;
+
+struct spslr_ipin_op {
+	spslr_u32 code;
+
+	union {
+		spslr_u32 patch_unused;
+		spslr_u32 add_initial_offset_target;
+		spslr_u32 sub_initial_offset_target;
+		spslr_u32 add_offset_target;
+		spslr_u32 sub_offset_target;
+		spslr_s32 add_const_value;
+	} op0;
+
+	union {
+		spslr_u32 patch_unused;
+		spslr_u32 add_initial_offset_field;
+		spslr_u32 sub_initial_offset_field;
+		spslr_u32 add_offset_field;
+		spslr_u32 sub_offset_field;
+		spslr_u32 add_const_unused;
+	} op1;
+} __packed;
+
+struct spslr_dpin {
+	spslr_u64 addr;
+	spslr_u32 target; // Index in spslr_target array
+} __packed;
+
+#undef __packed
+
+#endif
diff --git a/kernel/spslr/spslr_list_link.h b/kernel/spslr/spslr_list_link.h
new file mode 100644
index 000000000000..91a4e12f52b4
--- /dev/null
+++ b/kernel/spslr/spslr_list_link.h
@@ -0,0 +1,21 @@
+#ifndef SPSLR_LIST_LINK_H
+#define SPSLR_LIST_LINK_H
+
+#include "spslr_list.h"
+
+extern const spslr_u32 spslr_target_cnt;
+extern const struct spslr_target spslr_targets[];
+
+extern const spslr_u32 spslr_target_field_cnt;
+extern const struct spslr_target_field spslr_target_fields[];
+
+extern const spslr_u32 spslr_ipin_cnt;
+extern const struct spslr_ipin spslr_ipins[];
+
+extern const spslr_u32 spslr_ipin_op_cnt;
+extern const struct spslr_ipin_op spslr_ipin_ops[];
+
+extern const spslr_u32 spslr_dpin_cnt;
+extern const struct spslr_dpin spslr_dpins[];
+
+#endif
diff --git a/kernel/spslr/spslr_randomizer.c b/kernel/spslr/spslr_randomizer.c
new file mode 100644
index 000000000000..44dd94102656
--- /dev/null
+++ b/kernel/spslr/spslr_randomizer.c
@@ -0,0 +1,382 @@
+#include "spslr_randomizer.h"
+
+#include "spslr_list_link.h"
+#include "spslr_env.h"
+
+struct Field {
+	spslr_u32 offset; /* Final field offset -> fields[i].offset = offset of field i in final layout */
+	spslr_u32 oidx; /* Original field idx -> fields[i].oidx = original position of field i in final layout */
+	spslr_u32 fidx; /* Final field idx -> fields[i].fidx = randomized/final position of original field i */
+};
+
+static struct Field* fields;
+
+int __init spslr_randomizer_init(void) {
+	fields = (struct Field*)spslr_env_malloc(sizeof(struct Field) * spslr_target_field_cnt);
+	if (!fields)
+		return -1;
+
+	for (spslr_u32 tidx = 0; tidx < spslr_target_cnt; tidx++) {
+		const struct spslr_target* t = &spslr_targets[tidx];
+
+		for (spslr_u32 fidx = 0; fidx < t->fieldcnt; fidx++) {
+			spslr_u32 gfidx = t->fieldoff + fidx;
+
+			const struct spslr_target_field* srcf = &spslr_target_fields[gfidx];
+			struct Field* dstf = &fields[gfidx];
+
+			dstf->offset = srcf->offset;
+			dstf->oidx = fidx;
+			dstf->fidx = fidx;
+		}
+	}
+
+	return 0;
+}
+
+int spslr_randomizer_get_target(spslr_u32 target, spslr_u32* size, spslr_u32* fieldcnt) {
+	if (target >= spslr_target_cnt)
+		return -1;
+
+	const struct spslr_target* t = &spslr_targets[target];
+
+	if (size)
+		*size = t->size;
+
+	if (fieldcnt)
+		*fieldcnt = t->fieldcnt;
+
+	return 0;
+}
+
+int spslr_randomizer_get_field(spslr_u32 target, spslr_u32 field, int field_idx_mode,
+		struct spslr_randomizer_field_info* info) {
+	if (target >= spslr_target_cnt)
+		return -1;
+
+	if (!info)
+		return 0;
+
+	const struct spslr_target* t = &spslr_targets[target];
+
+	if (field >= t->fieldcnt)
+		return -1;
+
+	const struct spslr_target_field* of = NULL;
+	const struct Field* rf = NULL;
+
+	switch (field_idx_mode) {
+		case SPSLR_RANDOMIZER_FIELD_IDX_MODE_ORIGINAL:
+			of = &spslr_target_fields[t->fieldoff + field];
+			rf = &fields[t->fieldoff + fields[t->fieldoff + field].fidx];
+			break;
+		case SPSLR_RANDOMIZER_FIELD_IDX_MODE_FINAL:
+			of = &spslr_target_fields[t->fieldoff + fields[t->fieldoff + field].oidx];
+			rf = &fields[t->fieldoff + field];
+			break;
+		default:
+			return -1;
+	}
+
+	info->size = of->size;
+	info->offset = rf->offset;
+	info->initial_offset = of->offset;
+	info->alignment = of->alignment;
+	info->flags = of->flags;
+
+	return 0;
+}
+
+int __init spslr_randomizer_validate_target(spslr_u32 target) {
+	spslr_u32 tsize, fieldcnt;
+
+	if (spslr_randomizer_get_target(target, &tsize, &fieldcnt) < 0)
+		return -1;
+
+	spslr_u32 cur_end = 0; 
+    
+	for (spslr_u32 i = 0; i < fieldcnt; i++) {
+		struct spslr_randomizer_field_info finfo;
+		if (spslr_randomizer_get_field(target, i, SPSLR_RANDOMIZER_FIELD_IDX_MODE_FINAL, &finfo) < 0)
+			return -1;
+
+		if (finfo.offset < cur_end)
+			return -1;
+
+		if (finfo.offset % finfo.alignment != 0)
+			return -1;
+
+		if ((finfo.flags & SPSLR_FLAG_FIELD_FIXED) && finfo.offset != finfo.initial_offset)
+			return -1;
+
+		if (finfo.offset + finfo.size > tsize)
+			return -1;
+
+		cur_end = finfo.offset + finfo.size;
+	}
+
+	return 0;
+}
+
+// RANDOMIZATION CODE
+
+struct ShuffleRegion {
+	spslr_u32 begin; 
+	spslr_u32 end;
+	spslr_u32 fill_begin;
+	spslr_u32 fill_end;
+};
+
+static spslr_u32 rand_u32(void);
+static struct Field* get_rfield(spslr_u32 target, spslr_u32 final_idx);
+static const struct spslr_target_field* get_ofield(spslr_u32 target, spslr_u32 orig_idx);
+static void get_origin_region(spslr_u32 target, spslr_u32 final_idx, struct ShuffleRegion* region);
+static int pick_shuffle_option(spslr_u32 target, spslr_u32 origin_final_idx,
+	const struct ShuffleRegion* origin, spslr_u32 alignment, spslr_u32* selected);
+static void do_swap(spslr_u32 target, spslr_u32 origin_final_idx,
+	const struct ShuffleRegion* origin_region, spslr_u32 new_offset);
+static void shuffle_one_target(spslr_u32 target);
+static void shuffle_target(spslr_u32 target);
+
+static spslr_u32 rand_u32(void) {
+	return spslr_env_random_u32();
+}
+
+static struct Field* __init get_rfield(spslr_u32 target, spslr_u32 final_idx) {
+	const struct spslr_target* t = &spslr_targets[target];
+	return &fields[t->fieldoff + final_idx];
+}
+
+static const struct spslr_target_field* __init get_ofield(spslr_u32 target, spslr_u32 orig_idx) {
+	const struct spslr_target* t = &spslr_targets[target];
+	return &spslr_target_fields[t->fieldoff + orig_idx];
+}
+
+static void __init get_origin_region(spslr_u32 target, spslr_u32 final_idx, struct ShuffleRegion* region) {
+	const struct spslr_target* t = &spslr_targets[target];
+	const struct Field* rf = get_rfield(target, final_idx);
+	const struct spslr_target_field* of = get_ofield(target, rf->oidx);
+
+	region->fill_begin = rf->offset;
+	region->fill_end = region->fill_begin + of->size;
+
+	if (final_idx == 0) {
+		region->begin = 0;
+	} else {
+		const struct Field* pred_rf = get_rfield(target, final_idx - 1);
+		const struct spslr_target_field* pred_of = get_ofield(target, pred_rf->oidx);
+		region->begin = pred_rf->offset + pred_of->size;
+	}
+
+	if (final_idx + 1 >= t->fieldcnt) {
+		region->end = t->size;
+	} else {
+		const struct Field* succ_rf = get_rfield(target, final_idx + 1);
+		region->end = succ_rf->offset;
+	}
+}
+
+static int __init option_is_valid(spslr_u32 target, spslr_u32 origin_final_idx, const struct ShuffleRegion* origin, spslr_u32 offset) {
+	const struct spslr_target* t = &spslr_targets[target];
+	const struct spslr_target_field* origin_of =
+		get_ofield(target, get_rfield(target, origin_final_idx)->oidx);
+
+	// When placed at offset, field will occupy [offset, option_would_end)
+	spslr_u32 option_would_end = offset + origin_of->size;
+	if (option_would_end > t->size)
+		return 0;
+
+	// Field may overlap with origin region. Moving field to offset truly frees:
+	// [true_origin_region_begin, true_origin_region_end)
+	spslr_u32 true_origin_region_begin = origin->begin;
+	spslr_u32 true_origin_region_end = origin->end;
+
+	if (offset <= origin->fill_begin && option_would_end > true_origin_region_begin)
+		true_origin_region_begin = option_would_end;
+
+	if (offset >= origin->fill_begin && offset < true_origin_region_end)
+		true_origin_region_end = offset;
+
+	// Iterate over fields in target region [offset, option_would_end] and see if they fit into true origin region
+	spslr_u32 origin_region_ptr = true_origin_region_begin;
+	for (spslr_u32 it = 0; it < t->fieldcnt; it++) {
+		const struct Field* rf = get_rfield(target, it);
+		const struct spslr_target_field* of = get_ofield(target, rf->oidx);
+
+		// The field being moved does not need to go into origin region
+		if (it == origin_final_idx)
+			continue;
+
+		// Field ends before target region -> must not be moved to origin region
+		if (rf->offset + of->size <= offset)
+			continue;
+
+		// Field starts after target region -> must not be moved to origin region
+		if (rf->offset >= option_would_end)
+			break;
+
+		// Fixed fields in target region unconditionally deny option
+		if (of->flags & SPSLR_FLAG_FIELD_FIXED)
+			return 0;
+
+		// Field from target region must be moved to aligned position in origin region
+		if (origin_region_ptr % of->alignment != 0)
+			origin_region_ptr += of->alignment - (origin_region_ptr % of->alignment);
+
+		origin_region_ptr += of->size;
+
+		// Field does not fit into origin region -> option not possible
+		if (origin_region_ptr > true_origin_region_end)
+			return 0;
+	}
+
+	return 1;
+}
+
+static int __init pick_shuffle_option(spslr_u32 target, spslr_u32 origin_final_idx, const struct ShuffleRegion* origin,
+		spslr_u32 alignment, spslr_u32* selected) {
+	const struct spslr_target* t = &spslr_targets[target];
+	spslr_u32 seen = 0;
+
+	/*
+	Note: Instead of looping over entire field array for each option, loops can be merged into one.
+	*/
+
+	for (spslr_u32 offset = 0; offset < t->size; offset += alignment) {
+		if (!option_is_valid(target, origin_final_idx, origin, offset))
+			continue;
+
+		// Reservoir sampling -> uniform distribution with O(1) memory consumption
+		seen++;
+		if ((rand_u32() % seen) == 0)
+			*selected = offset;
+	}
+
+	return seen ? 0 : -1;
+}
+
+static void __init do_swap(spslr_u32 target, spslr_u32 origin_idx,
+		const struct ShuffleRegion* origin_region, spslr_u32 new_offset) {
+	const struct spslr_target* t = &spslr_targets[target];
+	int pulled = 0;
+
+	spslr_u32 option_fill_end = new_offset + (origin_region->fill_end - origin_region->fill_begin);
+
+	spslr_u32 true_origin_region_begin = origin_region->begin;
+	if (new_offset <= origin_region->fill_begin && option_fill_end > true_origin_region_begin)
+		true_origin_region_begin = option_fill_end;
+
+	spslr_u32 origin_oidx = get_rfield(target, origin_idx)->oidx;
+
+	spslr_u32 origin_region_ptr = true_origin_region_begin;
+	for (spslr_u32 it = 0; it < t->fieldcnt; it++) {
+		struct Field* itf = get_rfield(target, it);
+
+		if (itf->oidx == origin_oidx)
+			continue;
+
+		const struct spslr_target_field* itof = get_ofield(target, itf->oidx);
+
+		if (itf->offset + itof->size <= new_offset)
+			continue;
+
+		if (itf->offset >= option_fill_end)
+			break;
+
+		spslr_u32 falign = itof->alignment;
+		if (origin_region_ptr % falign != 0)
+			origin_region_ptr += falign - (origin_region_ptr % falign);
+
+		if (!pulled) {
+			pulled = 1;
+
+			struct Field tmp = *get_rfield(target, it);
+			*get_rfield(target, it) = *get_rfield(target, origin_idx);
+			*get_rfield(target, origin_idx) = tmp;
+
+			get_rfield(target, it)->offset = new_offset;
+
+			get_rfield(target, origin_idx)->offset = origin_region_ptr;
+			origin_region_ptr += get_ofield(target, get_rfield(target, origin_idx)->oidx)->size;
+			continue;
+		}
+
+		{
+			struct Field tmp = *get_rfield(target, it);
+
+			if (origin_idx >= it) {
+				for (spslr_u32 pull_it = it + 1; pull_it <= origin_idx; pull_it++)
+					*get_rfield(target, pull_it - 1) = *get_rfield(target, pull_it);
+
+				*get_rfield(target, origin_idx) = tmp;
+				get_rfield(target, origin_idx)->offset = origin_region_ptr;
+				origin_region_ptr += get_ofield(target, get_rfield(target, origin_idx)->oidx)->size;
+
+				it--; // Must still look at the element now at it
+			} else {
+				for (spslr_u32 pull_it = it; pull_it > origin_idx + (spslr_u32)pulled; pull_it--)
+					*get_rfield(target, pull_it) = *get_rfield(target, pull_it - 1);
+
+				*get_rfield(target, origin_idx + (spslr_u32)pulled) = tmp;
+				get_rfield(target, origin_idx + (spslr_u32)pulled)->offset = origin_region_ptr;
+				origin_region_ptr += get_ofield(target, get_rfield(target, origin_idx + (spslr_u32)pulled)->oidx)->size;
+			}
+		}
+
+		pulled++;
+	}
+
+	/*
+	 * Rebuild original->final mapping for this target.
+	 */
+	for (spslr_u32 final_idx = 0; final_idx < t->fieldcnt; final_idx++) {
+		struct Field* rf = get_rfield(target, final_idx);
+		fields[t->fieldoff + rf->oidx].fidx = final_idx;
+	}
+}
+
+/*
+Note: final version should not shuffle random fields but try to shuffle each original field idx once
+*/
+static void __init shuffle_one_target(spslr_u32 target) {
+	const struct spslr_target* t = &spslr_targets[target];
+	if (t->fieldcnt == 0)
+		return;
+
+	spslr_u32 origin_final_idx = rand_u32() % t->fieldcnt;
+	struct Field* origin_rf = get_rfield(target, origin_final_idx);
+	const struct spslr_target_field* origin_of = get_ofield(target, origin_rf->oidx);
+
+	if (origin_of->flags & SPSLR_FLAG_FIELD_FIXED)
+		return;
+
+	struct ShuffleRegion origin_region;
+	spslr_u32 selected_option;
+
+	get_origin_region(target, origin_final_idx, &origin_region);
+
+	if (pick_shuffle_option(target, origin_final_idx, &origin_region,
+			origin_of->alignment, &selected_option) < 0)
+		return;
+
+	do_swap(target, origin_final_idx, &origin_region, selected_option);
+}
+
+static void __init shuffle_target(spslr_u32 target) {
+	const struct spslr_target* t = &spslr_targets[target];
+	spslr_u32 shuffle_count = t->fieldcnt * 2;
+
+	for (spslr_u32 i = 0; i < shuffle_count; i++)
+		shuffle_one_target(target);
+}
+
+int __init spslr_randomize(void) {
+	if (!fields)
+		return -1;
+
+	for (spslr_u32 tidx = 0; tidx < spslr_target_cnt; tidx++)
+		shuffle_target(tidx);
+
+	return 0;
+}
+
diff --git a/kernel/spslr/spslr_randomizer.h b/kernel/spslr/spslr_randomizer.h
new file mode 100644
index 000000000000..b240257d28b6
--- /dev/null
+++ b/kernel/spslr/spslr_randomizer.h
@@ -0,0 +1,26 @@
+#ifndef SPSLR_RANDOMIZER_H
+#define SPSLR_RANDOMIZER_H
+
+#include "spslr_env.h"
+
+#define SPSLR_RANDOMIZER_FIELD_IDX_MODE_ORIGINAL 1
+#define SPSLR_RANDOMIZER_FIELD_IDX_MODE_FINAL 2
+
+struct spslr_randomizer_field_info {
+	spslr_u32 size;
+	spslr_u32 offset;
+	spslr_u32 initial_offset;
+	spslr_u32 alignment;
+	spslr_u32 flags;
+};
+
+int spslr_randomizer_init(void);
+int spslr_randomize(void);
+
+int spslr_randomizer_get_target(spslr_u32 target, spslr_u32* size, spslr_u32* fieldcnt);
+int spslr_randomizer_get_field(spslr_u32 target, spslr_u32 field, int field_idx_mode,
+	struct spslr_randomizer_field_info* info);
+
+int spslr_randomizer_validate_target(spslr_u32 target);
+
+#endif
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [RFC 5/7] BPSLR task_struct integration
  2026-06-05 20:25 [RFC 0/7] Bootpatch-SLR: Randomizing Linux Kernel Structure Layouts at Boot York Jasper Niebuhr
                   ` (3 preceding siblings ...)
  2026-06-05 20:25 ` [RFC 4/7] SPSLR selfpatch York Jasper Niebuhr
@ 2026-06-05 20:25 ` York Jasper Niebuhr
  2026-06-05 20:25 ` [RFC 6/7] BPSLR tasklist sample module York Jasper Niebuhr
                   ` (3 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: York Jasper Niebuhr @ 2026-06-05 20:25 UTC (permalink / raw)
  To: linux-hardening; +Cc: kees, franzen

Signed-off-by: York Jasper Niebuhr <yjn@yjn-systems.com>
---
 Makefile                              | 131 ++++++++++++++++++++++++++
 arch/x86/boot/compressed/Makefile     |   2 +
 arch/x86/kernel/vmlinux.lds.S         |   8 ++
 drivers/firmware/efi/libstub/Makefile |   2 +
 include/linux/compiler_types.h        |  12 +++
 include/linux/sched.h                 |  50 +++++-----
 init/Kconfig                          |   7 ++
 init/main.c                           |  15 +++
 kernel/module/main.c                  |  62 ++++++++++++
 scripts/Makefile.build                |   9 +-
 scripts/Makefile.modfinal             | 131 +++++++++++++++++++++++++-
 scripts/Makefile.vmlinux              |   4 +
 scripts/Makefile.vmlinux_o            |   1 +
 scripts/link-vmlinux.sh               |   5 +
 14 files changed, 413 insertions(+), 26 deletions(-)

diff --git a/Makefile b/Makefile
index 68a8faff2543..d491db3db2e8 100644
--- a/Makefile
+++ b/Makefile
@@ -796,6 +796,39 @@ include/config/auto.conf:
 endif # may-sync-config
 endif # need-config
 
+# Outside config for make clean
+SPSLR_METADIR := $(objtree)/.spslr-meta
+SPSLR_VMLINUX_TARGET_MAP := $(objtree)/.vmlinux-spslr-targets
+
+ifdef CONFIG_SPSLR
+
+export SPSLR_METADIR
+export SPSLR_VMLINUX_TARGET_MAP
+export SPSLR_TOOL_PINPOINT := $(srctree)/tools/spslr/pinpoint.so
+export SPSLR_TOOL_PATCHCOMPILE := $(srctree)/tools/spslr/patchcompile
+
+# Use pinpoint plugin for kernel objects
+
+SPSLR_PLUGIN      := -fplugin=$(SPSLR_TOOL_PINPOINT)
+SPSLR_PLUGIN_META := -fplugin-arg-pinpoint-metadir=$(SPSLR_METADIR)
+SPSLR_PLUGIN_SRC  := -fplugin-arg-pinpoint-srcroot=$(srctree)
+export SPSLR_PLUGIN_FLAGS := -D__SPSLR__ $(SPSLR_PLUGIN) $(SPSLR_PLUGIN_META) $(SPSLR_PLUGIN_SRC)
+
+KBUILD_CFLAGS += $(SPSLR_PLUGIN_FLAGS)
+
+endif
+
+# Supress warnings generated by newer gcc-16
+
+KBUILD_CFLAGS += -Wno-address-of-packed-member
+KBUILD_CFLAGS += -Wno-format-truncation
+KBUILD_CFLAGS += -Wno-unused-but-set-variable
+KBUILD_CFLAGS += -Wno-dangling-pointer
+KBUILD_CFLAGS += -Wno-unterminated-string-initialization
+KBUILD_CFLAGS += -Wno-format-overflow
+KBUILD_CFLAGS += -Wno-stringop-truncation
+KBUILD_CFLAGS += -Wno-stringop-overread
+
 KBUILD_CFLAGS	+= -fno-delete-null-pointer-checks
 
 ifdef CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE
@@ -1142,6 +1175,104 @@ targets += vmlinux.a
 vmlinux.a: $(KBUILD_VMLINUX_OBJS) scripts/head-object-list.txt FORCE
 	$(call if_changed,ar_vmlinux.a)
 
+# Outside config for make clean
+SPSLR_VMLINUX_SPSLR_SECTION_ASM := $(objtree)/spslr_section.S
+SPSLR_VMLINUX_SPSLR_SECTION_OBJ := $(objtree)/spslr_section.o
+SPSLR_VMLINUX_SPSLR_LIST := $(objtree)/.vmlinux-spslr-files
+SPSLR_VMLINUX_PATCHCOMPILE_STAMP := $(objtree)/.vmlinux-spslr-patchcompile
+
+ifdef CONFIG_SPSLR
+
+targets += $(SPSLR_VMLINUX_SPSLR_SECTION_ASM) $(SPSLR_VMLINUX_SPSLR_SECTION_OBJ) $(SPSLR_VMLINUX_SPSLR_LIST) $(SPSLR_VMLINUX_TARGET_MAP) $(SPSLR_VMLINUX_PATCHCOMPILE_STAMP)
+
+SPSLR_VMLINUX_LINK_INPUTS := vmlinux.a $(KBUILD_VMLINUX_LIBS)
+
+export SPSLR_VMLINUX_SPSLR_SECTION_OBJ
+
+quiet_cmd_gen_spslr_list = SPSLRLST $@
+      cmd_gen_spslr_list = set -e; \
+	tmp=$@.tmp; \
+	rm -f $$tmp; \
+	for in_f in $(SPSLR_VMLINUX_LINK_INPUTS); do \
+		printf 'SPSLR input: %s\n' "$$in_f" >&2; \
+		case "$$in_f" in \
+		*.a) \
+			$(AR) t "$$in_f" ;; \
+		*.o) \
+			printf '%s\n' "$$in_f" ;; \
+		esac; \
+	done | awk '!seen[$$0]++' | while read -r obj; do \
+		spslr="$(SPSLR_METADIR)/$${obj%.o}.c.spslr"; \
+		printf 'SPSLR map: obj=%s spslr=%s\n' "$$obj" "$$spslr" >&2; \
+		if [ -f "$$spslr" ]; then \
+			printf 'SPSLR found: %s\n' "$$spslr" >&2; \
+			printf '%s\n' "$$spslr"; \
+		else \
+			printf 'SPSLR missing: obj=%s expected=%s\n' "$$obj" "$$spslr" >&2; \
+		fi; \
+	done > $$tmp; \
+	printf 'SPSLR wrote manifest: %s\n' "$$tmp" >&2; \
+	if [ -s "$$tmp" ]; then \
+		printf 'SPSLR manifest entries: %s\n' "$$(wc -l < "$$tmp")" >&2; \
+	else \
+		printf 'SPSLR manifest is empty: %s\n' "$$tmp" >&2; \
+	fi; \
+	mv -f $$tmp $@; \
+	printf 'SPSLR final manifest: %s\n' "$@" >&2
+
+$(SPSLR_VMLINUX_SPSLR_LIST): $(SPSLR_VMLINUX_LINK_INPUTS) FORCE
+	$(call if_changed,gen_spslr_list)
+
+quiet_cmd_spslr_patchcompile_vmlinux = SPSLR   $@
+      cmd_spslr_patchcompile_vmlinux = set -e; \
+	if [ ! -s $(SPSLR_VMLINUX_SPSLR_LIST) ]; then \
+		echo "No .spslr files found for vmlinux link inputs" >&2; \
+		exit 1; \
+	fi; \
+	targets_tmp="$(SPSLR_VMLINUX_TARGET_MAP).tmp"; \
+	trap 'rm -f "$$targets_tmp"' EXIT; \
+	$(SPSLR_TOOL_PATCHCOMPILE) \
+		--out $(SPSLR_VMLINUX_SPSLR_SECTION_ASM) \
+		--dump-targets "$$targets_tmp" \
+		--verbose \
+		$$(cat $(SPSLR_VMLINUX_SPSLR_LIST)); \
+	if ! cmp -s "$$targets_tmp" "$(SPSLR_VMLINUX_TARGET_MAP)"; then \
+		mv -f "$$targets_tmp" "$(SPSLR_VMLINUX_TARGET_MAP)"; \
+	else \
+		rm -f "$$targets_tmp"; \
+	fi
+
+quiet_cmd_spslr_as = AS      $@
+      cmd_spslr_as = $(CC) $(KBUILD_AFLAGS) $(NOSTDINC_FLAGS) $(LINUXINCLUDE) -c -o $@ $<
+
+$(SPSLR_VMLINUX_PATCHCOMPILE_STAMP): $(SPSLR_VMLINUX_SPSLR_LIST) $(SPSLR_TOOL_PATCHCOMPILE) FORCE
+	$(call if_changed,spslr_patchcompile_vmlinux)
+	@touch $@
+
+$(SPSLR_VMLINUX_SPSLR_SECTION_ASM): $(SPSLR_VMLINUX_PATCHCOMPILE_STAMP)
+	@:
+
+$(SPSLR_VMLINUX_TARGET_MAP): $(SPSLR_VMLINUX_PATCHCOMPILE_STAMP)
+	@:
+
+$(SPSLR_VMLINUX_SPSLR_SECTION_OBJ): $(SPSLR_VMLINUX_SPSLR_SECTION_ASM) FORCE
+	$(call if_changed,spslr_as)
+
+vmlinux_o: $(SPSLR_VMLINUX_SPSLR_SECTION_OBJ)
+
+modules: $(SPSLR_VMLINUX_TARGET_MAP)
+
+endif
+
+CLEAN_FILES += $(SPSLR_METADIR) \
+	       $(SPSLR_VMLINUX_SPSLR_SECTION_ASM) \
+	       $(SPSLR_VMLINUX_SPSLR_SECTION_OBJ) \
+	       $(SPSLR_VMLINUX_SPSLR_LIST) \
+	       $(SPSLR_VMLINUX_TARGET_MAP) \
+	       $(SPSLR_VMLINUX_PATCHCOMPILE_STAMP)
+
+clean: tools/spslr_clean
+
 PHONY += vmlinux_o
 vmlinux_o: vmlinux.a $(KBUILD_VMLINUX_LIBS)
 	$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.vmlinux_o
diff --git a/arch/x86/boot/compressed/Makefile b/arch/x86/boot/compressed/Makefile
index f2051644de94..bd0685bb1ef3 100644
--- a/arch/x86/boot/compressed/Makefile
+++ b/arch/x86/boot/compressed/Makefile
@@ -44,6 +44,8 @@ KBUILD_CFLAGS += -D__DISABLE_EXPORTS
 KBUILD_CFLAGS += $(call cc-option,-Wa$(comma)-mrelax-relocations=no)
 KBUILD_CFLAGS += -include $(srctree)/include/linux/hidden.h
 
+KBUILD_CFLAGS += -std=gnu11
+
 # sev.c indirectly includes inat-table.h which is generated during
 # compilation and stored in $(objtree). Add the directory to the includes so
 # that the compiler finds it even with out-of-tree builds (make O=/some/path).
diff --git a/arch/x86/kernel/vmlinux.lds.S b/arch/x86/kernel/vmlinux.lds.S
index feb8102a9ca7..b5e5cfc1d9db 100644
--- a/arch/x86/kernel/vmlinux.lds.S
+++ b/arch/x86/kernel/vmlinux.lds.S
@@ -185,6 +185,14 @@ SECTIONS
 		/* rarely changed data like cpu maps */
 		READ_MOSTLY_DATA(INTERNODE_CACHE_BYTES)
 
+#ifdef CONFIG_SPSLR
+		. = ALIGN(8);
+		__spslr_start = .;
+		KEEP(*(.spslr))
+		KEEP(*(.spslr.*))
+		__spslr_end = .;
+#endif
+
 		/* End of data section */
 		_edata = .;
 	} :data
diff --git a/drivers/firmware/efi/libstub/Makefile b/drivers/firmware/efi/libstub/Makefile
index ed4e8ddbe76a..cfc99d5a0e80 100644
--- a/drivers/firmware/efi/libstub/Makefile
+++ b/drivers/firmware/efi/libstub/Makefile
@@ -20,6 +20,8 @@ cflags-$(CONFIG_X86)		+= -m$(BITS) -D__KERNEL__ \
 				   -fno-asynchronous-unwind-tables \
 				   $(CLANG_FLAGS)
 
+cflags-$(CONFIG_X86_64) += -std=gnu11 -Wno-address-of-packed-member
+
 # arm64 uses the full KBUILD_CFLAGS so it's necessary to explicitly
 # disable the stackleak plugin
 cflags-$(CONFIG_ARM64)		+= -fpie $(DISABLE_STACKLEAK_PLUGIN) \
diff --git a/include/linux/compiler_types.h b/include/linux/compiler_types.h
index 1a957ea2f4fe..167c106574e0 100644
--- a/include/linux/compiler_types.h
+++ b/include/linux/compiler_types.h
@@ -374,6 +374,18 @@ struct ftrace_likely_data {
 # define __latent_entropy
 #endif
 
+#if defined(__SPSLR__)
+# define __spslr __attribute__((spslr))
+# define __spslr_field_fixed __attribute__((spslr_field_fixed))
+# define spslr_struct_fields_start struct {
+# define spslr_struct_fields_end   } __spslr;
+#else
+# define __spslr
+# define __spslr_field_fixed
+# define spslr_struct_fields_start
+# define spslr_struct_fields_end
+#endif
+
 #if defined(RANDSTRUCT) && !defined(__CHECKER__)
 # define __randomize_layout __designated_init __attribute__((randomize_layout))
 # define __no_randomize_layout __attribute__((no_randomize_layout))
diff --git a/include/linux/sched.h b/include/linux/sched.h
index bb343136ddd0..427de1e2a787 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -793,6 +793,7 @@ struct task_struct {
 	 * scheduling-critical items should be added above here.
 	 */
 	randomized_struct_fields_start
+	spslr_struct_fields_start
 
 	void				*stack;
 	refcount_t			usage;
@@ -828,12 +829,12 @@ struct task_struct {
 	int				normal_prio;
 	unsigned int			rt_priority;
 
-	struct sched_entity		se;
-	struct sched_rt_entity		rt;
+	struct sched_entity		se __spslr_field_fixed;
+	struct sched_rt_entity		rt __spslr_field_fixed;
 	struct sched_dl_entity		dl;
 	struct sched_dl_entity		*dl_server;
 #ifdef CONFIG_SCHED_CLASS_EXT
-	struct sched_ext_entity		scx;
+	struct sched_ext_entity		scx __spslr_field_fixed;
 #endif
 	const struct sched_class	*sched_class;
 
@@ -865,7 +866,7 @@ struct task_struct {
 
 #ifdef CONFIG_PREEMPT_NOTIFIERS
 	/* List of struct preempt_notifier: */
-	struct hlist_head		preempt_notifiers;
+	struct hlist_head		preempt_notifiers __spslr_field_fixed;
 #endif
 
 #ifdef CONFIG_BLK_DEV_IO_TRACE
@@ -877,7 +878,7 @@ struct task_struct {
 	int				nr_cpus_allowed;
 	const cpumask_t			*cpus_ptr;
 	cpumask_t			*user_cpus_ptr;
-	cpumask_t			cpus_mask;
+	cpumask_t			cpus_mask __spslr_field_fixed;
 	void				*migration_pending;
 #ifdef CONFIG_SMP
 	unsigned short			migration_disabled;
@@ -887,7 +888,7 @@ struct task_struct {
 #ifdef CONFIG_PREEMPT_RCU
 	int				rcu_read_lock_nesting;
 	union rcu_special		rcu_read_unlock_special;
-	struct list_head		rcu_node_entry;
+	struct list_head		rcu_node_entry __spslr_field_fixed;
 	struct rcu_node			*rcu_blocked_node;
 #endif /* #ifdef CONFIG_PREEMPT_RCU */
 
@@ -896,25 +897,25 @@ struct task_struct {
 	u8				rcu_tasks_holdout;
 	u8				rcu_tasks_idx;
 	int				rcu_tasks_idle_cpu;
-	struct list_head		rcu_tasks_holdout_list;
+	struct list_head		rcu_tasks_holdout_list __spslr_field_fixed;
 	int				rcu_tasks_exit_cpu;
-	struct list_head		rcu_tasks_exit_list;
+	struct list_head		rcu_tasks_exit_list __spslr_field_fixed;
 #endif /* #ifdef CONFIG_TASKS_RCU */
 
 #ifdef CONFIG_TASKS_TRACE_RCU
 	int				trc_reader_nesting;
 	int				trc_ipi_to_cpu;
 	union rcu_special		trc_reader_special;
-	struct list_head		trc_holdout_list;
-	struct list_head		trc_blkd_node;
+	struct list_head		trc_holdout_list __spslr_field_fixed;
+	struct list_head		trc_blkd_node __spslr_field_fixed;
 	int				trc_blkd_cpu;
 #endif /* #ifdef CONFIG_TASKS_TRACE_RCU */
 
 	struct sched_info		sched_info;
 
-	struct list_head		tasks;
+	struct list_head		tasks __spslr_field_fixed;
 #ifdef CONFIG_SMP
-	struct plist_node		pushable_tasks;
+	struct plist_node		pushable_tasks __spslr_field_fixed;
 	struct rb_node			pushable_dl_tasks;
 #endif
 
@@ -1019,8 +1020,12 @@ struct task_struct {
 	pid_t				tgid;
 
 #ifdef CONFIG_STACKPROTECTOR
+	/* Canary can not be randomized because of arch/x86/kernel/asm-offsets.c
+	 * Pinpoint plugin could recognize context of instrumented accesses
+	 * and e.g. hijack asm instructions that want to use them as constants.
+	 */
 	/* Canary value for the -fstack-protector GCC feature: */
-	unsigned long			stack_canary;
+	unsigned long				stack_canary __spslr_field_fixed;
 #endif
 	/*
 	 * Pointers to the (original) parent process, youngest child, younger sibling,
@@ -1037,8 +1042,8 @@ struct task_struct {
 	/*
 	 * Children/sibling form the list of natural children:
 	 */
-	struct list_head		children;
-	struct list_head		sibling;
+	struct list_head		children __spslr_field_fixed;
+	struct list_head		sibling __spslr_field_fixed;
 	struct task_struct		*group_leader;
 
 	/*
@@ -1047,13 +1052,13 @@ struct task_struct {
 	 * This includes both natural children and PTRACE_ATTACH targets.
 	 * 'ptrace_entry' is this task's link on the p->parent->ptraced list.
 	 */
-	struct list_head		ptraced;
-	struct list_head		ptrace_entry;
+	struct list_head		ptraced __spslr_field_fixed;
+	struct list_head		ptrace_entry __spslr_field_fixed;
 
 	/* PID/PID hash table linkage. */
 	struct pid			*thread_pid;
 	struct hlist_node		pid_links[PIDTYPE_MAX];
-	struct list_head		thread_node;
+	struct list_head		thread_node __spslr_field_fixed;
 
 	struct completion		*vfork_done;
 
@@ -1157,7 +1162,7 @@ struct task_struct {
 	sigset_t			real_blocked;
 	/* Restored if set_restore_sigmask() was used: */
 	sigset_t			saved_sigmask;
-	struct sigpending		pending;
+	struct sigpending		pending __spslr_field_fixed;
 	unsigned long			sas_ss_sp;
 	size_t				sas_ss_size;
 	unsigned int			sas_ss_flags;
@@ -1273,7 +1278,7 @@ struct task_struct {
 	/* Control Group info protected by css_set_lock: */
 	struct css_set __rcu		*cgroups;
 	/* cg_list protected by css_set_lock and tsk->alloc_lock: */
-	struct list_head		cg_list;
+	struct list_head		cg_list __spslr_field_fixed;
 #endif
 #ifdef CONFIG_X86_CPU_RESCTRL
 	u32				closid;
@@ -1292,8 +1297,8 @@ struct task_struct {
 #ifdef CONFIG_PERF_EVENTS
 	u8				perf_recursion[PERF_NR_CONTEXTS];
 	struct perf_event_context	*perf_event_ctxp;
-	struct mutex			perf_event_mutex;
-	struct list_head		perf_event_list;
+	struct mutex			perf_event_mutex __spslr_field_fixed;
+	struct list_head		perf_event_list __spslr_field_fixed;
 #endif
 #ifdef CONFIG_DEBUG_PREEMPT
 	unsigned long			preempt_disable_ip;
@@ -1596,6 +1601,7 @@ struct task_struct {
 	 * New fields for task_struct should be added above here, so that
 	 * they are included in the randomized portion of task_struct.
 	 */
+	spslr_struct_fields_end
 	randomized_struct_fields_end
 
 	/* CPU-specific state of this task: */
diff --git a/init/Kconfig b/init/Kconfig
index c521e1421ad4..c0cf7fa7a491 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2052,3 +2052,10 @@ config ARCH_HAS_SYNC_CORE_BEFORE_USERMODE
 # <asm/syscall_wrapper.h>.
 config ARCH_HAS_SYSCALL_WRAPPER
 	def_bool n
+
+config SPSLR
+	bool "Selfpatch SLR prototype"
+	depends on X86_64
+	depends on CC_IS_GCC
+	help
+	  Experimental structure layout randomization prototype.
diff --git a/init/main.c b/init/main.c
index c4778edae797..f2c4e606fa8a 100644
--- a/init/main.c
+++ b/init/main.c
@@ -114,6 +114,8 @@
 
 #include <kunit/test.h>
 
+#include <linux/spslr.h>
+
 static int kernel_init(void *);
 
 /*
@@ -953,6 +955,19 @@ void start_kernel(void)
 	/* Architectural and non-timekeeping rng init, before allocator init */
 	random_init_early(command_line);
 
+#ifdef CONFIG_SPSLR
+	/* Randomize structure layouts */
+	struct spslr_status spslr_init_status = spslr_init();
+	if (spslr_init_status.error != SPSLR_OK)
+		panic("SPSLR initialization failed");
+
+	struct spslr_status spslr_selfpatch_status = spslr_selfpatch();
+	if (spslr_selfpatch_status.error != SPSLR_OK)
+		panic("SPSLR selfpatch failed");
+
+	pr_notice("Successfully applied SPSLR\n");
+#endif
+
 	/*
 	 * These use large bootmem allocations and must precede
 	 * initalization of page allocator
diff --git a/kernel/module/main.c b/kernel/module/main.c
index 49b9bca9de12..209030881b8a 100644
--- a/kernel/module/main.c
+++ b/kernel/module/main.c
@@ -65,6 +65,8 @@
 #define CREATE_TRACE_POINTS
 #include <trace/events/module.h>
 
+#include <linux/spslr.h>
+
 /*
  * Mutex protects:
  * 1) List of modules (also safely readable with preempt_disable),
@@ -2847,6 +2849,59 @@ static int early_mod_check(struct load_info *info, int flags)
 	return err;
 }
 
+/* Find spslr symbol in module without kallsym */
+static unsigned long spslr_find_module_symbol(const struct load_info *info,
+					      const char *name)
+{
+	Elf_Shdr *symsec = &info->sechdrs[info->index.sym];
+	Elf_Sym *sym = (void *)symsec->sh_addr;
+	unsigned int i, n = symsec->sh_size / sizeof(*sym);
+
+	for (i = 1; i < n; i++) {
+		const char *symname = info->strtab + sym[i].st_name;
+
+		if (strcmp(symname, name) != 0)
+			continue;
+
+		/* Ignore undefined symbols just in case. */
+		if (sym[i].st_shndx == SHN_UNDEF)
+			return 0;
+
+		return (unsigned long)sym[i].st_value;
+	}
+
+	return 0;
+}
+
+/* Apply structure layout randomization to module */
+static int __maybe_unused spslr_prepare_module(struct module *mod, const struct load_info *info)
+{
+	struct spslr_module sm = { };
+	struct spslr_status st;
+
+	sm.ipin_cnt = (const void *)spslr_find_module_symbol(info, SPSLR_MODULE_SYM_IPIN_CNT);
+	sm.ipins = (const void *)spslr_find_module_symbol(info, SPSLR_MODULE_SYM_IPINS);
+	sm.ipin_op_cnt = (const void *)spslr_find_module_symbol(info, SPSLR_MODULE_SYM_IPIN_OP_CNT);
+	sm.ipin_ops = (const void *)spslr_find_module_symbol(info, SPSLR_MODULE_SYM_IPIN_OPS);
+	sm.dpin_cnt = (const void *)spslr_find_module_symbol(info, SPSLR_MODULE_SYM_DPIN_CNT);
+	sm.dpins = (const void *)spslr_find_module_symbol(info, SPSLR_MODULE_SYM_DPINS);
+
+	if (!sm.ipin_cnt || !sm.ipins || !sm.ipin_op_cnt ||
+	    !sm.ipin_ops || !sm.dpin_cnt || !sm.dpins) {
+		pr_err("%s: SPSLR metadata incomplete\n", mod->name);
+		return -ENOEXEC;
+	}
+
+	st = spslr_patch_module(&sm);
+	if (st.viability != SPSLR_VIABLE || st.error != SPSLR_OK) {
+		pr_err("%s: SPSLR patch failed: viability=%d error=%d\n",
+		       mod->name, st.viability, st.error);
+		return -ENOEXEC;
+	}
+
+	return 0;
+}
+
 /*
  * Allocate and load the module: note that size of section 0 is always
  * zero, and we rely on this for optional sections.
@@ -2950,6 +3005,13 @@ static int load_module(struct load_info *info, const char __user *uargs,
 	if (err < 0)
 		goto free_modinfo;
 
+#ifdef CONFIG_SPSLR
+	/* SPSLR must happen after relocation and icache should be flushed afterwards */
+	err = spslr_prepare_module(mod, info);
+	if (err < 0)
+		goto free_modinfo;
+#endif
+
 	flush_module_icache(mod);
 
 	/* Now copy in args */
diff --git a/scripts/Makefile.build b/scripts/Makefile.build
index 8f423a1faf50..59fc7489d9eb 100644
--- a/scripts/Makefile.build
+++ b/scripts/Makefile.build
@@ -95,10 +95,15 @@ endif
 # Compile C sources (.c)
 # ---------------------------------------------------------------------------
 
+ifdef SPSLR_TOOL_PINPOINT
+$(SPSLR_TOOL_PINPOINT):
+	$(Q)$(MAKE) -f $(srctree)/Makefile $(SPSLR_TOOL_PINPOINT)
+endif
+
 quiet_cmd_cc_s_c = CC $(quiet_modtag)  $@
       cmd_cc_s_c = $(CC) $(filter-out $(DEBUG_CFLAGS) $(CC_FLAGS_LTO), $(c_flags)) -fverbose-asm -S -o $@ $<
 
-$(obj)/%.s: $(obj)/%.c FORCE
+$(obj)/%.s: $(obj)/%.c $(if $(CONFIG_SPSLR),$(SPSLR_TOOL_PINPOINT)) FORCE
 	$(call if_changed_dep,cc_s_c)
 
 quiet_cmd_cpp_i_c = CPP $(quiet_modtag) $@
@@ -225,7 +230,7 @@ define rule_as_o_S
 endef
 
 # Built-in and composite module parts
-$(obj)/%.o: $(obj)/%.c $(recordmcount_source) FORCE
+$(obj)/%.o: $(obj)/%.c $(recordmcount_source) $(if $(CONFIG_SPSLR),$(SPSLR_TOOL_PINPOINT)) FORCE
 	$(call if_changed_rule,cc_o_c)
 	$(call cmd,force_checksrc)
 
diff --git a/scripts/Makefile.modfinal b/scripts/Makefile.modfinal
index 1482884ec3ca..5e5f203b1d6d 100644
--- a/scripts/Makefile.modfinal
+++ b/scripts/Makefile.modfinal
@@ -33,6 +33,128 @@ quiet_cmd_cc_o_c = CC [M]  $@
 $(extmod_prefix).module-common.o: $(srctree)/scripts/module-common.c FORCE
 	$(call if_changed_dep,cc_o_c)
 
+ifdef CONFIG_SPSLR
+
+quiet_cmd_gen_spslr_mod_list = SPSLRLST [M] $@
+      cmd_gen_spslr_mod_list = set -e; \
+	mod_o=$(@:.spslr-files=.o); \
+	modfile=$(@:.spslr-files=.mod); \
+	tmp=$@.tmp; \
+	found=0; \
+	rm -f "$$tmp"; \
+	add_src() { \
+		src="$$1"; \
+		objctx="$$2"; \
+		case "$$src" in \
+			*.c) \
+				spslr="$(SPSLR_METADIR)/$$src.spslr"; \
+				if [ -f "$$spslr" ]; then \
+					printf '%s\n' "$$spslr" >> "$$tmp"; \
+					found=1; \
+				else \
+					echo "Missing SPSLR sidecar for $$objctx from $$src: $$spslr" >&2; \
+					exit 1; \
+				fi; \
+				;; \
+			*.S) \
+				: ;; \
+			*) \
+				echo "Unsupported source for $$objctx: $$src" >&2; \
+				exit 1; \
+				;; \
+		esac; \
+	}; \
+	add_obj_from_cmd() { \
+		obj="$$1"; \
+		cmdfile="$$(dirname "$$obj")/.$$(basename "$$obj").cmd"; \
+		if [ -f "$$cmdfile" ]; then \
+			src="$$(sed -n 's/^source_.* := //p' "$$cmdfile" | head -n 1)"; \
+			if [ -n "$$src" ]; then \
+				add_src "$$src" "$$obj"; \
+				return 0; \
+			fi; \
+		fi; \
+		return 1; \
+	}; \
+	add_obj_direct() { \
+		obj="$$1"; \
+		src_c="$${obj%.o}.c"; \
+		src_S="$${obj%.o}.S"; \
+		if [ -f "$$src_c" ]; then \
+			add_src "$$src_c" "$$obj"; \
+		elif [ -f "$$src_S" ]; then \
+			: ; \
+		else \
+			echo "Could not determine SPSLR input for $$obj: no .cmd source entry, no $$src_c, and no $$src_S" >&2; \
+			exit 1; \
+		fi; \
+	}; \
+	if add_obj_from_cmd "$$mod_o"; then \
+		: ; \
+	elif [ -f "$$modfile" ]; then \
+		while read tok; do \
+			case "$$tok" in \
+				"" ) \
+					continue ;; \
+				*.mod.o|*.spslr_module.o) \
+					continue ;; \
+				*.o) \
+					if add_obj_from_cmd "$$tok"; then \
+						: ; \
+					else \
+						add_obj_direct "$$tok"; \
+					fi; \
+					;; \
+				*) \
+					echo "Unexpected entry in $$modfile: $$tok" >&2; \
+					exit 1; \
+					;; \
+			esac; \
+		done < "$$modfile"; \
+	else \
+		add_obj_direct "$$mod_o"; \
+	fi; \
+	if [ "$$found" = 1 ]; then \
+		awk '!seen[$$0]++' "$$tmp" > $@; \
+		rm -f "$$tmp"; \
+	else \
+		rm -f "$$tmp"; \
+		: > $@; \
+		echo "WARNING: no .spslr files found for module $$mod_o" >&2; \
+	fi
+
+quiet_cmd_spslr_patchcompile_mod = SPSLR   [M] $@
+      cmd_spslr_patchcompile_mod = set -e; \
+	if [ ! -s $(SPSLR_VMLINUX_TARGET_MAP) ]; then \
+		echo "Missing SPSLR target map: $(SPSLR_VMLINUX_TARGET_MAP)" >&2; \
+		exit 1; \
+	fi; \
+	if [ ! -s $< ]; then \
+		echo "Empty module SPSLR manifest: $<" >&2; \
+		exit 1; \
+	fi; \
+	$(SPSLR_TOOL_PATCHCOMPILE) \
+		--module \
+		--no-new-targets \
+		--load-targets $(SPSLR_VMLINUX_TARGET_MAP) \
+		--verbose \
+		--out $@ \
+		$$(cat $<)
+
+quiet_cmd_spslr_as_mod = AS      [M] $@
+      cmd_spslr_as_mod = $(CC) $(KBUILD_AFLAGS) $(NOSTDINC_FLAGS) $(LINUXINCLUDE) -DMODULE -c -o $@ $<
+
+%.spslr-files: %.o FORCE
+	$(call if_changed,gen_spslr_mod_list)
+
+%.spslr_module.S: %.spslr-files $(SPSLR_VMLINUX_TARGET_MAP) FORCE
+	$(call if_changed,spslr_patchcompile_mod)
+
+%.spslr_module.o: %.spslr_module.S FORCE
+	$(call if_changed,spslr_as_mod)
+
+endif
+
 quiet_cmd_ld_ko_o = LD [M]  $@
       cmd_ld_ko_o =							\
 	$(LD) -r $(KBUILD_LDFLAGS)					\
@@ -57,13 +179,18 @@ if_changed_except = $(if $(call newer_prereqs_except,$(2))$(cmd-check),      \
 	printf '%s\n' 'savedcmd_$@ := $(make-cmd)' > $(dot-target).cmd, @:)
 
 # Re-generate module BTFs if either module's .ko or vmlinux changed
-%.ko: %.o %.mod.o $(extmod_prefix).module-common.o scripts/module.lds $(and $(CONFIG_DEBUG_INFO_BTF_MODULES),$(KBUILD_BUILTIN),vmlinux) FORCE
+%.ko: %.o %.mod.o $(extmod_prefix).module-common.o $(if $(CONFIG_SPSLR),%.spslr_module.o) scripts/module.lds $(and $(CONFIG_DEBUG_INFO_BTF_MODULES),$(KBUILD_BUILTIN),vmlinux) FORCE
 	+$(call if_changed_except,ld_ko_o,vmlinux)
 ifdef CONFIG_DEBUG_INFO_BTF_MODULES
 	+$(if $(newer-prereqs),$(call cmd,btf_ko))
 endif
 
-targets += $(modules:%.o=%.ko) $(modules:%.o=%.mod.o) $(extmod_prefix).module-common.o
+targets += $(modules:%.o=%.ko) \
+	   $(modules:%.o=%.mod.o) \
+	   $(modules:%.o=%.spslr-files) \
+	   $(modules:%.o=%.spslr_module.S) \
+	   $(modules:%.o=%.spslr_module.o) \
+	   $(extmod_prefix).module-common.o
 
 # Add FORCE to the prerequisites of a target to force it to be always rebuilt.
 # ---------------------------------------------------------------------------
diff --git a/scripts/Makefile.vmlinux b/scripts/Makefile.vmlinux
index 1284f05555b9..afb723954150 100644
--- a/scripts/Makefile.vmlinux
+++ b/scripts/Makefile.vmlinux
@@ -24,6 +24,10 @@ endif
 
 ARCH_POSTLINK := $(wildcard $(srctree)/arch/$(SRCARCH)/Makefile.postlink)
 
+ifdef CONFIG_SPSLR
+vmlinux: $(SPSLR_VMLINUX_SPSLR_SECTION_OBJ)
+endif
+
 # Final link of vmlinux with optional arch pass after final link
 cmd_link_vmlinux =							\
 	$< "$(LD)" "$(KBUILD_LDFLAGS)" "$(LDFLAGS_vmlinux)";		\
diff --git a/scripts/Makefile.vmlinux_o b/scripts/Makefile.vmlinux_o
index 0b6e2ebf60dc..fd1483f5b793 100644
--- a/scripts/Makefile.vmlinux_o
+++ b/scripts/Makefile.vmlinux_o
@@ -54,6 +54,7 @@ quiet_cmd_ld_vmlinux.o = LD      $@
 	$(addprefix -T , $(initcalls-lds)) \
 	--whole-archive vmlinux.a --no-whole-archive \
 	--start-group $(KBUILD_VMLINUX_LIBS) --end-group \
+	$(SPSLR_VMLINUX_SPSLR_SECTION_OBJ) \
 	$(cmd_objtool)
 
 define rule_ld_vmlinux.o
diff --git a/scripts/link-vmlinux.sh b/scripts/link-vmlinux.sh
index a9b3f34a78d2..603a90811257 100755
--- a/scripts/link-vmlinux.sh
+++ b/scripts/link-vmlinux.sh
@@ -66,6 +66,11 @@ vmlinux_link()
 	else
 		objs=vmlinux.a
 		libs="${KBUILD_VMLINUX_LIBS}"
+
+		# Only add it here because the spslr section is already contained in vmlinux.o
+		if is_enabled CONFIG_SPSLR; then
+			objs="${objs} ${SPSLR_VMLINUX_SPSLR_SECTION_OBJ}"
+		fi
 	fi
 
 	if is_enabled CONFIG_MODULES; then
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [RFC 6/7] BPSLR tasklist sample module
  2026-06-05 20:25 [RFC 0/7] Bootpatch-SLR: Randomizing Linux Kernel Structure Layouts at Boot York Jasper Niebuhr
                   ` (4 preceding siblings ...)
  2026-06-05 20:25 ` [RFC 5/7] BPSLR task_struct integration York Jasper Niebuhr
@ 2026-06-05 20:25 ` York Jasper Niebuhr
  2026-06-05 20:25 ` [RFC 7/7] Ignore BPSLR generated files York Jasper Niebuhr
                   ` (2 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: York Jasper Niebuhr @ 2026-06-05 20:25 UTC (permalink / raw)
  To: linux-hardening; +Cc: kees, franzen

Signed-off-by: York Jasper Niebuhr <yjn@yjn-systems.com>
---
 samples/Kconfig                   |  3 ++
 samples/Makefile                  |  1 +
 samples/spslr/Kconfig             | 17 ++++++++
 samples/spslr/Makefile            |  1 +
 samples/spslr/tasklist/Makefile   |  1 +
 samples/spslr/tasklist/tasklist.c | 68 +++++++++++++++++++++++++++++++
 6 files changed, 91 insertions(+)
 create mode 100644 samples/spslr/Kconfig
 create mode 100644 samples/spslr/Makefile
 create mode 100644 samples/spslr/tasklist/Makefile
 create mode 100644 samples/spslr/tasklist/tasklist.c

diff --git a/samples/Kconfig b/samples/Kconfig
index b288d9991d27..0619cc57870e 100644
--- a/samples/Kconfig
+++ b/samples/Kconfig
@@ -293,6 +293,8 @@ config SAMPLE_CGROUP
 
 source "samples/rust/Kconfig"
 
+source "samples/spslr/Kconfig"
+
 endif # SAMPLES
 
 config HAVE_SAMPLE_FTRACE_DIRECT
@@ -300,3 +302,4 @@ config HAVE_SAMPLE_FTRACE_DIRECT
 
 config HAVE_SAMPLE_FTRACE_DIRECT_MULTI
 	bool
+
diff --git a/samples/Makefile b/samples/Makefile
index b85fa64390c5..193a5e9f9c1e 100644
--- a/samples/Makefile
+++ b/samples/Makefile
@@ -39,3 +39,4 @@ obj-$(CONFIG_SAMPLE_KMEMLEAK)		+= kmemleak/
 obj-$(CONFIG_SAMPLE_CORESIGHT_SYSCFG)	+= coresight/
 obj-$(CONFIG_SAMPLE_FPROBE)		+= fprobe/
 obj-$(CONFIG_SAMPLES_RUST)		+= rust/
+obj-$(CONFIG_SAMPLES_SPSLR)		+= spslr/
diff --git a/samples/spslr/Kconfig b/samples/spslr/Kconfig
new file mode 100644
index 000000000000..0b25cf62a190
--- /dev/null
+++ b/samples/spslr/Kconfig
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: GPL-2.0
+
+menuconfig SAMPLES_SPSLR
+	bool "SPSLR samples"
+	depends on SPSLR
+	help
+	  You can build sample Rust kernel code here.
+
+	  If unsure, say N.
+
+if SAMPLES_SPSLR
+
+config SAMPLE_SPSLR_TASKLIST
+	tristate "SPSLR tasklist module example"
+	depends on m
+
+endif # SAMPLES_SPSLR
diff --git a/samples/spslr/Makefile b/samples/spslr/Makefile
new file mode 100644
index 000000000000..2d330b105c95
--- /dev/null
+++ b/samples/spslr/Makefile
@@ -0,0 +1 @@
+obj-$(CONFIG_SAMPLE_SPSLR_TASKLIST) += tasklist/
diff --git a/samples/spslr/tasklist/Makefile b/samples/spslr/tasklist/Makefile
new file mode 100644
index 000000000000..6a5bc5b29b34
--- /dev/null
+++ b/samples/spslr/tasklist/Makefile
@@ -0,0 +1 @@
+obj-m += tasklist.o
diff --git a/samples/spslr/tasklist/tasklist.c b/samples/spslr/tasklist/tasklist.c
new file mode 100644
index 000000000000..e42e3f182286
--- /dev/null
+++ b/samples/spslr/tasklist/tasklist.c
@@ -0,0 +1,68 @@
+#include <linux/module.h>
+#include <linux/kernel.h>
+#include <linux/init.h>
+#include <linux/sched/signal.h>
+#include <linux/sched/task.h>
+#include <linux/cred.h>
+#include <linux/uidgid.h>
+#include <linux/module.h>
+#include <linux/list.h>
+
+static const struct task_struct module_target_data = { .flags = 42 };
+
+static int __init taskinfo_init(void)
+{
+    struct task_struct *p;
+
+    pr_info("taskinfo: loaded\n");
+    pr_info("    offsetof(task_struct, pid)=%lu\n", offsetof(struct task_struct, pid));
+    pr_info("    offsetof(task_struct, tgid)=%lu\n", offsetof(struct task_struct, tgid));
+    pr_info("    offsetof(task_struct, cred)=%lu\n", offsetof(struct task_struct, cred));
+    pr_info("    offsetof(task_struct, real_parent)=%lu\n", offsetof(struct task_struct, real_parent));
+    pr_info("    offsetof(task_struct, comm)=%lu\n", offsetof(struct task_struct, comm));
+    pr_info("    offsetof(task_struct, __state)=%lu\n", offsetof(struct task_struct, __state));
+    pr_info("    offsetof(task_struct, flags)=%lu\n", offsetof(struct task_struct, flags));
+    pr_info("    offsetof(task_struct, prio)=%lu\n", offsetof(struct task_struct, prio));
+    pr_info("    offsetof(task_struct, policy)=%lu\n", offsetof(struct task_struct, policy));
+
+    pr_info("datapin flags value is %u (should be 42)\n", module_target_data.flags);
+
+    pr_info("=== Task List ===\n");
+
+    rcu_read_lock();
+
+    for_each_process(p) {
+        const struct cred *cred;
+        struct task_struct *parent;
+
+        cred = rcu_dereference(p->cred);
+        parent = rcu_dereference(p->real_parent);
+
+        pr_info("task: pid=%d tgid=%d ppid=%d uid=%u gid=%u comm=%s state=%u flags=0x%x prio=%d policy=%u\n",
+                p->pid,
+                p->tgid,
+                parent ? parent->tgid : -1,
+                __kuid_val(cred->uid),
+                __kgid_val(cred->gid),
+                p->comm,
+                READ_ONCE(p->__state),
+                p->flags,
+                p->prio,
+                p->policy);
+    }
+
+    rcu_read_unlock();
+
+    return 0;
+}
+
+static void __exit taskinfo_exit(void)
+{
+    pr_info("taskinfo: unloaded\n");
+}
+
+module_init(taskinfo_init);
+module_exit(taskinfo_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("List modules and tasks (task_struct stress)");
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [RFC 7/7] Ignore BPSLR generated files
  2026-06-05 20:25 [RFC 0/7] Bootpatch-SLR: Randomizing Linux Kernel Structure Layouts at Boot York Jasper Niebuhr
                   ` (5 preceding siblings ...)
  2026-06-05 20:25 ` [RFC 6/7] BPSLR tasklist sample module York Jasper Niebuhr
@ 2026-06-05 20:25 ` York Jasper Niebuhr
  2026-06-05 20:35 ` [GCC PATCH] Add PLUGIN_BUILD_COMPONENT_REF callback York Jasper Niebuhr
  2026-06-10 20:12 ` [RFC 0/7] Bootpatch-SLR: Randomizing Linux Kernel Structure Layouts at Boot Kees Cook
  8 siblings, 0 replies; 10+ messages in thread
From: York Jasper Niebuhr @ 2026-06-05 20:25 UTC (permalink / raw)
  To: linux-hardening; +Cc: kees, franzen

Signed-off-by: York Jasper Niebuhr <yjn@yjn-systems.com>
---
 .gitignore | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/.gitignore b/.gitignore
index 56972adb5031..75ab3befd8a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -177,3 +177,10 @@ sphinx_*/
 
 # Rust analyzer configuration
 /rust-project.json
+
+# SPSLR generated files
+spslr_section.S
+*.spslr-files
+*.spslr_module.S
+tools/spslr/pinpoint.so
+tools/spslr/patchcompile
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [GCC PATCH] Add PLUGIN_BUILD_COMPONENT_REF callback
  2026-06-05 20:25 [RFC 0/7] Bootpatch-SLR: Randomizing Linux Kernel Structure Layouts at Boot York Jasper Niebuhr
                   ` (6 preceding siblings ...)
  2026-06-05 20:25 ` [RFC 7/7] Ignore BPSLR generated files York Jasper Niebuhr
@ 2026-06-05 20:35 ` York Jasper Niebuhr
  2026-06-10 20:12 ` [RFC 0/7] Bootpatch-SLR: Randomizing Linux Kernel Structure Layouts at Boot Kees Cook
  8 siblings, 0 replies; 10+ messages in thread
From: York Jasper Niebuhr @ 2026-06-05 20:35 UTC (permalink / raw)
  To: linux-hardening; +Cc: kees, franzen, York Jasper Niebuhr

This patch adds the PLUGIN_BUILD_COMPONENT_REF callback, which is
required by Selfpatch-SLR tooling. It is based on mainline GCC 16.

The callback is invoked by the C front end when a COMPONENT_REF node is
built. The callback receives a pointer to the COMPONENT_REF tree (of
type 'tree *'). Plugins may replace the node by assigning through the
pointer, but any replacement must be type-compatible with the original
node.

The callback allows plugins to observe or instrument struct member
accesses that would otherwise be lost due to folding before the earliest
possible plugin pass or hook. In particular, the fold_offsetof
functionality removes all traces of type and member information in
offsetof-like trees, leaving only an integer constant for plugins to
inspect.

A considered alternative was to disable fold_offsetof altogether.
However, that prevents offsetof expressions from qualifying as
constant-expressions; for example, static assertions can no longer be
evaluated if they contain non-folded offsetof expressions. The callback
provides fine-grained control over individual COMPONENT_REFs instead of
universally changing folding behavior.

Allowing PLUGIN_BUILD_COMPONENT_REF to alter COMPONENT_REF nodes required
minor adjustments to fold_offsetof, which assumes a specific input
format. Code paths that cannot guarantee that format should now use
fold_offsetof_maybe(), which attempts to fold normally but, on failure,
casts the unfolded expression to the desired output type.

If the callback is not used to alter COMPONENT_REF trees, there is **no
change** in GCC’s behavior.

Signed-off-by: York Jasper Niebuhr <yjn@yjn-systems.com>
---
 gcc/c-family/c-common.cc | 48 +++++++++++++++++++++++++++++++---------
 gcc/c-family/c-common.h  |  3 ++-
 gcc/c/c-parser.cc        |  2 +-
 gcc/c/c-typeck.cc        | 11 +++++++++
 gcc/doc/plugins.texi     |  6 +++++
 gcc/plugin.cc            |  2 ++
 gcc/plugin.def           |  6 +++++
 7 files changed, 65 insertions(+), 13 deletions(-)

diff --git a/gcc/c-family/c-common.cc b/gcc/c-family/c-common.cc
index 5acb221d31f..63f7158cde9 100644
--- a/gcc/c-family/c-common.cc
+++ b/gcc/c-family/c-common.cc
@@ -7223,10 +7223,11 @@ c_common_to_target_charset (HOST_WIDE_INT c)
    the whole expression.  Return the folded result.  */
 
 tree
-fold_offsetof (tree expr, tree type, enum tree_code ctx)
+fold_offsetof (tree expr, tree type, enum tree_code ctx, bool may_fail)
 {
   tree base, off, t;
   tree_code code = TREE_CODE (expr);
+
   switch (code)
     {
     case ERROR_MARK:
@@ -7243,33 +7244,37 @@ fold_offsetof (tree expr, tree type, enum tree_code ctx)
      break;
 
     case VAR_DECL:
-      error ("cannot apply %<offsetof%> to static data member %qD", expr);
+      if (!may_fail)
+	error ("cannot apply %<offsetof%> to static data member %qD", expr);
       return error_mark_node;
 
     case CALL_EXPR:
     case TARGET_EXPR:
-      error ("cannot apply %<offsetof%> when %<operator[]%> is overloaded");
+      if (!may_fail)
+	error ("cannot apply %<offsetof%> when %<operator[]%> is overloaded");
       return error_mark_node;
 
     case NOP_EXPR:
     case INDIRECT_REF:
       if (!TREE_CONSTANT (TREE_OPERAND (expr, 0)))
 	{
-	  error ("cannot apply %<offsetof%> to a non constant address");
+	  if (!may_fail)
+	    error ("cannot apply %<offsetof%> to a non constant address");
 	  return error_mark_node;
 	}
       return convert (type, TREE_OPERAND (expr, 0));
 
     case COMPONENT_REF:
-      base = fold_offsetof (TREE_OPERAND (expr, 0), type, code);
+      base = fold_offsetof (TREE_OPERAND (expr, 0), type, code, may_fail);
       if (base == error_mark_node)
 	return base;
 
       t = TREE_OPERAND (expr, 1);
       if (DECL_C_BIT_FIELD (t))
 	{
-	  error ("attempt to take address of bit-field structure "
-		 "member %qD", t);
+	  if (!may_fail)
+	    error ("attempt to take address of bit-field structure "
+		   "member %qD", t);
 	  return error_mark_node;
 	}
       off = size_binop_loc (input_location, PLUS_EXPR, DECL_FIELD_OFFSET (t),
@@ -7278,7 +7283,7 @@ fold_offsetof (tree expr, tree type, enum tree_code ctx)
       break;
 
     case ARRAY_REF:
-      base = fold_offsetof (TREE_OPERAND (expr, 0), type, code);
+      base = fold_offsetof (TREE_OPERAND (expr, 0), type, code, may_fail);
       if (base == error_mark_node)
 	return base;
 
@@ -7335,17 +7340,38 @@ fold_offsetof (tree expr, tree type, enum tree_code ctx)
     case COMPOUND_EXPR:
       /* Handle static members of volatile structs.  */
       t = TREE_OPERAND (expr, 1);
-      gcc_checking_assert (VAR_P (get_base_address (t)));
-      return fold_offsetof (t, type);
+      if (!VAR_P (get_base_address (t)))
+	return error_mark_node;
+      return fold_offsetof (t, type, ERROR_MARK, may_fail);
 
     default:
-      gcc_unreachable ();
+      return error_mark_node;
     }
 
   if (!POINTER_TYPE_P (type))
     return size_binop (PLUS_EXPR, base, convert (type, off));
   return fold_build_pointer_plus (base, off);
 }
+
+/* Tries folding expr using fold_offsetof.  On success, the folded offsetof
+   is returned.  On failure, the original expr is wrapped in an ADDR_EXPR
+   and converted to the desired expression type.  The resulting expression
+   may or may not be constant!  */
+
+tree
+fold_offsetof_maybe (tree expr, tree type)
+{
+  /* expr might not have the correct structure, thus folding may fail.  */
+  tree maybe_folded = fold_offsetof (expr, type, ERROR_MARK, true);
+  if (maybe_folded != error_mark_node)
+    return maybe_folded;
+
+  tree ptr_type = build_pointer_type (TREE_TYPE (expr));
+  tree ptr = build1 (ADDR_EXPR, ptr_type, expr);
+
+  return fold_convert (type, ptr);
+}
+
 \f
 /* *PTYPE is an incomplete array.  Complete it with a domain based on
    INITIAL_VALUE.  If INITIAL_VALUE is not present, use 1 if DO_DEFAULT
diff --git a/gcc/c-family/c-common.h b/gcc/c-family/c-common.h
index 89517e2a80c..13e1a00ce88 100644
--- a/gcc/c-family/c-common.h
+++ b/gcc/c-family/c-common.h
@@ -1184,7 +1184,8 @@ extern bool c_dump_tree (void *, tree);
 extern void verify_sequence_points (tree);
 
 extern tree fold_offsetof (tree, tree = size_type_node,
-			   tree_code ctx = ERROR_MARK);
+			   tree_code ctx = ERROR_MARK, bool may_fail = false);
+extern tree fold_offsetof_maybe (tree, tree = size_type_node);
 
 extern int complete_array_type (tree *, tree, bool);
 extern void complete_flexible_array_elts (tree);
diff --git a/gcc/c/c-parser.cc b/gcc/c/c-parser.cc
index 0b8b2387109..5e4d65814e1 100644
--- a/gcc/c/c-parser.cc
+++ b/gcc/c/c-parser.cc
@@ -12170,7 +12170,7 @@ c_parser_postfix_expression (c_parser *parser)
 	    location_t end_loc = c_parser_peek_token (parser)->get_finish ();
 	    c_parser_skip_until_found (parser, CPP_CLOSE_PAREN,
 				       "expected %<)%>");
-	    expr.value = fold_offsetof (offsetof_ref);
+	    expr.value = fold_offsetof_maybe (offsetof_ref);
 	    set_c_expr_source_range (&expr, loc, end_loc);
 	  }
 	  break;
diff --git a/gcc/c/c-typeck.cc b/gcc/c/c-typeck.cc
index aa368a6e95a..232bf487d21 100644
--- a/gcc/c/c-typeck.cc
+++ b/gcc/c/c-typeck.cc
@@ -55,6 +55,7 @@ along with GCC; see the file COPYING3.  If not see
 #include "realmpfr.h"
 #include "tree-pretty-print-markup.h"
 #include "gcc-urlifier.h"
+#include "plugin.h"
 
 /* Possible cases of implicit conversions.  Used to select diagnostic messages
    and control folding initializers in convert_for_assignment.  */
@@ -3440,6 +3441,16 @@ build_component_ref (location_t loc, tree datum, tree component,
 	  else if (TREE_DEPRECATED (subdatum))
 	    warn_deprecated_use (subdatum, NULL_TREE);
 
+      tree pre_cb_type = TREE_TYPE (ref);
+      if (invoke_plugin_callbacks (PLUGIN_BUILD_COMPONENT_REF, &ref)
+	      == PLUGEVT_SUCCESS
+	      && !comptypes (TREE_TYPE (ref), pre_cb_type))
+	{
+	  error_at (EXPR_LOCATION (ref),
+		    "PLUGIN_BUILD_COMPONENT_REF callback returned"
+		    " expression of incompatible type");
+	}
+
 	  datum = ref;
 
 	  field = TREE_CHAIN (field);
diff --git a/gcc/doc/plugins.texi b/gcc/doc/plugins.texi
index ff0b5866f25..5e808013899 100644
--- a/gcc/doc/plugins.texi
+++ b/gcc/doc/plugins.texi
@@ -218,6 +218,12 @@ enum plugin_event
    as a const char* pointer.  */
   PLUGIN_INCLUDE_FILE,
 
+  /* Called by the C front end when a COMPONENT_REF node is built.  The
+     callback receives a pointer to the COMPONENT_REF tree (of type 'tree *').
+     Plugins may replace the node by assigning through the pointer, but any
+     replacement must be type-compatible with the original node.  */
+  PLUGIN_BUILD_COMPONENT_REF,
+
   PLUGIN_EVENT_FIRST_DYNAMIC    /* Dummy event used for indexing callback
                                    array.  */
 @};
diff --git a/gcc/plugin.cc b/gcc/plugin.cc
index 436be5e0147..3ea21956335 100644
--- a/gcc/plugin.cc
+++ b/gcc/plugin.cc
@@ -499,6 +499,7 @@ register_callback (const char *plugin_name,
       case PLUGIN_EARLY_GIMPLE_PASSES_END:
       case PLUGIN_NEW_PASS:
       case PLUGIN_INCLUDE_FILE:
+      case PLUGIN_BUILD_COMPONENT_REF:
         {
           struct callback_info *new_callback;
           if (!callback)
@@ -579,6 +580,7 @@ invoke_plugin_callbacks_full (int event, void *gcc_data)
       case PLUGIN_EARLY_GIMPLE_PASSES_END:
       case PLUGIN_NEW_PASS:
       case PLUGIN_INCLUDE_FILE:
+      case PLUGIN_BUILD_COMPONENT_REF:
         {
           /* Iterate over every callback registered with this event and
              call it.  */
diff --git a/gcc/plugin.def b/gcc/plugin.def
index b2fd27fa6e1..d44d45385b9 100644
--- a/gcc/plugin.def
+++ b/gcc/plugin.def
@@ -99,6 +99,12 @@ DEFEVENT (PLUGIN_NEW_PASS)
    as a const char* pointer.  */
 DEFEVENT (PLUGIN_INCLUDE_FILE)
 
+/* Called by the C front end when a COMPONENT_REF node is built.
+   The callback receives a pointer to the COMPONENT_REF tree (of type 'tree *').
+   Plugins may replace the node by assigning through the pointer, but any
+   replacement must be type-compatible with the original node.  */
+DEFEVENT (PLUGIN_BUILD_COMPONENT_REF)
+
 /* When adding a new hard-coded plugin event, don't forget to edit in
    file plugin.cc the functions register_callback and
    invoke_plugin_callbacks_full accordingly!  */
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: [RFC 0/7] Bootpatch-SLR: Randomizing Linux Kernel Structure Layouts at Boot
  2026-06-05 20:25 [RFC 0/7] Bootpatch-SLR: Randomizing Linux Kernel Structure Layouts at Boot York Jasper Niebuhr
                   ` (7 preceding siblings ...)
  2026-06-05 20:35 ` [GCC PATCH] Add PLUGIN_BUILD_COMPONENT_REF callback York Jasper Niebuhr
@ 2026-06-10 20:12 ` Kees Cook
  8 siblings, 0 replies; 10+ messages in thread
From: Kees Cook @ 2026-06-10 20:12 UTC (permalink / raw)
  To: York Jasper Niebuhr; +Cc: linux-hardening, franzen, Ard Biesheuvel

On Fri, Jun 05, 2026 at 10:25:04PM +0200, York Jasper Niebuhr wrote:
> my name is York Jasper Niebuhr. I am a Master's student in Informatics
> at the Technical University of Munich.
> 
> For a while now, I have been developing Selfpatch-SLR (SPSLR), a system
> to apply Structure Layout Randomization at runtime, and its Linux kernel
> implementation, Bootpatch-SLR (BPSLR), as an independent research
> project in my spare time. I am now beginning my Master's thesis, which
> will build upon and further evaluate this work. This patch series
> contains the current state of Bootpatch-SLR.

Thanks for working on this! I asked the Sashiko folks to give this
series a review, just so you have some more immediate feedback:
https://sashiko.dev/#/patchset/20260605202511.79272-1-yjn%40yjn-systems.com

I'd love to get Ard's feedback on this series too; he's done a lot of
poking around at layouts, annotation sections, etc.

As was mentioned at the KSPP meeting (thank you for attending!) one
aspect for further down the road will be getting this implemented
directly in GCC and Clang (since no new GCC plugins will be accepted in
Linux). But in the meantime, I think it would be best to move the plugin
into the existing GCC plugin infrastructure in the kernel (see
scripts/gcc-plugins/), as that should better control when it gets built,
make dependencies, and when it gets enabled for various build stages.

> The design goal of SPSLR is to eliminate the need for runtime lookup
> tables or similar indirection mechanisms. Structure access instructions
> are patched directly to reflect the randomized field offsets before
> normal kernel execution begins.

The implementation looks very similar to relocation fixups; is it
possible to create a new relocation type for this so it more naturally
fits into the ELF image? The .data/.rodata annotations likely need to
remain a separate section, similar to how existing things like fault
instrumentation works (i.e. from the sanitizers for things like KCFI,
UBSAN, etc).

>  * Patch 7 ignores BPSLR-generated files.

You can just fold this into the patches that add the build outputs.

> I have also attempted to port BPSLR to v6.18 and later releases. While I
> got the system to build, occasional runtime failures currently prevent
> successful operation.

I do wonder if getting the plugin merged into Linux's gcc plugin
infrastructure would solve this (i.e. it would get run/not run at the
right times, etc). I suppose it's possible it won't help, of course.

> In particular, I would be interested in pointers to areas that are
> likely to require special handling, as well as assumptions made by the
> kernel that may not be obvious to someone approaching the problem from
> outside the subsystem in question.

Targeting task_struct is quite a challenge! I wonder if you might want
to start with a different subset of the __randomize_layout-marked
structs? I know there are a bunch of filesystem and network structs that
maybe aren't quite as sensitive to early boot like I imagine task_struct
is.

What did you need to make exceptions for in the task_struct? I suspect
that may also be a hint as to why getting this ported to the latest
kernel isn't easy?

-Kees

-- 
Kees Cook

^ permalink raw reply	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2026-06-10 20:12 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-05 20:25 [RFC 0/7] Bootpatch-SLR: Randomizing Linux Kernel Structure Layouts at Boot York Jasper Niebuhr
2026-06-05 20:25 ` [RFC 1/7] SPSLR pinpoint plugin source York Jasper Niebuhr
2026-06-05 20:25 ` [RFC 2/7] SPSLR patchcompile cli source York Jasper Niebuhr
2026-06-05 20:25 ` [RFC 3/7] SPSLR tool build integration York Jasper Niebuhr
2026-06-05 20:25 ` [RFC 4/7] SPSLR selfpatch York Jasper Niebuhr
2026-06-05 20:25 ` [RFC 5/7] BPSLR task_struct integration York Jasper Niebuhr
2026-06-05 20:25 ` [RFC 6/7] BPSLR tasklist sample module York Jasper Niebuhr
2026-06-05 20:25 ` [RFC 7/7] Ignore BPSLR generated files York Jasper Niebuhr
2026-06-05 20:35 ` [GCC PATCH] Add PLUGIN_BUILD_COMPONENT_REF callback York Jasper Niebuhr
2026-06-10 20:12 ` [RFC 0/7] Bootpatch-SLR: Randomizing Linux Kernel Structure Layouts at Boot Kees Cook

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.