Skip to content

Commit 0679257

Browse files
committed
docs: document the whole environment surface, and guard it
Six of ten environment variables had a kasld(1) entry. Two of the undocumented ones matter more than the rest: KASLD_COMPONENT_DIR selects the directory every component is run from and KASLD_EXEC_WRAPPER selects a program run in their place, so an undocumented one is an execution knob invisible to anyone reviewing a sudoers rule or a packaging script. The section now lists all ten and opens by saying so, with sudo's env_reset and the env_keep/SETENV exception named. The surface is what is read OUTSIDE src/components/. A component is a standalone program and its debugging knobs are its own; documenting them would oblige one page to track 113 components' internals. A variable read in a component and elsewhere -- KASLD_EXPERIMENTAL, KASLD_SYSROOT -- stays documented, being part of the orchestrator's interface too. check-env-docs holds the page and the code in step: every such getenv() needs an entry, every entry needs a reader. Documentation fixes this once; the parity check is what holds it as the surface grows, the same shape check-manpages applies to flags. check-results said it "runs kasld itself". It does not -- three input paths, stdin, a file, or a usage error, and no invocation anywhere. Both documented forms run this script as root over output kasld produced as the ordinary user. The line is replaced with what is true, and why it matters: nothing here elevates the analysis, so KASLD_* is never read at root.
1 parent 232a8f5 commit 0679257

5 files changed

Lines changed: 154 additions & 3 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,7 @@ lint :
687687
@$(TEST_DIR)/check-posture-diff
688688
@$(TEST_DIR)/check-posture-summary
689689
@$(TEST_DIR)/check-validators
690+
@$(TEST_DIR)/check-env-docs
690691
@$(TEST_DIR)/check-shellcheck
691692
@$(TEST_DIR)/check-baseline
692693
@$(TEST_DIR)/check-render-parity

docs/testing.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ and `make` halts on the first.
142142
| `check-shellcheck` | shellcheck over the `extra/` helper scripts |
143143
| `check-confidence-floor` | no engine rule pins the *guaranteed* window from a guess — a sub-floor signal may shape `likely` only, outside the reviewed allowlist |
144144
| `check-unattributed-leak-floor` | a component that cannot establish *which* region its leaked value belongs to stays below the sound floor. Some leaks hand over a kernel address with no evidence of what it points at: the value is real, the region is a guess, and a region guess admitted to the guaranteed window is a soundness bug rather than an imprecision — an interior-text sample implies `image_base <= sample`, so a value that is not text but sits below the real `_text` carves the truth out of the window that promises to contain it. `/proc/<pid>/syscall` (CVE-2020-28588) is the registered case: it leaks whatever the reading task's call chain left on `proc_pid_syscall()`'s kernel stack frame, which is a return address into text on x86_32, arm and riscv32, and a direct-map pointer on powerpc and mips, where the image is randomized above them so the leaked word sits *below* `_text`. Nothing in the component can tell the two apart, because the text band is the unknown being solved for. The rule is per file rather than per call — a listed component has no region-establishing evidence at all, so no emission it makes can earn the sound band — which also keeps the matcher line-oriented, since a confidence argument routinely wraps onto its own line. A component that gains real corroboration is removed from the list, never raised in place |
145+
| `check-env-docs` | every environment variable read outside `src/components/` has a `kasld(1)` ENVIRONMENT entry, and every entry is actually read. Component-exclusive variables are excluded deliberately: a component is a standalone program whose debugging knobs belong to it, not to the orchestrator's interface, and documenting them would oblige one page to track 113 components' internals. Two of them — `KASLD_COMPONENT_DIR` and `KASLD_EXEC_WRAPPER` — name programs kasld will execute, so an undocumented one is an execution knob invisible to anyone reviewing a `sudoers` rule or a packaging script. The same parity check `check-manpages` applies to flags; documentation fixes the surface once, this keeps it fixed as the surface grows |
145146
| `check-validators` | no arithmetic-input validator accepts anything dangerous. `extra/check-results` and `extra/ksymoff` both feed parsed fields into shell arithmetic, where `$(( x ))` evaluates embedded command substitutions — and `check-results` is documented as running under `sudo`, so a value like `a[$(cmd)]` reaching it would be root command execution. The validator is duplicated four ways because neither script can source a library (`ksymoff` installs to `$PREFIX/bin`; `check-results` is copied to a target), so a correction to one does not reach the others. What is asserted is *rejection*, not sameness: the four accept different sets on purpose. Also asserts each one accepts a known-good value, so a validator that rejected everything could not pass vacuously, and that the `@arith-validator` marker count matches the number exercised, so a new one cannot escape the corpus |
146147
| `check-arch-macros` | every macro an architecture header defines is read by something. A name nothing reads is a misspelling, a retired spelling one header kept, or dead weight — and the first two are silent: the architecture falls back to the contract's default for the macro it *meant* to set, which costs precision with nothing to show for it. No test catches that, because the tests read the same declaration the code does and assert whatever it says. Complements the retired-spelling `#error`s in `api.h`, which fail the build for one known-old name; this catches the names no such check lists |
147148
| `check-lattice-seam` | the quantities held to the estimate accessors (`Q_PAGE_OFFSET`, `Q_VA_BITS`) are read through `quantity_pinned/window/admits/narrowed`, never through `.lo` / `.hi`. `struct estimate` means different things per lattice — on a finite set `lo` is a live-candidate bitmask and `hi` is unused — and which lattice a quantity uses is declared once in the quantity table, so a direct read hard-codes an answer the reader never asked for. Nothing would fail loudly: a bitmask read as an address is a small integer, so the result is a plausible wrong answer rather than a crash. The pointer alias is discovered from its binding rather than assumed to be named `po`, so renaming it cannot slip a read past |

extra/check-results

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33
#
44
# Validate KASLD results against ground truth available to root.
55
#
6-
# Reads tagged lines from a KASLD run (or runs kasld itself), then
7-
# compares each result against /proc/kallsyms, /proc/iomem, /proc/modules,
8-
# and /proc/kcore (direct-map base / page_offset_base).
6+
# Reads tagged lines from a file or standard input and compares each result
7+
# against /proc/kallsyms, /proc/iomem, /proc/modules, and /proc/kcore
8+
# (direct-map base / page_offset_base).
9+
#
10+
# It never runs kasld. Only this script needs root, for the unmasked ground
11+
# truth; kasld itself runs unprivileged and its output is piped or saved. That
12+
# separation is the point -- nothing here elevates the analysis, so the
13+
# KASLD_* environment (see kasld(1) ENVIRONMENT) is never read at root.
914
#
1015
# Wire format (see src/include/kasld/api.h):
1116
# <type> <region>[:<name>] pos=<pos> conf=<conf> \

man/kasld.1

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,26 @@ of
153153
values would be indistinguishable from the hardened host that yields no results
154154
(exit 1).
155155
.SH ENVIRONMENT
156+
Two of these name programs that
157+
.B kasld
158+
will execute:
159+
.B KASLD_COMPONENT_DIR
160+
selects the directory every component binary is run from, and
161+
.B KASLD_EXEC_WRAPPER
162+
selects a program run in their place. Preserving
163+
.B KASLD_*
164+
across a privilege boundary therefore hands over code execution at that
165+
privilege.
166+
.B sudo
167+
strips them under its default
168+
.BR env_reset ;
169+
a
170+
.B sudoers
171+
rule with
172+
.B env_keep
173+
or
174+
.BR SETENV ,
175+
or any wrapper that elevates while keeping the environment, does not.
156176
.TP
157177
.B KASLD_COMPONENT_DIR
158178
Directory to load component binaries from, overriding the default search
@@ -168,6 +188,30 @@ skipped.
168188
When set, enable experimental components, equivalent to
169189
.BR \-\-experimental .
170190
.TP
191+
.B KASLD_EXEC_WRAPPER
192+
Program to execute in place of each component, with the component's path as
193+
its first argument. Intended for running guest-architecture components under
194+
a user-mode emulator. Unset or empty gives the normal path, a direct
195+
.BR execve (2)
196+
of the component.
197+
.TP
198+
.B KASLD_VERBOSE
199+
When set to a value that is non-empty and does not begin with
200+
.BR 0 ,
201+
enable verbose output, equivalent to
202+
.BR \-\-verbose .
203+
.TP
204+
.B KASLD_UNAME_RELEASE
205+
Override the kernel release reported by
206+
.BR uname (2),
207+
so a captured tree replays under the release it was taken from rather than
208+
the running one. Used with
209+
.BR KASLD_SYSROOT .
210+
.TP
211+
.B KASLD_DEBUG_CONSTRAINTS
212+
When set, print each inference constraint and the resulting bounds to standard
213+
error. Diagnostic only; it does not affect the result.
214+
.TP
171215
.B NO_COLOR
172216
When present (any value, including empty), disable coloured output regardless of
173217
whether stdout is a TTY. An explicit

tests/check-env-docs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/bin/sh
2+
# This file is part of KASLD - https://github.com/bcoles/kasld
3+
#
4+
# check-env-docs — every environment variable the code reads is documented in
5+
# kasld(1) ENVIRONMENT, and every one documented there is actually read.
6+
#
7+
# The audit that prompted this found the environment surface had grown to twelve
8+
# variables while the design notes described four. Two of them -- KASLD_EXEC_WRAPPER
9+
# and KASLD_COMPONENT_DIR -- name programs kasld will execute, so an undocumented
10+
# one is not merely untidy: it is an execution knob nobody reviewing a sudoers
11+
# rule or a packaging script can see.
12+
#
13+
# The same parity check check-manpages applies to flags, applied to getenv().
14+
# Documentation alone fixes a surface once; this is what keeps it fixed as the
15+
# surface grows.
16+
#
17+
# A variable counts as documented when it appears in a .B / .BR request inside
18+
# the ENVIRONMENT section -- an entry, not a passing mention in prose, since the
19+
# section's own introduction names two of them.
20+
#
21+
# Pure text, so it needs no build.
22+
# ---
23+
# <bcoles@gmail.com>
24+
25+
set -u
26+
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
27+
MAN="$ROOT/man/kasld.1"
28+
29+
if [ -t 1 ]; then
30+
RED=$(printf '\033[31m')
31+
GREEN=$(printf '\033[32m')
32+
RESET=$(printf '\033[0m')
33+
else
34+
RED=
35+
GREEN=
36+
RESET=
37+
fi
38+
39+
fail=0
40+
note() {
41+
printf '%scheck-env-docs: FAIL%s — %s\n' "$RED" "$RESET" "$1" >&2
42+
fail=1
43+
}
44+
45+
[ -f "$MAN" ] || {
46+
note "man/kasld.1 is missing"
47+
exit 1
48+
}
49+
50+
code=$(mktemp) || exit 1
51+
documented=$(mktemp) || exit 1
52+
trap 'rm -f "$code" "$documented"' EXIT INT TERM
53+
54+
# The variables kasld(1) is answerable for: those read anywhere OUTSIDE
55+
# src/components/. A component is a standalone program with its own debugging
56+
# knobs -- KASLD_ZOMBIELOAD_DEBUG, MALI_MIN_COUNT -- and those belong to it, not
57+
# to the orchestrator's interface. Documenting them here would put 113
58+
# components' internals in one page and oblige it to track every one.
59+
#
60+
# A variable read in a component AND elsewhere (KASLD_EXPERIMENTAL,
61+
# KASLD_SYSROOT) is still part of the surface: it is picked up from the
62+
# non-component reader. Only component-EXCLUSIVE variables are private.
63+
#
64+
# src/ only in either case: tests and dev harnesses set these, they do not
65+
# define them.
66+
find "$ROOT/src" -name '*.c' -o -name '*.h' |
67+
grep -v '/components/' |
68+
xargs grep -hoE 'getenv\("[A-Z_][A-Z_0-9]*"\)' |
69+
sed 's/getenv("//; s/")//' | sort -u >"$code"
70+
71+
# Names given an ENTRY in the ENVIRONMENT section -- the tag line directly after
72+
# a .TP. Deliberately not every .B in the section: its introduction names two
73+
# variables in prose and entries cross-reference other sections, and neither is
74+
# a definition. One tag may carry several (`.BR CLICOLOR ", " CLICOLOR_FORCE`),
75+
# so take every uppercase token on that line.
76+
awk '/^\.SH ENVIRONMENT/ { inb = 1; next }
77+
/^\.SH / { inb = 0 }
78+
inb && tag { print; tag = 0 }
79+
inb && /^\.TP[[:space:]]*$/ { tag = 1 }' "$MAN" |
80+
grep -oE '[A-Z_][A-Z_0-9]{2,}' | sort -u >"$documented"
81+
82+
n=0
83+
while IFS= read -r v; do
84+
[ -n "$v" ] || continue
85+
n=$((n + 1))
86+
grep -qxF "$v" "$documented" ||
87+
note "$v is read by src/ but has no kasld(1) ENVIRONMENT entry"
88+
done <"$code"
89+
90+
while IFS= read -r v; do
91+
[ -n "$v" ] || continue
92+
grep -qxF "$v" "$code" ||
93+
note "$v has a kasld(1) ENVIRONMENT entry but nothing in src/ reads it"
94+
done <"$documented"
95+
96+
if [ "$fail" -ne 0 ]; then
97+
exit 1
98+
fi
99+
printf '%scheck-env-docs: OK%s (%s environment variables, code and kasld(1) agree)\n' \
100+
"$GREEN" "$RESET" "$n"

0 commit comments

Comments
 (0)