kasld recovers a running kernel's memory layout — primarily the kernel text
base — from a local process, using as much as its vantage (privileges, groups,
capabilities, system configuration, and any container confinement) allows. Run
it with no arguments and it prints an answer: the recovered (or narrowed)
virtual and physical image base, the direct map base, and the leaks the answer
was derived from.
Underneath, kasld gathers evidence from many small leak components (each a
standalone technique that probes one source) and feeds it to an inference engine
that narrows the layout to the smallest set of placements the evidence supports,
reporting every value with its provenance and any residual entropy. In normal use
that machinery is invisible — a single invocation prints the result. This
document covers the output modes and command-line options; for how the pieces fit
together see architecture.md.
- Quick start
- Vantage
- Command-line options
- Output modes
- Explain mode
- Hardening assessment
- Continuous integration
sudo apt install libc-dev make gcc binutils git
git clone https://github.com/bcoles/kasld
cd kasld
make
./build/<arch>/kasldThe build/<arch>/ directory is self-contained and can be deployed to a
target system:
build/<arch>/
kasld <- run this
components/ <- leak components
A hardened configuration (kernel.dmesg_restrict=1, kernel.kptr_restrict=1,
kernel.perf_event_paranoid=2 or higher, kernel.unprivileged_bpf_disabled=1,
and %pK pointer hashing, on by default) narrows the filesystem-oracle path. It is one axis of the vantage
only: side-channel, weak-entropy, and capability-granted techniques are
independent of these sysctls. For testing purposes, the
extra/weaken-kernel-hardening script
can temporarily relax these settings (requires root).
What kasld recovers depends on the process's vantage — three independent
axes, not a single privilege ladder: privileges and capabilities, system
configuration, and confinement. The hardened sysctls above are only one of
them. A leak source is reachable only where every axis that gates it permits, so
more privilege is not a superset of less — configuration can deny a source to
root, and the side-channel and capability-granted techniques bypass the sysctls
entirely:
The -v, -j, and -m outputs report the detected vantage (container,
confinement, readable oracles, and the capability-gated leaks reachable from the
current capabilities). A richer vantage lets kasld attempt more and gather more
evidence, but the guaranteed (sound) window reflects only the evidence actually
proven — never the privilege that gathered it — so it stays trustworthy whatever
the vantage. See limitations.md.
-j, --json Machine-readable JSON output
-1, --oneline Single-line summary output (shell-pipeable)
-m, --markdown Markdown table output (for issue trackers)
-c, --color Colorize text output (auto-detected for TTYs)
-a, --ascii ASCII-only output: no Unicode glyphs or banner
(auto-enabled in a non-UTF-8 locale)
-q, --quiet Suppress banner, progress, and warnings
-v, --verbose Add banner, system-config block, per-component logs,
per-region Results table, KASLR analysis, and the
virtual + physical memory-layout maps. The default
text mode prints a tight ~15-line answer-first
readout; -v restores the full detail.
-e, --explain Show technique explanations before each component
-f, --fast Use 2s per-component timeout (fast scan mode)
-w, --workers N Parallel component workers (default: nproc; 0 = sequential)
-x, --experimental Enable experimental components
-s, --skip PATTERN Skip matching components (glob, comma-separated; multiple --skip flags accumulate)
-H, --hardening Append the hardening assessment to text/markdown output
--map Draw the address-space diagram (implied by --verbose)
-t, --timeout N Per-component timeout in seconds (default: 30)
-V, --version Print version and exit
-h, --help Show this help
Single-dash short flags may be bundled: -fq is -f -q, -vj is -v -j. A
value-taking flag (-s/-t/-w) may appear only as the last flag in a bundle,
taking the next argument — -fqt 2 is -f -q -t 2.
The text output uses a few Unicode glyphs (✓ ⚠ ✗ →) and a box-art banner
under -v. These are auto-disabled in a non-UTF-8 locale (LANG=C, legacy
consoles), and --ascii (-a) forces plain ASCII regardless — for screen
readers on a UTF-8 system, or non-UTF-8 pipelines. The JSON (-j) and one-line
(-1) formats carry no glyphs and are the fully accessible, machine-readable
interfaces.
Colour follows the standard environment conventions: NO_COLOR (any value)
disables it, CLICOLOR=0 disables it, and CLICOLOR_FORCE (non-empty, not 0)
forces it on even when piped. An explicit --color overrides NO_COLOR.
The default text mode prints a tight answer-first readout: a one-line header, the Layout table, the virt/phys coupling note, the leaks the answers were derived from, and a hint about the verbose mode. No banner, no system config, no memory-layout diagram.
The Layout table carries one row per quantity and basis:
| Column | What it holds |
|---|---|
Quantity |
what is being located — always a base, a single address, not a region |
Basis |
guaranteed (proven; contains the true base) or likely (the all-signals estimate, a subset of the guaranteed window, and may be wrong) |
Range |
the addresses: a window, a single address where the quantity is pinned, or a one-sided >= / <= bound. A concrete base carries its slide from the compile-time default |
Search space |
how many placements remain, against the set the row narrows — a guaranteed row against the window the kernel randomized over, a likely row against the guaranteed count above it. Reported whether or not evidence narrowed it, so a baseline run states the size of the problem; the denominator is dropped when nothing narrowed, leaving the bare total. - means no window is modelled for the quantity, which is the only thing that withholds the figure |
Align |
the grid the candidates sit on, which is what reconciles the count with the range |
The likely basis is deliberately conservative — "may be wrong" is a worst-case
caveat, not a coin toss. KASLD is build-agnostic: it never trusts a version string
or fingerprint to pin a base, so a signal that would fix the base on a specific
build — a leaked symbol pointer read as the base (e.g. perf_event_open), a
matched compile-time default — is still held below the sound floor to likely,
because the engine cannot assume it is looking at that build. Against a known
target (known hardware, known kernel and configuration) those deterministic
likely results are in practice effectively certain; the genuinely probabilistic
part is the hardware side channels (prefetch, entrybleed, …), whose success
varies from run to run. So a likely row is a floor on confidence, read together
with the source that produced it: a parsed leak is near-certain on a known target;
a timing oracle is the part that can miss.
Every quantity the architecture randomizes gets a row whether or not the engine bounded it, so the set of rows is a property of the machine rather than of the run — rows do not appear and vanish between boots. A row the engine never bounded says so rather than printing the architectural window, which would put a compile-time constant where a reader expects a measurement.
Quantities the architecture does not randomize follow a second rule: they get a row once the engine has bounded them, and none otherwise. Two are in this class.
The module region exists everywhere, and is resolved from whichever directions
the architecture affords. Where its placement is fully determined the row is a
single address: a fixed segment base (MIPS), a base computed from the hardware
virtual-address width (LoongArch), one selected by the translation mode and
page size (ppc64), or one derived from a resolved PAGE_OFFSET (arm32, ppc32,
riscv32). Where something is randomized or unknown it is a
window instead, narrowed by the architecture's module band, by the allocator's
own placement window (on x86_64 the base sits within 1024 pages of
MODULES_VADDR), by the resolved text base where the region is anchored to the
image (riscv64, s390), and by the lowest module address a leak discloses, which
caps where the region can start.
No leak is required for any of that: the sample above resolves the row to 1025 candidates with no module address in evidence at all.
The direct-map base is likewise fixed off x86_64, but still resolved: on a
32-bit kernel it is the VMSPLIT, read from the boot config or narrowed by an
mmap probe. Neither quantity is randomized there, so neither sits in a
randomization window — but each still has an architectural bracket, so both rows
are always present and state it. Where the pitch is not a modelled randomization
granule the Align column reads - rather than inventing a grid, and the search
space follows it.
Addresses are never zero-padded: a 16 MiB physical address would otherwise wear the costume of a 64-bit kernel pointer. They are right-aligned instead, so the endpoints still form columns.
KASLD 0.3.1-dev -- Kernel ASLR derandomization
Target: x86_64 / 7.0.0
Running 106 of 109 components (3 experimental skipped; use -x to enable)...
[####################] 100% 106/106 39.1s
1 component timed out after 30s and was killed (prefetch_directmap)
Quantity Basis Range Search space Align
------------------- ---------- --------------------------------------- ------------ -----
Virtual Image Base guaranteed 0xffffffffa2e00000 slide +0x21e00000 1 of 505 2 MiB
Physical Image Base guaranteed 0x200000 - 0x3d400000 481 2 MiB
Physical Image Base likely 0x1000000 - 0x3c345000 474 of 481 2 MiB
Direct Map Base guaranteed 0xffff800000000000 - 0xffffa4aa80000000 37547 1 GiB
Vmalloc Base guaranteed 0xffff898000000000 - 0xffffd6d580000000 79191 1 GiB
Vmemmap Base guaranteed 0xffffa98040000000 - 0xfffffd0000000000 85504 1 GiB
Module Region Base guaranteed 0xffffffffc0000000 - 0xffffffffc0400000 1025 4 KiB
Note: physical and virtual text randomize independently
Evidence (2 findings, 4 components)
virt kernel text [interior] 0xffffffffa309cab5
from perf_event_open, perf_text_poke_leak, proc_kallsyms
virt kernel image [base] 0xffffffffa2e00000
from perf_event_open, prefetch, proc_kallsyms
[-v: detailed results, memory map, system info] [-H: hardening assessment]
Terms in this readout (slide, directmap, coupling, slot, search space) are defined in the kaslr.md glossary; the engine vocabulary behind them (quantity, estimate, honest top) is in the architecture.md glossary.
-v (--verbose) restores the full banner, system-config block,
per-component logs, per-region "Results" table, KASLR analysis section,
and the virtual + physical address-space diagram (also available on its own
via --map). The KASLR analysis section draws the same Layout table the
default readout does, then adds only what its columns cannot carry: _stext
where a quantity has one, the compile-time default a slide is measured from,
and the residual expressed in bits.
The system-config block reports the recon vantage: whether the process is
containerized, which mandatory-access-control policy is in force, the security
context and the uid/gid/groups this process runs under, and — when it is
confined — its seccomp / capability / no-new-privs state, plus which /proc
leak oracles are readable here.
The identity is not redundant with the
capability set: several sources answer to discretionary permissions alone, so a
denial is only explicable with the uid and groups that were refused in view.
Group names come from /etc/group in the tree being analysed, so an offline
replay names that tree's groups rather than the analysing host's; the ids kasld
knows gate one of its own sources are named even where the tree cannot name
them, which is the case on Android, where the file exists but is empty and the
ids live inside the C library:
Click to expand verbose example
▄█ ▄█▄ ▄████████ ▄████████ ▄█ ████████▄
███ ▄███▀ ███ ███ ███ ███ ███ ███ ▀███
███▐██▀ ███ ███ ███ █▀ ███ ███ ███
▄█████▀ ███ ███ ███ ███ ███ ███
▀▀█████▄ ▀███████████ ▀███████████ ███ ███ ███
███▐██▄ ███ ███ ███ ███ ███ ███
███ ▀███▄ ███ ███ ▄█ ███ ███▌ ▄ ███ ▄███
███ ▀█▀ ███ █▀ ▄████████▀ █████▄▄██ ████████▀
▀ ▀ v0.3.1-dev
Kernel release: 6.15.6
Kernel version: #1 SMP PREEMPT_DYNAMIC Wed Jun 17 13:04:17 EDT 2026
Kernel arch: x86_64
kernel.kptr_restrict: 0
kernel.dmesg_restrict: 0
kernel.panic_on_oops: 0
kernel.perf_event_paranoid: -1
Kernel lockdown: (unavailable)
Container: none
LSM: lockdown,capability,landlock,yama,apparmor
Security context: unconfined
Identity: uid=1000 gid=1000
Supplementary groups: 4(adm),24(cdrom),27(sudo),1000(user)
Readable /proc/kallsyms: yes
Readable /proc/kcore: no
Readable /proc/iomem: yes
Readable /proc/modules: yes
Readable /var/log/dmesg: no
Readable /var/log/kern.log: no
Readable /var/log/syslog: no
Readable debugfs: yes
Readable /boot/System.map: no
Readable /boot/config: no
--- (per-component probe logs trimmed for brevity) ---
[engine] virt_image_base: constrained by 5 independent sources: ceiling_from_image_size image_floor_from_init_size range_from_interior physical_start_lower_bound text_pin_from_observation
[engine] phys_image_base: constrained by 13 independent sources: ceiling_from_image_size phys_ceiling_from_memtotal phys_bits_ceiling mmio_floor_phys_ceiling phys_hole_filter kernel_image_phys_bound initrd_phys_exclude phys_reservation_exclude ram_map_phys_exclude initrd_above_kernel cmdline_phys_exclude physical_start_lower_bound text_pin_from_observation
[engine] virt_kaslr_align: constrained by 2 independent sources: kaslr_align_arch_default boot_params_kaslr_align
[engine] phys_kaslr_align: constrained by 2 independent sources: kaslr_align_arch_default boot_params_kaslr_align
Components: 94 total, 24 succeeded, 26 unavailable, 44 no result
========================================
Results
========================================
Kernel text (virtual) / kernel_text [2]:
0xffffffff8fe00000 kernel_text:_stext [base] (proc_kallsyms)
0xffffffff900a9fc9 kernel_text [interior] (perf_event_open)
==> 0xffffffff8fe00000 (method: parsed, 1 source, 1 conflict)
range: 0xffffffff8fe00000 - 0xffffffff900a9fc9 (2.7 MiB)
Kernel text (virtual) / kernel_image [3]:
0xffffffff8fe00000 kernel_image:_text [base] (proc_kallsyms)
0xffffffff8fe00000 kernel_image [base] (prefetch)
0xffffffff90000000 kernel_image [base] (perf_event_open)
==> 0xffffffff8fe00000 (method: parsed, 2 sources, 1 conflict)
range: 0xffffffff8fe00000 - 0xffffffff90000000 (2.0 MiB)
----------------------------------------
Kernel text (physical) / kernel_image [1]:
0x0000000034600000 kernel_image:kernel_code [base] (proc_iomem_kernel)
==> 0x0000000034600000 (method: parsed, 1 source)
----------------------------------------
Kernel data (physical) / kernel_data [1]:
0x0000000036000000 kernel_data:kernel_data [base] (proc_iomem_kernel)
==> 0x0000000036000000 (method: parsed, 1 source)
----------------------------------------
Kernel BSS (physical) / kernel_bss [1]:
0x0000000036b34000 kernel_bss:kernel_bss [base] (proc_iomem_kernel)
==> 0x0000000036b34000 (method: parsed, 1 source)
----------------------------------------
Physical DRAM / ram [6]:
0x0000000000000000 ram (boot_params_e820)
0x0000000000000000 ram (firmware_memmap)
0x0000000000001000 ram [interior] (dmesg_free_area_init_node, proc_zoneinfo)
0x0000000000100000 ram [base] (boot_params_e820, dmesg_e820_memory_map, dmesg_free_area_init_node, dmesg_last_pfn, proc_zoneinfo, sysfs_firmware_memmap)
0x0000000000100000 ram (firmware_memmap)
0x0000000000100000 ram (boot_params_e820)
==> 0x0000000000100000 (method: parsed, 3 sources, 3 conflicts)
range: 0x0000000000000000 - 0x0000000000100000 (1.0 MiB)
----------------------------------------
Physical DRAM / initrd [1]:
0x000000003efc2000 initrd [base] (boot_params_e820, dmesg_ramdisk)
==> 0x000000003efc2000 (method: parsed, 1 source)
----------------------------------------
Physical DRAM / cmdline [1]:
0x0000000000020000 cmdline [base] (cmdline_region)
==> 0x0000000000020000 (method: parsed, 1 source)
----------------------------------------
Physical DRAM / numa_node [1]:
0x000000003ffdefff numa_node [interior] (dmesg_node_data)
==> 0x000000003ffdefff (method: parsed, 1 source)
----------------------------------------
Physical DRAM / vmcoreinfo [1]:
0x00000000011ee000 vmcoreinfo [interior] (sysfs_vmcoreinfo)
==> 0x00000000011ee000 (method: parsed, 1 source)
----------------------------------------
Physical MMIO / pci_mmio [8]:
0x00000000000c0000 pci_mmio:0000:00:01.0 [base] (sysfs_pci_resource)
0x00000000fd000000 pci_mmio:0000:00:01.0 [base] (sysfs_pci_resource)
0x00000000feb40000 pci_mmio:0000:00:02.0 [base] (sysfs_pci_resource)
0x00000000feb80000 pci_mmio:0000:00:02.0 [base] (sysfs_pci_resource)
0x00000000feba0000 pci_mmio:0000:00:02.0 [base] (sysfs_pci_resource)
0x00000000febd0000 pci_mmio:0000:00:02.0 [base] (sysfs_pci_resource)
0x00000000febd4000 pci_mmio:0000:00:01.0 [base] (sysfs_pci_resource)
0x00000000febd5000 pci_mmio:0000:00:1f.2 [base] (sysfs_pci_resource)
==> 0x00000000000c0000 (method: parsed, 1 source, 7 conflicts)
range: 0x00000000000c0000 - 0x00000000febd5000 (4.0 GiB)
----------------------------------------
KASLR analysis:
Quantity Basis Range Search space Align
------------------- ---------- --------------------------------------- ------------ -----
Virtual Image Base guaranteed 0xffffffff8fe00000 slide +0xee00000 1 of 505 2 MiB
Physical Image Base guaranteed 0x34600000 slide +0x33600000 1 2 MiB
Direct Map Base guaranteed >= 0xffff800000000000 - 1 GiB
Vmalloc Base guaranteed 0xffff810040000000 - 0xffffdcffc0000000 94206 1 GiB
Vmemmap Base guaranteed 0xffffa10080000000 - 0xfffffd0000000000 94206 1 GiB
Compile-time default: 0xffffffff81000000
Virtual entropy: ~0 of 9 bits
Physical entropy: ~0 bits
Direct map entropy: ~17 bits
----------------------------------------
Virtual address space (decoupled, not to scale):
0xffffffffffffffff
. . . 16 MiB gap . . .
0xffffffffff000000
modules (no leak)
0xffffffffc0000000
. . . 767.3 MiB gap . . .
0xffffffff900a9fc9
kernel text
leak hi: 0xffffffff900a9fc9
leak lo: 0xffffffff8fe00000
0xffffffff8fe00000
. . . 128 TiB gap . . .
^ extent unknown
direct map (base is a lower bound)
0xffff800000000000
. . . 65408 TiB gap . . .
0xff00000000000000 (user space + non-canonical hole below)
Physical address space (not to scale):
0xfebd5000
above DRAM
0xfebd5000 [mmio] pci_mmio:0000:00:1f.2
0xfebd4000 [mmio] pci_mmio:0000:00:01.0
0xfebd0000 [mmio] pci_mmio:0000:00:02.0
0xfeba0000 [mmio] pci_mmio:0000:00:02.0
0xfeb80000 [mmio] pci_mmio:0000:00:02.0
0xfeb40000 [mmio] pci_mmio:0000:00:02.0
0xfd000000 [mmio] pci_mmio:0000:00:01.0
0x3ffdefff
in DRAM
0x3efc2000 [dram] initrd
0x36b34000 [bss] kernel_bss:kernel_bss
0x36000000 [data] kernel_data:kernel_data
0x34600000 [text] kernel
0x11ee000 [dram] vmcoreinfo
0xc0000 [mmio] pci_mmio:0000:00:01.0
0x20000 [dram] cmdline
0x1000 [dram] ram
0x0
The slot and bit figures in the KASLR-analysis block are computed as
slots = range / alignment-step and bits = ceil(log2(slots)) — the count of
alignment-aligned positions spanning the inferred range, and the rounded-up
base-2 logarithm of that count. The bits are an upper bound on residual
entropy: they assume every surviving slot is equally likely, whereas real KASLR
placement is slightly non-uniform (see
bypass-techniques.md — Weak entropy), so the
true entropy is marginally lower. 0 bits (a single surviving slot) means the
value is fully recovered.
These figures bound what KASLD recovered from the current vantage. A wide residual or an empty result is not a security assurance — it reflects what the implemented techniques could do here and now, not that the base is beyond reach. See limitations.md for what a negative or partial result does and does not imply.
-1 (--oneline) produces a single shell-pipeable line with a fixed
key set — every key below appears on every line, in this order, so a
scraper can match field= unconditionally. A value that is unresolved
or not applicable to the arch/run renders the sentinel na (never a
fabricated, defaulted, or leaked value):
arch=x86_64 kaslr=on text=0xffffffffa2e00000 stext=na slide=+0x21e00000(568328192) entropy=0bits ptext=na pstext=na pslide=na pentropy=9bits dmap=0xffff800000000000 dram=[0x0..0x3ffdefff](1023.9 MiB) results=27
| Key | Meaning |
|---|---|
arch |
kernel machine (uname), or unknown |
kaslr |
on | off | unsupported | failed (failed = randomization failed at boot: effective 0 bits, deterministic per boot — distinct from off, a deliberate opt-out at the link-time default) |
text |
virtual image base (_text); engine-resolved, never a leak. This is the best concrete answer, which may rest on a sub-floor signal; the line carries no speculative marker, so read entropy alongside it — 0bits means one surviving candidate, i.e. proven |
stext |
virtual _stext, when it differs from the image base |
slide |
virtual KASLR slide, signed ±0xHEX(decimal) |
entropy |
virtual residual entropy over the guaranteed window, Nbits; present whenever a window was resolved — an unpinned window reports its N bits, a pin reports 0bits. na only when KASLR is off/unsupported |
ptext |
physical image base (_text) |
pstext |
physical _stext, when it differs from the physical image base |
pslide |
physical KASLR slide (decoupled arches only) |
pentropy |
physical residual entropy (same window / na rule as entropy) |
dmap |
direct-map base (PAGE_OFFSET): the engine-resolved pin or proven floor, never the compile-time constant — except where the architecture guarantees that constant is the runtime value. na when neither is established |
dram |
physical DRAM extent, [0xLO..0xHI](size) |
results |
count of merged result records (not the raw component count) |
na carries the same "no value asserted" guarantee the human formats
express by omitting a row; extra/ksymoff anchors on text=0x…, so a
text=na line is correctly treated as having no derivable base.
-j (--json) emits the complete structured view: every block
documented below is always present, whatever the other flags — a machine
consumer keys on presence-of-key, not on which flags were passed. This makes
one -j blob the full, self-contained posture snapshot a fleet/CI layer can
diff against a baseline or aggregate across hosts. (--verbose is the sole
addition: it appends each component's raw stdout output lines.) See
docs/exploitation.md for how the JSON plugs into an exploit —
control-flow and data-only strategies, a pwntools template, and ksymoff.
The KASLR object reports two windows plus a headline base. The key names differ from the text labels; the mapping is:
| Concept | JSON key | Text label |
|---|---|---|
| Guaranteed window (sound floor; contains the true base) | inferred / inferred_physical |
guaranteed in the Layout table (readout and -v draw the same one) |
| Likely window (all signals; a subset of the guaranteed window; may be wrong) | likely / likely_physical (with "speculative": true) |
likely in the Layout table |
| Headline concrete base | virtual / physical → image_base |
"Virtual / Physical image base" |
So inferred* is the guaranteed window and likely* is the speculative
best-guess, always contained within it. Memory-KASLR regions
(memory_kaslr) carry the same guaranteed min/max and an optional
nested likely object.
A non-zero exit can replace the report rather than accompany it: on exit 3 the
document is {"error": {...}}, carrying no layout or kaslr key so that no
field of it can be mistaken for a measurement. Check the status before parsing;
kasld(1) EXIT STATUS gives the codes and what each output mode writes.
The environment object is the recon vantage: container, seccomp, lsm,
selinux, security_context, mac_enforcing, capabilities,
no_new_privs, uid, euid, gid, egid, groups (null when unreadable,
with groups_truncated when the process holds more than the report keeps),
group_gated_sources (those of them kasld knows gate a source it reads, named),
and a readable_oracles map for the /proc leak sources
(fields are a null or enum when they do not apply). lsm, selinux and
security_context are null when this vantage cannot read them, which is not
the same as their being absent — an enforcing policy commonly hides its own
state. mac_enforcing is the only one of the four that asserts anything: it is
true only where a policy was observed actively confining this process.
The groups array carries the leak evidence, one object per (type,
section, region) — the same split the text readout prints as separate
blocks. A group's aggregate describes only the region it names: consensus
(the most base-like address), consensus_method, consensus_sources,
conflicts, interior_only and the lo/hi span are computed over that
region's records alone, never across the other regions sharing its section.
Several regions routinely share one section — dram alone spans ram,
initrd, cmdline, acpi_table and more, whose bases are unrelated — so
section is not a unique key and region is what tells the groups apart.
Each group's results array lists its own records; valid marks whether a
record passes the layout bounds check.
The components array holds one record per component — name,
exit_code, outcome, an optional disposition (why a component produced no
tagged result: category — mitigation / absent / disabled /
inconclusive — plus, for a mitigation, the gate it confirmed and an optional
message), and the parsed meta from KASLD_META (including cve / patch /
config / sysctl keys). The hardening object is described under
Hardening assessment.
A per-component patch worklist — {component, cve, fixed_in, leaked_here}
— is a direct projection of the components array:
kasld -j | jq '[.components[]
| select(.meta.cve or .meta.patch)
| {component: .name, cve: .meta.cve, fixed_in: .meta.patch,
leaked_here: (.outcome == "success")}]'-m (--markdown) formats the summary for issue trackers (GitHub / GitLab
markdown tables). It carries the same Layout table as the text readout,
with the same five columns and the same rows — both are rendered from one row
model, so the two views cannot describe the same resolved state differently.
Beneath it a short table holds what those columns have no room for: _stext
where a quantity has one, the compile-time default a slide is measured from,
and the Phys/Virt coupling classification. When the kernel-text function
order is reordered (FG-KASLR / LTO / AutoFDO / Propeller), a Caution note
warns that a leaked address no longer resolves the rest of the symbols via a
generic System.map.
When KASLR is disabled or unsupported there is no slide,
so the resolved quantities themselves are the answer: the same rows the Layout
table would carry, written as lines rather than a table because with nothing
randomized the Search space and Align columns hold nothing. They are drawn
from the same row model, so a posture that reports fewer quantities than another
format is not possible. A remark follows where the compile-time default is not
the resolved base, saying whether the evidence rules the default out.
The leak table credits the component(s) that produced each address. With
--verbose, a ## Address space section
embeds the virtual and physical ASCII address-space maps in a fenced code
block (the same diagrams the text readout draws). An ## Environment
section reports the recon vantage (container / confinement / readable
oracles). With -H it also appends the hardening assessment (see below).
The --explain (-e) flag prints a brief technique explanation before
each component runs. Each component embeds a plain-text explanation in a
dedicated ELF section (.kasld_explain) via the KASLD_EXPLAIN() macro.
The orchestrator reads this section from the binary without executing it
and displays it inline.
This mode implies --verbose.
$ ./kasld --explain
...
[dmesg_free_reserved_area]
Searches dmesg for 'Freeing ... memory' messages from free_reserved_area()
that print kernel virtual addresses. These messages were removed in v4.10.
On older kernels, they reveal kernel text and init section virtual addresses.
Access is gated by dmesg_restrict.
-> unavailable (feature/hardware not present)
...
The --hardening (-H) flag appends a post-run hardening assessment that
evaluates the system's KASLR defenses based on the component results and
their machine-readable metadata. It opens with Confirmed active mitigations
(shown when present) — controls a component observed to defeat its leak this
run, keyed by the gate (kpti, an MDS hardware fix, a hardening CONFIG); this
is the runtime-observed complement to the sysctl gates, and appears in json as
hardening.confirmed_mitigations. It is followed by seven analysis sections:
-
KASLR posture (only when degraded) — surfaces a runtime KASLR state that downgrades effective slot entropy to 0 bits. Fires on "randomization failed" boot conditions (the boot stub attempted KASLR but could not produce a random offset: missing entropy seed, no PRNG, insufficient memory). The kernel still relocates but lands at a firmware-/boot-stub-deterministic position rather than the link-time default — meaningfully different from a deliberate opt-out, which the main results banner already reports. The full set of runtime states is catalogued in kaslr.md — KASLR runtime states. Omitted when KASLR is healthy or opted out.
-
Active defenses — runtime security settings detected on the system (
dmesg_restrict,kptr_restrict,perf_event_paranoid,unprivileged_bpf_disabled,%pKpointer hashing, lockdown mode) and their current values. -
Available hardening — actionable suggestions for settings that are not currently active but would block one or more successful components (e.g. "Set
kernel.dmesg_restrict= 1" if dmesg-based leaks succeeded). When the engine resolves a guaranteed base window, each suggestion is scored by re-resolving it: the section anchors on the current versus fully-hardened residual entropy, then reports, per suggestion, how much of that gap it is load-bearing for. The verdicts are load-bearing — omitting forfeits N bits (closing the others is not enough without this one), recovers nothing (the gate governs components but none leak the base), speculative window only (the leaks do not narrow the guaranteed base — the honest reading on a host whose guaranteed posture is already maxed), and not required (the base is recoverable, but the remaining suggestions already reach the same guaranteed posture). The numbers are deliberately non-additive: redundant leaks each read as load-bearing because closing any single one still leaves the base pinned. -
Patched vulnerabilities — components that target known CVEs. Shows how many are patched (returned no result or unavailable) versus unpatched (successfully leaked), with CVE identifiers and patch versions.
-
Compile-time attack surface — successful components that exploit kernel features enabled at compile time (e.g.
CONFIG_E820_TABLE,CONFIG_EFI), grouped by address type (physical vs. virtual). -
Hardware side-channels — successful components that exploit CPU microarchitectural side channels (prefetch, EntryBleed, ZombieLoad, etc.), grouped by hardware mitigation status.
-
No known mitigation — successful components with no known sysctl gate, lockdown restriction, CVE, or kernel config dependency. These represent leak vectors that cannot be blocked by runtime hardening alone.
When the kernel-text function order can be determined, the assessment also
prints a Function layout block above these sections: text ordering
(canonical, or reordered static / per-boot) and symbol resolution (whether a
generic System.map resolves symbols, or only this build's does). Reordered
text is the FG-KASLR / reordered-text class
(LTO, AutoFDO, Propeller, or FG-KASLR): functions no longer sit at a constant
offset from _text, so a leaked address pins only its own symbol and a generic
System.map no longer locates the rest.
In JSON, the assessment is the top-level hardening object, with fields
exposure, kaslr_posture (state is one of
active / disabled / unsupported / randomization_failed),
active_defenses, lockdown, available_hardening,
patched_vulnerabilities, compile_time_surface,
hardware_side_channels, and no_mitigation.
Each active_defenses and available_hardening entry carries a
surface — the enforcement lever the change lives on (sysctl,
boot_param, lsm, mac, file_permissions, or seccomp) — so a report can
route each item to the team that owns it. Kernel lockdown and a MAC policy are
both LSMs but are different levers, so they carry lsm and mac separately.
A denial is credited to the mac surface only when the component declares
sysctl gates and none of them accounts for it, so an ordinary file-permission
denial is never reported as a policy decision.
When the engine resolves a guaranteed base window, each available_hardening
entry also carries silences (base-leaks it removes) and a projected
object (the residual entropy with every other suggestion applied, and the
bits forfeited by omitting this one), and a top-level projected_posture
reports the current and all_suggestions_applied postures.
Markdown output (-m -H) appends the same assessment as a
## Hardening Assessment section; each Available-hardening suggestion
ends with its enforcement surface as a trailing [`lever`] tag
(e.g. [`sysctl`], [`file_permissions`]).
KASLD has no built-in pass/fail flag: it measures, and the CI script
decides. A single -j blob carries every value a policy would key on —
including the whole hardening object — so a regression gate is a one-line
jq predicate, more flexible than a baked-in threshold, and it composes into
any policy. A distro or kernel builder can fail the build when a freshly
built kernel's KASLR posture regresses below policy. The same blob is the unit
a fleet layer baselines and diffs: capture one per host, compare a later run,
and alert on any guaranteed-bit regression.
The fields a gate keys on:
.kaslr.inferred.entropy_bits— the guaranteed residual entropy of the kernel base: the bits KASLD could not strip with certainty. Gate here for a sound "provably ≥ N bits" policy..kaslr.likely.entropy_bits— the speculative residual (the narrower best-guess window). Gate here to also fail when a speculative technique could plausibly recover the base, accepting that this window is unproven..kaslr.disabled/.kaslr.unsupported— booleans: KASLR opted out, or not applicable to the arch/config.
To ask which placement the kernel actually chose — for a distribution across
boots, say — divide .kaslr.virtual.slide_bytes by the quantity's alignment.
That counts from the un-randomized base, so it describes the target. A position
counted from .kaslr.inferred.range_min instead describes how far KASLD
narrowed the window and from which direction: for a leak that bounds from
above, the base sits at that window's last slot by construction.
.hardening.patched_vulnerabilities.possibly_unpatched— CVE-class components that succeeded. A behavioural signal, not a version check: KASLD never trustsuname, so this is "a CVE-class leak worked here", never "the kernel is version X". Empty means none did.
Each gate below exits non-zero on a policy breach:
# Fail if the guaranteed base entropy drops below 12 bits.
bits=$(kasld -j | jq '.kaslr.inferred.entropy_bits')
[ "$bits" -ge 12 ] || { echo "KASLR regressed: $bits guaranteed bits"; exit 1; }
# Fail if KASLR is off (disabled, or unsupported for this arch/config).
if kasld -j | jq -e '.kaslr.disabled or .kaslr.unsupported' >/dev/null; then
echo "KASLR not active"; exit 1
fi
# Fail if any CVE-class leak succeeded.
n=$(kasld -j | jq '.hardening.patched_vulnerabilities.possibly_unpatched | length')
[ "$n" -eq 0 ] || { echo "$n CVE-class leak(s) succeeded"; exit 1; }Run KASLD unprivileged in the target environment for the posture a real
unprivileged attacker sees; running it as root additionally exposes root-gated
sources (e.g. /proc/iomem), which measure a different threat model.
To gate on drift over time rather than an absolute threshold, save a -j
snapshot as a baseline and compare a later run against it with
extra/posture-diff — a small jq-based helper that
exits non-zero if the KASLR posture regressed:
kasld -j > baseline.json # once, when the host is known-good
# … later, or in CI …
kasld -j > current.json
extra/posture-diff baseline.json current.json || echo "posture regressed"It compares only the boot-stable, security-relevant posture — guaranteed
residual entropy (virtual and physical), the KASLR posture state, unpatched
CVE-class leaks, and which defenses are off. The per-boot volatile values
(the resolved base address, slide, direct-map base) are never compared, so a
healthy reboot — which re-randomizes all of them — is not flagged; only a
genuine posture regression is. Exit 0 = no regression, 1 = regression
(findings printed), 2 = error. Snapshots can be live -j or replayed from an
extra/collect bundle
(KASLD_SYSROOT=<bundle>/sysroot kasld -j), so a baseline captured on one host
can be checked from another.
To scan a whole estate at once, extra/posture-summary
rolls up many -j snapshots into one table — one row per host — instead of
reading N separate reports:
# one snapshot file per host (the filename is the host label)
for h in $(cat hosts); do ssh "$h" 'kasld -j' > "snap/$h.json"; done
extra/posture-summary snap/*.jsonhost arch kernel kaslr vbits pbits leaks cves top-fix
cache03 x86_64 6.12.81 active 9b 31b 2/71 0 Set kernel.perf_event_paranoid = 2
db02 aarch64 6.12.90 active 16b 16b 0/68 0 -
web01 x86_64 6.15.6 active 9b 31b 1/70 1 Enable kernel lockdown (confidentiality mode)
Each row carries only the boot-stable posture — KASLR state, guaranteed
residual entropy (virtual/physical), leaks succeeded/total, unpatched CVE-class
count, and the most load-bearing hardening action. The host label is the
snapshot's filename (-j carries no hostname), so each file must be named after
its host at collection time; this tool does no collection or transport itself.
Output is an aligned text table by default, or --markdown (issue trackers),
--csv (spreadsheets), or --json (further tooling). A file that is not a
valid kasld -j snapshot is skipped with a warning rather than aborting the
report.