2026-06-24 14:26:50 +03:00
|
|
|
/* test_mtree.c — unit tests for mtree_low_split (the below-4G split parser). Pure text in,
|
|
|
|
|
* number out; no QMP/transport. The fragmented fixture reproduces the structural traps the
|
|
|
|
|
* old heuristic tripped on (Hyper-V synic overlays, smbase/tseg blackhole holes, rom holes)
|
|
|
|
|
* plus a decoy non-system flatview that carries its OWN GPA-0 stub and a DIFFERENT @offset,
|
|
|
|
|
* proving the system address space is selected (not "first match in the text"). */
|
|
|
|
|
#define _GNU_SOURCE
|
|
|
|
|
#include "mtree.h"
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
#ifndef FIXTURE_DIR
|
|
|
|
|
#define FIXTURE_DIR "."
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
static int g_fail = 0;
|
|
|
|
|
#define CHECK(cond, msg) do { if (!(cond)) { printf(" FAIL: %s\n", (msg)); g_fail = 1; } } while (0)
|
|
|
|
|
|
|
|
|
|
/* Slurp a whole text file into a heap buffer (NUL-terminated). NULL on error. */
|
|
|
|
|
static char* slurp(const char* path) {
|
|
|
|
|
FILE* f = fopen(path, "rb");
|
|
|
|
|
if (!f) return NULL;
|
|
|
|
|
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; }
|
|
|
|
|
long sz = ftell(f);
|
|
|
|
|
if (sz < 0) { fclose(f); return NULL; }
|
|
|
|
|
rewind(f);
|
|
|
|
|
char* buf = malloc((size_t)sz + 1);
|
|
|
|
|
if (!buf) { fclose(f); return NULL; }
|
|
|
|
|
size_t got = fread(buf, 1, (size_t)sz, f);
|
|
|
|
|
fclose(f);
|
|
|
|
|
buf[got] = 0;
|
|
|
|
|
return buf;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 15:08:07 +03:00
|
|
|
/* Re-encode every '\n' as '\r\n' (QEMU's HMP output is CRLF). Caller frees; NULL on OOM. */
|
|
|
|
|
static char* to_crlf(const char* lf) {
|
|
|
|
|
size_t n = 0, extra = 0;
|
|
|
|
|
for (const char* p = lf; *p; p++) { n++; if (*p == '\n') extra++; }
|
|
|
|
|
char* out = malloc(n + extra + 1);
|
|
|
|
|
if (!out) return NULL;
|
|
|
|
|
char* o = out;
|
|
|
|
|
for (const char* p = lf; *p; p++) { if (*p == '\n') *o++ = '\r'; *o++ = *p; }
|
|
|
|
|
*o = 0;
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 14:26:50 +03:00
|
|
|
/* Case B: a minimal, NON-fragmented system flatview — one big GPA-0 ram run plus high-RAM
|
|
|
|
|
* carrying @<low>. Must not be broken by the new parser. */
|
|
|
|
|
static const char* k_happy =
|
|
|
|
|
"FlatView #0\n"
|
|
|
|
|
" AS \"memory\", root: system\n"
|
|
|
|
|
" Root memory region: system\n"
|
|
|
|
|
" 0000000000000000-000000007fffffff (prio 0, ram): ram0\n"
|
|
|
|
|
" 0000000080000000-0000000081ffffff (prio 0, i/o): vfio-pci-bar3\n"
|
|
|
|
|
" 0000000100000000-000000017fffffff (prio 0, ram): ram0 @0000000080000000\n";
|
|
|
|
|
|
|
|
|
|
/* Case C: text without any system flatview => fail-closed. */
|
|
|
|
|
static const char* k_no_system =
|
|
|
|
|
"FlatView #0\n"
|
|
|
|
|
" AS \"I/O\", root: io\n"
|
|
|
|
|
" Root memory region: io\n"
|
|
|
|
|
" 0000000000000000-0000000000000007 (prio 0, i/o): dma-chan\n";
|
|
|
|
|
|
|
|
|
|
int main(void) {
|
|
|
|
|
printf("test_mtree\n");
|
|
|
|
|
|
|
|
|
|
/* Cases A and E: the fragmented fixture (decoy first, system second). */
|
|
|
|
|
char path[1024];
|
|
|
|
|
snprintf(path, sizeof path, "%s/mtree_split_fragmented.txt", FIXTURE_DIR);
|
|
|
|
|
char* frag = slurp(path);
|
|
|
|
|
CHECK(frag != NULL, "fragmented fixture loaded");
|
|
|
|
|
if (frag) {
|
|
|
|
|
uint64_t low = mtree_low_split(frag);
|
|
|
|
|
/* A: fragmented low-RAM must NOT yield the GPA-0 stub end (0x18000) — the bug. */
|
|
|
|
|
CHECK(low == 0x80000000ull, "A: fragmented split == 0x80000000");
|
|
|
|
|
CHECK(low != 0x18000ull, "A: not the GPA-0 stub end (0x18000)");
|
|
|
|
|
/* E: the decoy (non-system) flatview comes FIRST and carries @0x40000000; the
|
|
|
|
|
* function must select the SYSTEM flatview (@0x80000000), not the decoy. */
|
|
|
|
|
CHECK(low != 0x40000000ull, "E: decoy flatview @offset rejected (system AS chosen)");
|
2026-06-24 15:08:07 +03:00
|
|
|
/* F: real QEMU HMP output is CRLF. The parser MUST tolerate '\r' — a synthetic
|
|
|
|
|
* LF-only fixture hid this, so the shipped parser returned 0 on the real VM mtree
|
|
|
|
|
* (-> low=0 -> VM never attached). Regression guard, independent of how git stores
|
|
|
|
|
* the fixture's line endings. */
|
|
|
|
|
char* frag_crlf = to_crlf(frag);
|
|
|
|
|
CHECK(frag_crlf != NULL, "F: CRLF copy allocated");
|
|
|
|
|
if (frag_crlf) {
|
|
|
|
|
CHECK(mtree_low_split(frag_crlf) == 0x80000000ull, "F: CRLF fragmented split == 0x80000000");
|
|
|
|
|
free(frag_crlf);
|
|
|
|
|
}
|
2026-06-24 14:26:50 +03:00
|
|
|
free(frag);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Case B: happy path (non-fragmented) still resolves to the high-RAM @offset. */
|
|
|
|
|
CHECK(mtree_low_split(k_happy) == 0x80000000ull, "B: non-fragmented happy path == 0x80000000");
|
|
|
|
|
|
|
|
|
|
/* Case C: no system flatview => 0. */
|
|
|
|
|
CHECK(mtree_low_split(k_no_system) == 0, "C: no system flatview => fail-closed 0");
|
|
|
|
|
|
|
|
|
|
/* Case D: garbage / empty => 0. */
|
|
|
|
|
CHECK(mtree_low_split("") == 0, "D: empty text => 0");
|
|
|
|
|
CHECK(mtree_low_split("not an mtree at all\n") == 0, "D: junk text => 0");
|
|
|
|
|
|
|
|
|
|
printf("mtree tests: %s\n", g_fail ? "FAIL" : "PASS");
|
|
|
|
|
return g_fail ? 1 : 0;
|
|
|
|
|
}
|