core/host: add meltdown_mitigation passthrough + migrate entrybleed
The kpti_enabled bool in struct skeletonkey_host flattens three
distinct sysfs states into one bit:
/sys/devices/system/cpu/vulnerabilities/meltdown content:
- 'Not affected' → CPU is Meltdown-immune; KPTI off; EntryBleed
doesn't apply (verdict: OK)
- 'Mitigation: PTI' → KPTI on (verdict: VULNERABLE)
- 'Vulnerable' → KPTI off but CPU not hardened (rare;
verdict: VULNERABLE conservatively)
- file unreadable → unknown (verdict: VULNERABLE conservatively)
kpti_enabled=true only captures 'Mitigation: PTI'; kpti_enabled=false
collapses 'Not affected', 'Vulnerable', and 'unreadable' into one
indistinguishable case. That meant entrybleed_detect() had to
re-open the sysfs file to recover the raw string.
Fix by also stashing the raw first line in
ctx->host->meltdown_mitigation[64]. kpti_enabled stays for callers
that only need the simple bool; new code that needs the nuance reads
the string. populate happens once at startup, like every other host
field.
entrybleed migration:
- reads ctx->host->meltdown_mitigation instead of opening sysfs
- removes the file-local read_first_line() helper (now dead code)
- same three-way verdict logic, but driven by a const char *
instead of a fresh fopen() each detect()
Test coverage:
- 3 new test rows on x86_64 fingerprints:
empty mitigation → VULNERABLE (conservative)
'Not affected' → OK
'Mitigation: PTI' → VULNERABLE
- 1 stub-path test row on non-x86_64 fingerprints (PRECOND_FAIL)
- registry coverage report: 30/31 modules now have direct tests
(up from 29/31; copy_fail is the only remaining untested module)
Verification:
- macOS: 33 kernel_range + 1 entrybleed-stub = 34 passes, 0 fails
- Linux (docker gcc:latest): 33 kernel_range + 54 detect = 87
passes, 0 fails. Up from 83 last commit.
This commit is contained in:
+11
-1
@@ -190,6 +190,7 @@ static void populate_caps(struct skeletonkey_host *h)
|
|||||||
h->apparmor_restrict_userns = false;
|
h->apparmor_restrict_userns = false;
|
||||||
h->unprivileged_bpf_disabled = false;
|
h->unprivileged_bpf_disabled = false;
|
||||||
h->kpti_enabled = false;
|
h->kpti_enabled = false;
|
||||||
|
h->meltdown_mitigation[0] = '\0';
|
||||||
h->kernel_lockdown_active = false;
|
h->kernel_lockdown_active = false;
|
||||||
h->selinux_enforcing = false;
|
h->selinux_enforcing = false;
|
||||||
h->yama_ptrace_restricted = false;
|
h->yama_ptrace_restricted = false;
|
||||||
@@ -208,8 +209,17 @@ static void populate_caps(struct skeletonkey_host *h)
|
|||||||
h->yama_ptrace_restricted = (v > 0);
|
h->yama_ptrace_restricted = (v > 0);
|
||||||
|
|
||||||
char buf[256];
|
char buf[256];
|
||||||
if (read_first_line("/sys/devices/system/cpu/vulnerabilities/meltdown", buf, sizeof buf))
|
if (read_first_line("/sys/devices/system/cpu/vulnerabilities/meltdown", buf, sizeof buf)) {
|
||||||
h->kpti_enabled = (strstr(buf, "Mitigation: PTI") != NULL);
|
h->kpti_enabled = (strstr(buf, "Mitigation: PTI") != NULL);
|
||||||
|
/* Stash the raw value so modules that need richer matching
|
||||||
|
* (e.g. entrybleed distinguishing "Not affected" CPUs from
|
||||||
|
* "Vulnerable" / "Mitigation: PTI") don't re-read sysfs. */
|
||||||
|
size_t L = strlen(buf);
|
||||||
|
if (L >= sizeof h->meltdown_mitigation)
|
||||||
|
L = sizeof h->meltdown_mitigation - 1;
|
||||||
|
memcpy(h->meltdown_mitigation, buf, L);
|
||||||
|
h->meltdown_mitigation[L] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
/* /sys/kernel/security/lockdown format: "[none] integrity confidentiality"
|
/* /sys/kernel/security/lockdown format: "[none] integrity confidentiality"
|
||||||
* — whichever level is bracketed is the active one. */
|
* — whichever level is bracketed is the active one. */
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ struct skeletonkey_host {
|
|||||||
bool apparmor_restrict_userns; /* sysctl: 1 = AA blocks unpriv userns */
|
bool apparmor_restrict_userns; /* sysctl: 1 = AA blocks unpriv userns */
|
||||||
bool unprivileged_bpf_disabled; /* /proc/sys/kernel/unprivileged_bpf_disabled = 1 */
|
bool unprivileged_bpf_disabled; /* /proc/sys/kernel/unprivileged_bpf_disabled = 1 */
|
||||||
bool kpti_enabled; /* /sys/.../meltdown contains "Mitigation: PTI" */
|
bool kpti_enabled; /* /sys/.../meltdown contains "Mitigation: PTI" */
|
||||||
|
char meltdown_mitigation[64]; /* raw first line of
|
||||||
|
* /sys/devices/system/cpu/vulnerabilities/meltdown
|
||||||
|
* — empty string if unreadable. Modules that need
|
||||||
|
* to distinguish "Not affected" (CPU immune) from
|
||||||
|
* "Mitigation: PTI" / "Vulnerable" can read this. */
|
||||||
bool kernel_lockdown_active; /* /sys/kernel/security/lockdown != [none] */
|
bool kernel_lockdown_active; /* /sys/kernel/security/lockdown != [none] */
|
||||||
bool selinux_enforcing; /* /sys/fs/selinux/enforce = 1 */
|
bool selinux_enforcing; /* /sys/fs/selinux/enforce = 1 */
|
||||||
bool yama_ptrace_restricted; /* /proc/sys/kernel/yama/ptrace_scope > 0 */
|
bool yama_ptrace_restricted; /* /proc/sys/kernel/yama/ptrace_scope > 0 */
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
|
|
||||||
#include "skeletonkey_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
@@ -108,40 +109,33 @@ unsigned long entrybleed_leak_kbase_lib(unsigned long entry_syscall_slot_offset)
|
|||||||
return (unsigned long)best_base;
|
return (unsigned long)best_base;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int read_first_line(const char *path, char *out, size_t n)
|
/* (read_first_line() removed — meltdown status now comes from
|
||||||
{
|
* ctx->host->meltdown_mitigation, populated once at startup in
|
||||||
FILE *f = fopen(path, "r");
|
* core/host.c. One file open across the corpus instead of per-detect.) */
|
||||||
if (!f) return -1;
|
|
||||||
if (!fgets(out, n, f)) { fclose(f); return -1; }
|
|
||||||
fclose(f);
|
|
||||||
/* trim trailing newline */
|
|
||||||
size_t L = strlen(out);
|
|
||||||
while (L && (out[L-1] == '\n' || out[L-1] == '\r')) out[--L] = 0;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static skeletonkey_result_t entrybleed_detect(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t entrybleed_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
/* Probe KPTI status. /sys/devices/system/cpu/vulnerabilities/meltdown
|
/* KPTI status comes from the shared host fingerprint
|
||||||
* is the most direct signal: "Mitigation: PTI" means KPTI is on
|
* (ctx->host->meltdown_mitigation) — populated once at startup by
|
||||||
* (= EntryBleed-applicable). "Not affected" means a hardened CPU
|
* reading /sys/devices/system/cpu/vulnerabilities/meltdown. The
|
||||||
* (very recent Intel + most AMD = no KPTI = no EntryBleed). */
|
* raw string is preserved (not just the kpti_enabled bool) so we
|
||||||
char buf[256];
|
* can distinguish "Not affected" (CPU immune; OK) from
|
||||||
int rc = read_first_line(
|
* "Mitigation: PTI" / "Vulnerable" (KPTI on; vulnerable to
|
||||||
"/sys/devices/system/cpu/vulnerabilities/meltdown", buf, sizeof buf);
|
* EntryBleed) without re-reading sysfs. */
|
||||||
if (rc < 0) {
|
const char *meltdown = ctx->host ? ctx->host->meltdown_mitigation : "";
|
||||||
|
if (meltdown[0] == '\0') {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[?] entrybleed: cannot read meltdown vuln status — "
|
fprintf(stderr, "[?] entrybleed: meltdown vuln status unknown — "
|
||||||
"assuming KPTI on (conservative)\n");
|
"assuming KPTI on (conservative)\n");
|
||||||
}
|
}
|
||||||
return SKELETONKEY_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] entrybleed: meltdown status = '%s'\n", buf);
|
fprintf(stderr, "[i] entrybleed: meltdown status = '%s'\n", meltdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* "Not affected" → CPU is Meltdown-immune → no KPTI → no EntryBleed */
|
/* "Not affected" → CPU is Meltdown-immune → no KPTI → no EntryBleed */
|
||||||
if (strstr(buf, "Not affected") != NULL) {
|
if (strstr(meltdown, "Not affected") != NULL) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] entrybleed: CPU is Meltdown-immune; KPTI off; "
|
fprintf(stderr, "[+] entrybleed: CPU is Meltdown-immune; KPTI off; "
|
||||||
"EntryBleed N/A\n");
|
"EntryBleed N/A\n");
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ extern const struct skeletonkey_module dirtydecrypt_module;
|
|||||||
extern const struct skeletonkey_module fragnesia_module;
|
extern const struct skeletonkey_module fragnesia_module;
|
||||||
extern const struct skeletonkey_module pack2theroot_module;
|
extern const struct skeletonkey_module pack2theroot_module;
|
||||||
extern const struct skeletonkey_module overlayfs_module;
|
extern const struct skeletonkey_module overlayfs_module;
|
||||||
|
extern const struct skeletonkey_module entrybleed_module;
|
||||||
extern const struct skeletonkey_module dirty_pipe_module;
|
extern const struct skeletonkey_module dirty_pipe_module;
|
||||||
extern const struct skeletonkey_module dirty_cow_module;
|
extern const struct skeletonkey_module dirty_cow_module;
|
||||||
extern const struct skeletonkey_module ptrace_traceme_module;
|
extern const struct skeletonkey_module ptrace_traceme_module;
|
||||||
@@ -594,6 +595,41 @@ static void run_all(void)
|
|||||||
&nf_tables_module, &h_nf_tables_5_10_209,
|
&nf_tables_module, &h_nf_tables_5_10_209,
|
||||||
SKELETONKEY_OK);
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
/* ── entrybleed: meltdown_mitigation passthrough ────────────────
|
||||||
|
* entrybleed reads ctx->host->meltdown_mitigation (raw sysfs line)
|
||||||
|
* instead of re-opening /sys/.../meltdown. Test the three branches:
|
||||||
|
* - empty string ("probe failed") → conservative VULNERABLE
|
||||||
|
* - "Not affected" (Meltdown-immune CPU) → OK
|
||||||
|
* - "Mitigation: PTI" (KPTI on, vulnerable) → VULNERABLE
|
||||||
|
* The module is x86_64-only; on other arches the stub returns
|
||||||
|
* PRECOND_FAIL regardless of meltdown status. We test the x86_64
|
||||||
|
* branch via the synthetic host's `arch` field. */
|
||||||
|
#if defined(__x86_64__) || defined(__amd64__)
|
||||||
|
struct skeletonkey_host h_entry_no_data = h_kernel_6_12;
|
||||||
|
h_entry_no_data.meltdown_mitigation[0] = '\0';
|
||||||
|
run_one("entrybleed: meltdown probe unread → conservative VULNERABLE",
|
||||||
|
&entrybleed_module, &h_entry_no_data,
|
||||||
|
SKELETONKEY_VULNERABLE);
|
||||||
|
|
||||||
|
struct skeletonkey_host h_entry_immune = h_kernel_6_12;
|
||||||
|
strcpy(h_entry_immune.meltdown_mitigation, "Not affected");
|
||||||
|
run_one("entrybleed: meltdown=Not affected (immune CPU) → OK",
|
||||||
|
&entrybleed_module, &h_entry_immune,
|
||||||
|
SKELETONKEY_OK);
|
||||||
|
|
||||||
|
struct skeletonkey_host h_entry_kpti = h_kernel_6_12;
|
||||||
|
strcpy(h_entry_kpti.meltdown_mitigation, "Mitigation: PTI");
|
||||||
|
run_one("entrybleed: meltdown=Mitigation: PTI → VULNERABLE",
|
||||||
|
&entrybleed_module, &h_entry_kpti,
|
||||||
|
SKELETONKEY_VULNERABLE);
|
||||||
|
#else
|
||||||
|
/* On non-x86_64 dev / CI containers, the stubbed detect() returns
|
||||||
|
* PRECOND_FAIL regardless of meltdown_mitigation contents. */
|
||||||
|
run_one("entrybleed: non-x86_64 arch → PRECOND_FAIL (stub)",
|
||||||
|
&entrybleed_module, &h_kernel_6_12,
|
||||||
|
SKELETONKEY_PRECOND_FAIL);
|
||||||
|
#endif
|
||||||
|
|
||||||
/* ── coverage report ─────────────────────────────────────────
|
/* ── coverage report ─────────────────────────────────────────
|
||||||
* Iterate the runtime registry (populated by skeletonkey_register_*
|
* Iterate the runtime registry (populated by skeletonkey_register_*
|
||||||
* calls in main()) and warn for any module that was not touched
|
* calls in main()) and warn for any module that was not touched
|
||||||
|
|||||||
Reference in New Issue
Block a user