From 929bcf0e7493509f772eb0b82e9e8b177796699b Mon Sep 17 00:00:00 2001 From: Gregory Lirent Date: Wed, 24 Jun 2026 15:08:07 +0300 Subject: [PATCH] fix(discovery): tolerate CRLF line endings in mtree parsing mtree_low_split anchored the system flatview on "Root memory region: system" followed by LF/space/EOF, but QEMU's HMP `info mtree -f` output is CRLF, so the byte after "system" is '\r'. The anchor was rejected, the parser returned 0 (fail-closed), and on a real guest the daemon never attached the VM (low=0 => ok=0). The synthetic LF-only fixture hid this; the fix is verified against the real CRLF output. Accept '\r' in the anchor check (LF-only input still works) and add a regression test that re-encodes the fixture as CRLF in memory. Bump 0.3.7. --- CMakeLists.txt | 2 +- src/discovery/linux/mtree.c | 7 ++++--- src/test/test_mtree.c | 22 ++++++++++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 28f22d9..0cbc5bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.16) # Single source of truth for the version: CI passes -DVMSIG_VERSION=${TAG#v}, so the project # version (-> libvgpu-perception SONAME/.so version) and the .deb version come from one tag. -set(VMSIG_VERSION "0.3.6" CACHE STRING "Release version (MAJOR.MINOR.PATCH); CI passes the tag") +set(VMSIG_VERSION "0.3.7" CACHE STRING "Release version (MAJOR.MINOR.PATCH); CI passes the tag") project(vmsig VERSION ${VMSIG_VERSION} LANGUAGES C) set(CMAKE_C_STANDARD 17) diff --git a/src/discovery/linux/mtree.c b/src/discovery/linux/mtree.c index 1a49d1d..62f51bf 100644 --- a/src/discovery/linux/mtree.c +++ b/src/discovery/linux/mtree.c @@ -95,10 +95,11 @@ static const char* find_system_flatview(const char* text, const char** body_end) const char* anchor = "Root memory region: system"; const char* p = text; while ((p = strstr(p, anchor)) != NULL) { - /* The root name must end the token (newline/EOF) — reject "system.flash0" etc., - * and reject roots that merely contain the word elsewhere. */ + /* The root name must end the token (CR/LF/space/EOF) — reject "system.flash0" etc., + * and reject roots that merely contain the word elsewhere. QEMU's HMP output is + * CRLF, so the byte after "system" is '\r'; accept it (LF-only input also works). */ const char* after = p + strlen(anchor); - if (*after == '\n' || *after == '\0' || *after == ' ') { + if (*after == '\n' || *after == '\0' || *after == ' ' || *after == '\r') { const char* body = strchr(p, '\n'); if (!body) return NULL; body++; /* first region line */ diff --git a/src/test/test_mtree.c b/src/test/test_mtree.c index c69f20d..d3478c6 100644 --- a/src/test/test_mtree.c +++ b/src/test/test_mtree.c @@ -32,6 +32,18 @@ static char* slurp(const char* path) { return buf; } +/* 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; +} + /* Case B: a minimal, NON-fragmented system flatview — one big GPA-0 ram run plus high-RAM * carrying @. Must not be broken by the new parser. */ static const char* k_happy = @@ -65,6 +77,16 @@ int main(void) { /* 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)"); + /* 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); + } free(frag); }