Skip to content

Commit 3eeee15

Browse files
committed
components: stop claiming the /proc/pid/syscall leak is kernel text
collect_syscall() stores six `unsigned long` into a __u64 args[6], so on 32-bit args[3..5] are never written and hold whatever the reading task's call chain left on proc_pid_syscall()'s own stack frame. What lands there differs by architecture: a return address into text on x86_32, arm and riscv32; a direct-map pointer on powerpc and mips, where the image is randomized above them so the leaked word sits below _text. The component tagged it REGION_KERNEL_TEXT at CONF_PARSED regardless. An interior-text sample implies image_base <= sample, so a direct-map word truncated the guaranteed window below the true base -- reproduced on ppc32 in 2 of 5 boots of a 5.9 kernel. Emitting at CONF_HEURISTIC puts it under KASLD_SOUND_FLOOR, where resolve_evidence() drops it from the floored run, so it cannot bound the guaranteed window on any architecture. It still shapes the likely window; i686 keeps recovering the base exactly there. Also corrects the header: the bug is not unzeroed upper bits, and 64-bit runs the same code with sizeof(long) == 8, filling the array exactly.
1 parent d84cc92 commit 3eeee15

1 file changed

Lines changed: 36 additions & 20 deletions

File tree

src/components/proc_pid_syscall.c

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,22 @@
1212
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4f134b89a24b965991e7c345b9a4591821f7c2a6
1313
//
1414
// Leak primitive:
15-
// Data leaked: kernel stack data (uninitialized upper bytes of syscall
16-
// args) Kernel subsystem: fs/proc — /proc/<PID>/syscall (collect_syscall)
17-
// Data structure: struct syscall_info → data.args[] (upper 32 bits on
18-
// 32-bit) Address type: virtual (kernel stack) Method: parsed
15+
// Data leaked: kernel stack data (args[3..5], never written on 32-bit)
16+
// Kernel subsystem: fs/proc — /proc/<PID>/syscall (collect_syscall)
17+
// Data structure: struct syscall_info → data.args[]
18+
// Address type: virtual (kernel stack)
19+
// Method: parsed
1920
// CVE: CVE-2020-28588
2021
// Patched: v5.10 (commit 4f134b89a24b)
2122
// Status: fixed in v5.10
2223
// Access check: none pre-v5.10 (world-readable /proc/<PID>/syscall)
2324
// Source: https://elixir.bootlin.com/linux/v5.9/source/fs/proc/base.c
2425
//
2526
// Mitigations:
26-
// Patched in v5.10. No runtime sysctl could restrict access — the bug was
27-
// in collect_syscall() failing to zero upper bytes of 64-bit arg fields on
28-
// 32-bit systems. Only affects 32-bit kernels (ARM, x86_32, etc.).
27+
// Patched in v5.10. No runtime sysctl could restrict access — the bug is
28+
// that collect_syscall() stores six `unsigned long` into a __u64 args[6],
29+
// covering 24 of 48 bytes on 32-bit. Only affects 32-bit kernels (ARM,
30+
// x86_32, etc.).
2931
//
3032
// Requires:
3133
// - CONFIG_HAVE_ARCH_TRACEHOOK=y
@@ -36,14 +38,14 @@
3638
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4f134b89a24b965991e7c345b9a4591821f7c2a6
3739
// https://cateee.net/lkddb/web-lkddb/HAVE_ARCH_TRACEHOOK.html
3840
//
39-
// 32-bit-kernel only — gated at compile time. The bug is in 32-bit kernels'
40-
// collect_syscall() (the high 32 bits of 64-bit syscall_info args are not
41-
// zeroed). 64-bit kernels follow a different code path and are not
42-
// vulnerable. On 64-bit kernels the argument-register values exposed by
43-
// /proc/<PID>/syscall are ordinary syscall numbers and pointers, none of
44-
// which carry the kernel-stack residue the exploit reads on 32-bit, and
45-
// misinterpreting a small integer (e.g. a syscall number) as a kernel
46-
// address would produce a nonsense observation.
41+
// 32-bit-kernel only — gated at compile time. collect_syscall() fills
42+
// data.args[] by storing six `unsigned long`, but the array is __u64 args[6]:
43+
// on 32-bit that writes 24 of 48 bytes, so args[3..5] are never written at
44+
// all, while args[0..2] each carry two real arguments packed into one field.
45+
// 64-bit kernels run the SAME code with sizeof(long) == 8, where six stores
46+
// fill the array exactly and no residue remains — the values exposed there
47+
// are ordinary syscall numbers and pointers, and misinterpreting one as a
48+
// kernel address would produce a nonsense observation.
4749
// ---
4850
// <bcoles@gmail.com>
4951

@@ -64,10 +66,10 @@
6466

6567
KASLD_EXPLAIN(
6668
"Reads /proc/<PID>/syscall on a 32-bit kernel. The file reports six "
67-
"64-bit argument registers, but on 32-bit only the lower 32 bits are "
68-
"used. Before the v5.10 fix (CVE-2020-28588), the upper 32 bits were "
69-
"not zeroed, leaking stale kernel stack data that often contains "
70-
"kernel text or stack pointers.");
69+
"64-bit argument fields, but the kernel fills them with six 32-bit "
70+
"words. Before the v5.10 fix (CVE-2020-28588), the last three fields "
71+
"were left holding stale kernel stack data — a text pointer on x86_32, "
72+
"arm and riscv32, a direct-map pointer on powerpc and mips.");
7173

7274
KASLD_META("method:parsed\n"
7375
"phase:inference\n"
@@ -214,8 +216,22 @@ int main(void) {
214216

215217
kasld_info("lowest leaked address: %lx", addr);
216218
kasld_info("possible kernel base: %lx", kasld_floor_text_base(addr));
219+
/* The leaked word is a kernel address; nothing available here establishes
220+
* that it is TEXT. It is whatever the reading task's call chain left on
221+
* proc_pid_syscall()'s own stack frame, which differs by architecture: a
222+
* return address into text on x86_32, arm and riscv32; a direct-map pointer
223+
* (kernel stacks, lowmem objects) on powerpc and mips, where the image is
224+
* randomized above them and a direct-map word therefore lands BELOW _text.
225+
*
226+
* An interior-text sample implies image_base <= sample, so tagging a
227+
* direct-map word as text truncates the image-base window below the true
228+
* base. CONF_HEURISTIC keeps the observation under the sound floor, where
229+
* the engine's confidence gate drops it from the floored run entirely: it
230+
* shapes the likely window, and cannot bound the guaranteed one. Confirming
231+
* text membership needs a text band this component cannot see — it runs
232+
* before inference, and that band is the unknown being solved for. */
217233
kasld_result_sample(KASLD_TYPE_VIRT, REGION_KERNEL_TEXT, addr, NULL,
218-
CONF_PARSED);
234+
CONF_HEURISTIC);
219235

220236
return 0;
221237
}

0 commit comments

Comments
 (0)