Skip to content

Commit 739dbd1

Browse files
committed
components: read /proc/pid/syscall without a shell, and pin the floor
popen() runs /bin/sh -c, so the preferred `cat` path needs both a shell and cat. A target with neither still leaks, but the component reported "no kernel address found" there -- what it also reports on a kernel that was never vulnerable, which is why every VM run of it read as an undifferentiated no_result. Fall back to an in-process read through kasld_fopen when popen fails. `cat` stays preferred: measured on 32-bit arm it returned a word 0x2c4 above _text where the in-process read's lowest sat 1.9 MiB higher. check-unattributed-leak-floor pins the confidence a component may claim when it cannot establish which region its leaked value belongs to: below KASLD_SOUND_FLOOR, so a region guess can never bound the guaranteed window. Per file rather than per call -- a listed component has no region-establishing evidence at all -- which also keeps the matcher line-oriented, since a confidence argument routinely wraps onto its own line.
1 parent 3eeee15 commit 739dbd1

4 files changed

Lines changed: 158 additions & 19 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,7 @@ lint :
663663
@$(TEST_DIR)/check-text-floor
664664
@$(TEST_DIR)/check-text-region
665665
@$(TEST_DIR)/check-confidence-floor
666+
@$(TEST_DIR)/check-unattributed-leak-floor
666667
@$(TEST_DIR)/check-arch-macros
667668
@$(TEST_DIR)/check-lattice-seam
668669
@$(TEST_DIR)/check-page-offset-substitution

docs/testing.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ and `make` halts on the first.
113113
| `check-text-floor` | no component rolls its own text-base floor — they must use the `api.h` helper |
114114
| `check-shellcheck` | shellcheck over the `extra/` helper scripts |
115115
| `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 |
116+
| `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 |
116117
| `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 |
117118
| `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 |
118119
| `check-page-offset-substitution` | no engine rule or leak component substitutes the compile-time `PAGE_OFFSET` for the target's linear-map base. That constant describes the analysing build, not the kernel under examination, and on the VMSPLIT arches the two differ routinely — code that reaches for it is asserting the split it was compiled with. The failure is invisible: it compiles everywhere, passes on the whole default-split corpus, and is off by exactly the gap between two build configurations, which is zero on every machine anyone tests. In a rule, an equality must read the resolved `Q_PAGE_OFFSET` via `quantity_pinned()`, and a bound may instead use `PAGE_OFFSET_MAX` (upper) or `PAGE_OFFSET_MIN` (lower), which hold against every target and need no resolution. A component runs before inference and can never see an estimate, so it measures the boundary instead — `kasld_kernel_pointer_floor()` for the user/kernel split, `kasld_page_offset_floor()` for a region-tagged bound. Comments and string literals are stripped first, and `#if` / `#elif` lines are exempt by construction (a constant expression cannot call an accessor, which is why the band assertions keep `PAGE_OFFSET` a plain scalar), so only C code counts |

src/components/proc_pid_syscall.c

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,52 @@ KASLD_META("method:parsed\n"
7979
"patch:v5.10\n"
8080
"config:CONFIG_HAVE_ARCH_TRACEHOOK\n");
8181

82-
static unsigned long get_kernel_addr_proc_pid_syscall(void) {
82+
/* One /proc/self/syscall line into `buf`. Returns 1 on success.
83+
*
84+
* Preferred: `cat` is the leaker, not this process. The leaked words are the
85+
* READING task's kernel stack at collect_syscall() time; a freshly exec'd cat
86+
* reads at a shallower depth and yields addresses closer to _stext than an
87+
* in-process read, whose stack carries libc startup and the KASLD emitter call
88+
* chain. Measured: on a 32-bit arm cell cat returned a word 0x2c4 above _text
89+
* where the in-process read's lowest text word sat 1.9 MiB higher.
90+
*
91+
* Fallback: read the file directly. popen() runs /bin/sh -c, so the preferred
92+
* path needs both a shell and cat; a target with neither still leaks. Without
93+
* this the component reports "no kernel address found" there — the same thing
94+
* it reports on a kernel that is not vulnerable at all. The fallback recovers
95+
* less, not nothing, and it goes through kasld_fopen so the read is replayable.
96+
*/
97+
static int read_syscall_line(char *buf, size_t sz, int *use_popen) {
8398
FILE *f;
99+
char *got = NULL;
100+
101+
if (*use_popen) {
102+
f = popen("/bin/cat /proc/self/syscall", "r");
103+
if (f) {
104+
got = fgets(buf, (int)sz, f);
105+
pclose(f);
106+
if (got)
107+
return 1;
108+
}
109+
/* No shell, no cat, or nothing on stdout — stop paying for it every pass.
110+
*/
111+
*use_popen = 0;
112+
kasld_info("no /bin/cat via popen; reading /proc/self/syscall in-process");
113+
}
114+
115+
f = kasld_fopen("/proc/self/syscall", "r");
116+
if (!f)
117+
return 0;
118+
got = fgets(buf, (int)sz, f);
119+
fclose(f);
120+
return got ? 1 : 0;
121+
}
122+
123+
static unsigned long get_kernel_addr_proc_pid_syscall(void) {
84124
int iterations = 10;
125+
int use_popen = 1;
85126
unsigned long addr = 0;
86127
unsigned long leaked_addr = 0;
87-
const char *cmd = "/bin/cat /proc/self/syscall";
88128
char buff[1024];
89129
char *ptr;
90130
char *endptr;
@@ -93,26 +133,11 @@ static unsigned long get_kernel_addr_proc_pid_syscall(void) {
93133

94134
int i;
95135
for (i = 0; i < iterations; i++) {
96-
/* cat is the leaker, not this process. The CVE leaks stale upper
97-
* bytes of 64-bit arg fields from the reading process's kernel
98-
* stack at collect_syscall() time; cat's stack at read(2) is
99-
* shallower and empirically yields lower (closer to _stext)
100-
* addresses than an in-process fopen, whose stack carries libc
101-
* startup + the KASLD emitter call chain. */
102-
f = popen(cmd, "r");
103-
if (f == NULL) {
104-
perror("[-] popen");
105-
return 0;
106-
}
107-
108-
if (fgets(buff, sizeof(buff), f) == NULL) {
109-
perror("[-] fgets");
110-
pclose(f);
136+
if (!read_syscall_line(buff, sizeof(buff), &use_popen)) {
137+
kasld_err("could not read /proc/self/syscall");
111138
return 0;
112139
}
113140

114-
pclose(f);
115-
116141
/* Lazy implementation. In practice we only want data after the first 24
117142
* bytes (from the fifth value onwards).
118143
*
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/bin/sh
2+
# This file is part of KASLD - https://github.com/bcoles/kasld
3+
#
4+
# check-unattributed-leak-floor — a component that cannot establish WHICH region
5+
# its leaked value belongs to must stay below the sound floor.
6+
#
7+
# Some leaks hand over a kernel address with no evidence of what it points at.
8+
# The value is real; the region is a guess. Emitting such a value at or above
9+
# KASLD_SOUND_FLOOR (CONF_INFERRED) lets it shape the GUARANTEED window, and a
10+
# region guess in the guaranteed window is a soundness bug rather than an
11+
# imprecision: an interior-text sample implies image_base <= sample, so a value
12+
# that is not text — but sits below the real _text — carves the truth out of the
13+
# window that promises to contain it.
14+
#
15+
# This is not hypothetical. /proc/<pid>/syscall (CVE-2020-28588) leaks whatever
16+
# the reading task's call chain left on proc_pid_syscall()'s kernel stack frame.
17+
# What lands there is per-architecture: a return address into text on x86_32, arm
18+
# and riscv32; a direct-map pointer on powerpc and mips, where the image is
19+
# randomized above them so the leaked word sits BELOW _text. Tagged
20+
# REGION_KERNEL_TEXT at CONF_PARSED, that truncated the guaranteed window below
21+
# the true base on 2 of 5 boots of a 5.9 ppc32 kernel. Nothing in the component
22+
# could tell the two cases apart — the text band is the unknown being solved for.
23+
#
24+
# The check is the whole-file rule rather than a per-call one: a component listed
25+
# here has no source of region-establishing evidence at all, so no emission it
26+
# makes can earn the sound band. That also keeps the check line-oriented, because
27+
# an emission's confidence argument routinely wraps onto its own line and a
28+
# multi-line matcher is exactly the kind of fragile guard that silently stops
29+
# checking.
30+
#
31+
# Registering a component here is a statement that its region claim rests on the
32+
# address's value alone. A component that gains real corroboration should be
33+
# removed from the list, not have its confidence raised in place.
34+
#
35+
# Comments are stripped before matching (tests/lib/c-code-grep.sh), so these
36+
# files may discuss CONF_PARSED and the sound floor freely — as they must, since
37+
# the reason for the floor is the thing worth writing down.
38+
#
39+
# Pure text, so it needs no build.
40+
# ---
41+
# <bcoles@gmail.com>
42+
43+
set -u
44+
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
45+
COMPONENTS=$ROOT/src/components
46+
# shellcheck source=tests/lib/guard-scope.sh
47+
. "$ROOT/tests/lib/guard-scope.sh"
48+
# shellcheck source=tests/lib/c-code-grep.sh
49+
. "$ROOT/tests/lib/c-code-grep.sh"
50+
51+
if [ -t 1 ]; then
52+
RED=$(printf '\033[31m')
53+
GREEN=$(printf '\033[32m')
54+
RESET=$(printf '\033[0m')
55+
else
56+
RED=
57+
GREEN=
58+
RESET=
59+
fi
60+
61+
# Components whose leaked value carries no evidence of its own region.
62+
# proc_pid_syscall — uninitialised kernel stack via /proc/<pid>/syscall; the
63+
# word is a text pointer on some arches and a direct-map
64+
# pointer on others, indistinguishable by value.
65+
UNATTRIBUTED="proc_pid_syscall.c"
66+
67+
AT_OR_ABOVE_FLOOR='(^|[^A-Za-z0-9_])CONF_(PARSED|DERIVED|INFERRED)([^A-Za-z0-9_]|$)'
68+
BELOW_FLOOR='(^|[^A-Za-z0-9_])CONF_(HEURISTIC|TIMING|BRUTE)([^A-Za-z0-9_]|$)'
69+
70+
bad=0
71+
n=0
72+
for base in $UNATTRIBUTED; do
73+
f=$COMPONENTS/$base
74+
if [ ! -f "$f" ]; then
75+
echo "${RED}$base: listed here but not present in src/components${RESET}"
76+
bad=1
77+
continue
78+
fi
79+
n=$((n + 1))
80+
81+
hits=$(c_code_grep "$f" "$AT_OR_ABOVE_FLOOR")
82+
if [ -n "$hits" ]; then
83+
bad=1
84+
echo "${RED}$base: emits at or above the sound floor${RESET}"
85+
echo "$hits" | while IFS= read -r h; do
86+
echo " $h"
87+
done
88+
fi
89+
90+
# A file that emits nothing at all would pass the rule above vacuously, and
91+
# would keep passing if its emission were deleted — so require the positive
92+
# case too.
93+
if [ -z "$(c_code_grep "$f" "$BELOW_FLOOR")" ]; then
94+
bad=1
95+
echo "${RED}$base: no sub-floor emission found — is it still emitting?${RESET}"
96+
fi
97+
done
98+
99+
if [ "$bad" -ne 0 ]; then
100+
echo
101+
echo "A component listed in this guard cannot show which region its leaked"
102+
echo "value belongs to, so the value must not reach the guaranteed window."
103+
echo "Emit at CONF_HEURISTIC or below. If the component has gained evidence"
104+
echo "that establishes the region, drop it from UNATTRIBUTED here rather than"
105+
echo "raising the confidence in place."
106+
exit 1
107+
fi
108+
109+
guard_scope "check-unattributed-leak-floor" "$n" 1
110+
111+
printf '%scheck-unattributed-leak-floor: OK%s (%d component%s below the sound floor)\n' \
112+
"$GREEN" "$RESET" "$n" "$([ "$n" -eq 1 ] || echo s)"

0 commit comments

Comments
 (0)