Skip to content

Commit d4706e7

Browse files
committed
tests: one owner for the staged filesystem tree
Fifteen test binaries each carried a private mkdtemp, a few mkdir calls and a write_file copy. Eleven removed nothing, so every passing suite run left a tree per binary under /tmp to accumulate indefinitely, with nothing failing. They now stage through test_sysroot.h, which names the root after the binary and registers its own removal. A test that returns from main() cleans up whether or not it remembers to; one that dies on an assertion does not, since abort() runs no atexit handler, so the tree survives for a reader. The header grew a length-taking write — a zero page and an ELF core header are not text, and fputs truncates them at the first NUL — and a stage_path for the two tests that need the file APIs directly. check-test-staging keeps it to one owner: no second mkdtemp caller, no test setting KASLD_SYSROOT itself, and a header that still cleans up. The test binaries also had no dependency on tests/*.h, $(HDRS) being the product's headers, so editing test_sysroot.h rebuilt nothing while the build reported success. TEST_ALL_BINS now depends on TEST_HDRS. Until that was fixed, measuring this change measured the build before it.
1 parent 57cdd6f commit d4706e7

20 files changed

Lines changed: 239 additions & 302 deletions

Makefile

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,47 @@ $(TEST_PARSERS_BIN): $(TEST_DIR)/test_sysfs_parsers.c $(TEST_PARSERS_SRCS) $(HDR
661661
$(Q)$(CC) $(TEST_ALL_CFLAGS) $(ALL_LDFLAGS) -I$(SRC_DIR) $(TEST_DIR)/test_sysfs_parsers.c -o $@
662662

663663
.PHONY: test
664-
test : $(KASLD_BIN) $(TEST_BIN) $(TEST_RENDER_BIN) $(TEST_EST_BIN) $(TEST_EV_BIN) $(TEST_ALIGN_BIN) $(TEST_ADDRP_BIN) $(TEST_TWIDTH_BIN) $(TEST_TS_BIN) $(TEST_PREFETCH_SCAN_BIN) $(TEST_CPU_BIN) $(TEST_OUTCOME_BIN) $(TEST_TEXT_ORDER_BIN) $(TEST_KIMG_BIN) $(TEST_ENG_BIN) $(TEST_INT_BIN) $(TEST_DMESG_BIN) $(TEST_BACKTRACE_BIN) $(TEST_BOOTCFG_BIN) $(TEST_KASLRDIS_BIN) $(TEST_DTMEM_BIN) $(TEST_SOCKPTR_BIN) $(TEST_TIMERLIST_BIN) $(TEST_KALLSYMS_BIN) $(TEST_AVAILFILTER_BIN) $(TEST_BTF_BIN) $(TEST_DMESG_RESV_BIN) $(TEST_BPE820_BIN) $(TEST_PARSERS_BIN) $(TEST_KCORE_BIN)
664+
# Test headers carry behaviour, not just declarations: test_harness.h runs the
665+
# suite, and test_sysroot.h makes the staged tree and registers its removal.
666+
# They are not in $(HDRS), which is the product's headers, so a test binary had
667+
# no dependency on them at all -- editing one left every binary stale while the
668+
# build reported success, and a measurement of the edit measured the build
669+
# before it.
670+
TEST_HDRS := $(wildcard $(TEST_DIR)/*.h)
671+
672+
TEST_ALL_BINS := $(TEST_BIN) \
673+
$(TEST_RENDER_BIN) \
674+
$(TEST_EST_BIN) \
675+
$(TEST_EV_BIN) \
676+
$(TEST_ALIGN_BIN) \
677+
$(TEST_ADDRP_BIN) \
678+
$(TEST_TWIDTH_BIN) \
679+
$(TEST_TS_BIN) \
680+
$(TEST_PREFETCH_SCAN_BIN) \
681+
$(TEST_CPU_BIN) \
682+
$(TEST_OUTCOME_BIN) \
683+
$(TEST_TEXT_ORDER_BIN) \
684+
$(TEST_KIMG_BIN) \
685+
$(TEST_ENG_BIN) \
686+
$(TEST_INT_BIN) \
687+
$(TEST_DMESG_BIN) \
688+
$(TEST_BACKTRACE_BIN) \
689+
$(TEST_BOOTCFG_BIN) \
690+
$(TEST_KASLRDIS_BIN) \
691+
$(TEST_DTMEM_BIN) \
692+
$(TEST_SOCKPTR_BIN) \
693+
$(TEST_TIMERLIST_BIN) \
694+
$(TEST_KALLSYMS_BIN) \
695+
$(TEST_AVAILFILTER_BIN) \
696+
$(TEST_BTF_BIN) \
697+
$(TEST_DMESG_RESV_BIN) \
698+
$(TEST_BPE820_BIN) \
699+
$(TEST_PARSERS_BIN) \
700+
$(TEST_KCORE_BIN)
701+
702+
$(TEST_ALL_BINS): $(TEST_HDRS)
703+
704+
test : $(KASLD_BIN) $(TEST_ALL_BINS)
665705
@$(TEST_DIR)/run-all
666706
@$(TEST_DIR)/check-render-width
667707
@$(MAKE) --no-print-directory lint
@@ -679,6 +719,7 @@ lint :
679719
@$(TEST_DIR)/check-discard-ledger
680720
@$(TEST_DIR)/check-discard-accounting
681721
@$(TEST_DIR)/check-vantage-coverage
722+
@$(TEST_DIR)/check-test-staging
682723
@$(TEST_DIR)/check-truncation
683724
@$(TEST_DIR)/check-addr-parse
684725
@$(TEST_DIR)/check-absence-vs-denial

docs/testing.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ and `make` halts on the first.
132132
| `check-extent-callers` | only reviewed whole-map components call `kasld_result_extent` (the covering-completeness contract; a partial map would carve a false gap) |
133133
| `check-discard-accounting` | the shipped binary, with its worker pool running, counts discards exactly — N components x M bad wire records must yield total N*M in N kinds, repeated. The unit tests are single-threaded and their build defines no `HAVE_PTHREAD`, so nothing else exercises the ledger's mutex; probabilistic, so a failure is conclusive and a pass is evidence |
134134
| `check-vantage-coverage` | every filesystem source `kasld_gather_vantage()` reads is staged by a test, the suite actually calls the gatherer, and the absent direction is asserted — the gatherer was once constrained by nothing at all, a `memset` stub leaving the suite green, because the tests named "vantage" asserted on the formatters over a hand-filled struct |
135+
| `check-test-staging` | every test binary stages its filesystem through `test_sysroot.h`, which names the root after the binary and registers its own removal — fifteen tests each carried a private `mkdtemp`, of which eleven removed nothing, so a passing suite left a tree per binary under `/tmp` to accumulate indefinitely, with nothing ever failing |
135136
| `check-discard-ledger` | every reason in the discard vocabulary has a wire name, no layer keeps a private drop-counter beside the ledger, and the renderers read it through its accessors — a run that discarded evidence resolved from a subset of what was available, so a consumer unable to see the discard reads a bounded answer as a complete one |
136137
| `check-covering-consumers` | every rule reading `ev->coverings[]` is reviewed and calls `covering_active()` first — the read end of the same contract; the floor gate demotes a below-floor map by clearing its valid bit, and a rule that never asks carries it into the guaranteed window regardless of what it emits |
137138
| `check-truncation` | no silent 64-bit→word narrowing when compiled for 32-bit (compiles a TU with `i686-linux-gnu-gcc`) |

tests/check-test-staging

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#!/bin/sh
2+
# This file is part of KASLD - https://github.com/bcoles/kasld
3+
#
4+
# check-test-staging — a test binary stages its filesystem through
5+
# test_sysroot.h, and that header removes what it made.
6+
#
7+
# A test that consults a fact source needs a tree to point KASLD_SYSROOT at, and
8+
# for a long time each one built its own: mkdtemp, a few mkdir calls, a private
9+
# write_file. Fifteen binaries carried a copy, and eleven of them removed
10+
# nothing, so every passing suite run left a tree per binary under /tmp to
11+
# accumulate indefinitely. Nothing failed. A leak that only ever costs disk is
12+
# invisible to a green suite, which is why it ran for so long and why it needs a
13+
# guard rather than a fix.
14+
#
15+
# The fix was one owner: test_sysroot.h makes the root, names it after the
16+
# binary, and registers its own removal. This keeps that the only owner.
17+
#
18+
# 1. mkdtemp() is called in test_sysroot.h and nowhere else. A second caller
19+
# is a second root with no owner -- the shape that leaked.
20+
# 2. KASLD_SYSROOT is set in test_sysroot.h and nowhere else. Pointing the I/O
21+
# layer at a directory the header did not make escapes its lifetime just as
22+
# completely, without naming mkdtemp.
23+
# 3. The header registers removal. Both checks above pass against a header
24+
# that never cleans up, which is what the tree looked like before.
25+
#
26+
# WHAT THIS DOES NOT PROVE: that no test leaks. A test can still create a file
27+
# by any other means; the guard names the three ways the tree actually leaked.
28+
# The measurement that settles it is a `make test` bracketed by a count of
29+
# /tmp/kasld* -- which is how the leak was found, and is not something grep can
30+
# do.
31+
#
32+
# Pure text, so it needs no build.
33+
# ---
34+
# <bcoles@gmail.com>
35+
36+
set -u
37+
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
38+
TESTS="$ROOT/tests"
39+
HDR="$TESTS/test_sysroot.h"
40+
41+
if [ -t 1 ]; then
42+
RED=$(printf '\033[31m')
43+
GREEN=$(printf '\033[32m')
44+
RESET=$(printf '\033[0m')
45+
else
46+
RED=
47+
GREEN=
48+
RESET=
49+
fi
50+
51+
fail=0
52+
note() {
53+
printf '%scheck-test-staging: FAIL%s — %s\n' "$RED" "$RESET" "$1" >&2
54+
fail=1
55+
}
56+
57+
[ -f "$HDR" ] || {
58+
note "test_sysroot.h is missing"
59+
exit 1
60+
}
61+
62+
# --- 1 & 2. one owner of the root -------------------------------------------
63+
# The CALL, not the word: two tests name mkdtemp in a feature-test-macro comment
64+
# because they include this header, and those are accurate.
65+
n=0
66+
for f in "$TESTS"/*.c "$TESTS"/*.h; do
67+
[ -f "$f" ] || continue
68+
[ "$f" = "$HDR" ] && continue
69+
n=$((n + 1))
70+
if grep -q 'mkdtemp(' "$f"; then
71+
note "$(basename "$f") calls mkdtemp(); stage through th_sysroot_init() so the tree is removed"
72+
fi
73+
if grep -q 'setenv("KASLD_SYSROOT"' "$f"; then
74+
note "$(basename "$f") sets KASLD_SYSROOT itself; th_sysroot_init() owns the root and its lifetime"
75+
fi
76+
done
77+
78+
# --- 3. the owner actually cleans up ----------------------------------------
79+
# Checks 1 and 2 are satisfied by a header that leaks exactly as the copies did,
80+
# so the removal is asserted rather than assumed.
81+
init=$(sed -n '/^static void th_sysroot_init/,/^}/p' "$HDR")
82+
printf '%s\n' "$init" | grep -q 'atexit(th_sysroot_fini)' ||
83+
note "th_sysroot_init() does not register th_sysroot_fini(); a passing run would leave its tree"
84+
85+
sed -n '/^static void th_sysroot_fini/,/^}/p' "$HDR" | grep -q 'rmdir(th_sysroot_root)' ||
86+
note "th_sysroot_fini() does not remove the root directory"
87+
88+
if [ "$fail" -ne 0 ]; then
89+
exit 1
90+
fi
91+
printf '%scheck-test-staging: OK%s (%s test files stage through test_sysroot.h, which removes its tree)\n' \
92+
"$GREEN" "$RESET" "$n"

tests/test_boot_config.c

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ int boot_config_main(void);
2020
#undef main
2121

2222
#include "test_harness.h"
23+
#include "test_sysroot.h"
2324

2425
#include <assert.h>
2526
#include <fcntl.h>
@@ -28,7 +29,6 @@ int boot_config_main(void);
2829
#include <sys/utsname.h>
2930
#include <unistd.h>
3031

31-
static char g_root[256];
3232
static char cap[8192];
3333

3434
/* KASLR compiled out so the disabled facts fire; CONFIG_PHYSICAL_START gives a
@@ -38,20 +38,10 @@ static const char *CFG =
3838
"CONFIG_RANDOMIZE_BASE is not set\nCONFIG_PHYSICAL_START=0x1000000\n";
3939

4040
static void write_file(const char *rel, const char *content) {
41-
char path[512];
42-
snprintf(path, sizeof(path), "%s%s", g_root, rel);
43-
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
44-
assert(fd >= 0);
45-
size_t n = strlen(content);
46-
assert(write(fd, content, n) == (ssize_t)n);
47-
close(fd);
41+
th_sysroot_write(rel, content);
4842
}
4943

50-
static void rm_file(const char *rel) {
51-
char path[512];
52-
snprintf(path, sizeof(path), "%s%s", g_root, rel);
53-
unlink(path);
54-
}
44+
static void rm_file(const char *rel) { th_sysroot_rm(rel); }
5545

5646
/* Run the component, capturing its stdout (the wire channel) into `cap`;
5747
* the stderr diagnostics are silenced. */
@@ -134,14 +124,7 @@ static void test_keyed_beats_unkeyed(void) {
134124
}
135125

136126
int main(void) {
137-
char tmpl[] = "/tmp/kasld_bc_rootXXXXXX";
138-
char *r = mkdtemp(tmpl);
139-
assert(r != NULL);
140-
snprintf(g_root, sizeof(g_root), "%s", r);
141-
char dir[300];
142-
snprintf(dir, sizeof(dir), "%s/boot", g_root);
143-
mkdir(dir, 0755);
144-
setenv("KASLD_SYSROOT", g_root, 1);
127+
th_sysroot_init("boot_config");
145128

146129
TEST_SUITE("test_boot_config");
147130
BEGIN_CATEGORY("kernel-config provenance -> confidence");

tests/test_boot_params_e820.c

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ int bpe820_main(void);
2929
#undef main
3030

3131
#include "test_harness.h"
32+
#include "test_sysroot.h"
3233

3334
#include <assert.h>
3435
#include <fcntl.h>
@@ -38,19 +39,6 @@ int bpe820_main(void);
3839
#include <sys/types.h>
3940
#include <unistd.h>
4041

41-
static char g_root[256];
42-
43-
static void mkparents(const char *path) {
44-
char buf[512];
45-
snprintf(buf, sizeof(buf), "%s", path);
46-
for (char *p = buf + 1; *p; p++)
47-
if (*p == '/') {
48-
*p = '\0';
49-
mkdir(buf, 0755);
50-
*p = '/';
51-
}
52-
}
53-
5442
static void put_le(unsigned char *p, unsigned long long v, int n) {
5543
for (int i = 0; i < n; i++)
5644
p[i] = (unsigned char)(v >> (8 * i));
@@ -70,13 +58,7 @@ static void e820_set(int idx, unsigned long long addr, unsigned long long size,
7058

7159
static void stage_zeropage(int nent) {
7260
zp[0x1e8] = (unsigned char)nent;
73-
char path[512];
74-
snprintf(path, sizeof(path), "%s/sys/kernel/boot_params/data", g_root);
75-
mkparents(path);
76-
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
77-
assert(fd >= 0);
78-
assert(write(fd, zp, sizeof(zp)) == (ssize_t)sizeof(zp));
79-
close(fd);
61+
th_sysroot_write_n("/sys/kernel/boot_params/data", zp, sizeof(zp));
8062
}
8163

8264
static char cap[16384];
@@ -159,11 +141,7 @@ static void test_boot_params_e820_acpi_bands(void) {
159141
}
160142

161143
int main(void) {
162-
char tmpl[] = "/tmp/kasld_bpe820_rootXXXXXX";
163-
char *r = mkdtemp(tmpl);
164-
assert(r != NULL);
165-
snprintf(g_root, sizeof(g_root), "%s", r);
166-
setenv("KASLD_SYSROOT", g_root, 1);
144+
th_sysroot_init("boot_params_e820");
167145

168146
TEST_SUITE("test_boot_params_e820");
169147
BEGIN_CATEGORY("boot_params E820 RAM covering");

tests/test_dmesg_backtrace.c

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,21 @@ int dmesg_backtrace_main(void);
2121
#undef main
2222

2323
#include "test_harness.h"
24+
#include "test_sysroot.h"
2425

2526
#include <assert.h>
2627
#include <fcntl.h>
2728
#include <sys/stat.h>
2829
#include <unistd.h>
2930

30-
static char g_root[256];
3131
static char cap[8192];
3232

3333
/* Write `text` as the sysroot's /var/log/dmesg (the source the component reads
3434
* under KASLD_SYSROOT). One root for the whole suite — kasld_sysroot() caches
3535
* the path, but the file content is re-read each run, so rewriting it per case
3636
* works. */
3737
static void stage_dmesg(const char *text) {
38-
char path[320];
39-
snprintf(path, sizeof(path), "%s/var/log/dmesg", g_root);
40-
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
41-
assert(fd >= 0);
42-
size_t n = strlen(text);
43-
assert(write(fd, text, n) == (ssize_t)n);
44-
close(fd);
38+
th_sysroot_write("/var/log/dmesg", text);
4539
}
4640

4741
/* Run the component, capturing its stdout (the wire channel) into `cap`;
@@ -171,16 +165,7 @@ static void test_loongarch_pc_ra_parse(void) {
171165
}
172166

173167
int main(void) {
174-
char tmpl[] = "/tmp/kasld_bt_rootXXXXXX";
175-
char *r = mkdtemp(tmpl);
176-
assert(r != NULL);
177-
snprintf(g_root, sizeof(g_root), "%s", r);
178-
char dir[300];
179-
snprintf(dir, sizeof(dir), "%s/var", g_root);
180-
mkdir(dir, 0755);
181-
snprintf(dir, sizeof(dir), "%s/var/log", g_root);
182-
mkdir(dir, 0755);
183-
setenv("KASLD_SYSROOT", g_root, 1);
168+
th_sysroot_init("dmesg_backtrace");
184169

185170
TEST_SUITE("test_dmesg_backtrace");
186171
BEGIN_CATEGORY("oops block parser + CR3 context tag");

tests/test_dmesg_kaslr_disabled.c

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,18 @@ int dmesg_kaslr_disabled_main(void);
2020
#undef main
2121

2222
#include "test_harness.h"
23+
#include "test_sysroot.h"
2324

2425
#include <assert.h>
2526
#include <fcntl.h>
2627
#include <string.h>
2728
#include <sys/stat.h>
2829
#include <unistd.h>
2930

30-
static char g_root[256];
3131
static char cap[8192];
3232

3333
static void stage_dmesg(const char *text) {
34-
char path[320];
35-
snprintf(path, sizeof(path), "%s/var/log/dmesg", g_root);
36-
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
37-
assert(fd >= 0);
38-
size_t n = strlen(text);
39-
assert(write(fd, text, n) == (ssize_t)n);
40-
close(fd);
34+
th_sysroot_write("/var/log/dmesg", text);
4135
}
4236

4337
static void run_capture(void) {
@@ -108,16 +102,7 @@ static void test_unknown_disabled_line_emits_nothing(void) {
108102
}
109103

110104
int main(void) {
111-
char tmpl[] = "/tmp/kasld_kd_rootXXXXXX";
112-
char *r = mkdtemp(tmpl);
113-
assert(r != NULL);
114-
snprintf(g_root, sizeof(g_root), "%s", r);
115-
char dir[300];
116-
snprintf(dir, sizeof(dir), "%s/var", g_root);
117-
mkdir(dir, 0755);
118-
snprintf(dir, sizeof(dir), "%s/var/log", g_root);
119-
mkdir(dir, 0755);
120-
setenv("KASLD_SYSROOT", g_root, 1);
105+
th_sysroot_init("dmesg_kaslr_disabled");
121106

122107
TEST_SUITE("test_dmesg_kaslr_disabled");
123108
BEGIN_CATEGORY("KASLR-disabled line classification");

0 commit comments

Comments
 (0)