Repository navigation
fix(gdb_stub): accept a packet of the advertised size, and survive a refused one - #158
Conversation
Nine PRs were merged into master within minutes on 09-08, each on the base it was written against. Master has not compiled since, and the Python guards that would have named the rest never ran because the C build failed first. Compile (every C job, CodeQL, and the ARM64 kernel in the simulation): - services/linux/src/linux_security.c: embeddedos-org#119 and embeddedos-org#132 each added an #else to the same #ifndef _WIN32 in eos_busybox_install_to_rootfs(), so master has "#else after #else". The embeddedos-org#132 arm (`(void)bb;`) is the one removed -- the embeddedos-org#119 arm already uses bb and reports the unsupported platform. Guards from embeddedos-org#121 / embeddedos-org#93 that later merges walked back: - ci.yml: embeddedos-org#132 added windows-test after embeddedos-org#121's gate; the gate did not wait for it, so "CI Gate" could be green with the MSVC leg red. - test_ci_gate.py: embeddedos-org#129 gave eosim-sanity.yml and simulation-test.yml a path-filtered pull_request trigger (they test their own edits); a path-filtered check cannot be required, so both are recorded in NOT_REQUIRED with the book-build.yml reason. - tests/CMakeLists.txt: test_linux_security_paths (embeddedos-org#119) and test_pkg_fetch (embeddedos-org#115) had no add_executable(). embeddedos-org#115's replay replaced embeddedos-org#119's block with its own, and embeddedos-org#118's replay replaced that; two suites compiled against nothing. Both registered again. 41 -> 43 suites. - tests/test_kernel.c: four tests from embeddedos-org#130 and embeddedos-org#131 were defined and never called -- their RUN() lines did not survive the replay of main(). And the one that was not bookkeeping: - kernel/src/task.c: embeddedos-org#130 was merged after embeddedos-org#131 from a base that predates it, and its copy of task.c replaced embeddedos-org#131's. embeddedos-org#131 had also flattened 574 CRLF line endings, so its 1166-line diff hid a 39/21 change and the replay took embeddedos-org#130's side wholesale. Master kept embeddedos-org#131's tests and lost its kernel: the idle task could be deleted and suspended, a half-initialised TCB was published to the scheduler before its stack existed, and eos_schedule() pointed g_current_sp at the outgoing task. test_idle_task_is_permanent fails on master the moment it is called. embeddedos-org#131's task.c diff re-applied on top of embeddedos-org#130's; the result is embeddedos-org#131's file plus embeddedos-org#130's wake_armed hunk and nothing else (verified by diff against 3a00bd9). Verified locally (macOS, clang): Release build clean, 43/43 ctest; the README default configuration builds; 47/47 pytest. Not in this PR: the nightly "Upstream drift" job builds eBoot at master and eBoot master is broken separately (its own repair PR); bump EBOOT_COMMIT in eos-simulation.yml once that lands.
Re-registering it put it in front of the MSVC leg (embeddedos-org#132) for the first time, where <unistd.h> does not exist. The suite probes with access(), open(O_CREAT) and rmdir to prove refused inputs never reach a shell; on Windows the library's own #else arm refuses every install/build call before is_path_safe() matters, so there is nothing for it to test there.
… gives 1 MB Re-registering the POSIX-only suite let the MSVC leg reach ctest for the first time, and test_firmware died there with SEGFAULT: five tests declare a stack EosConfig (sizeof 3,953,236 on this ABI), which the 8 MB default stack on Linux and macOS absorbs and MSVC's 1 MB does not. test_config.c already keeps its EosConfig static for the same reason; the same here.
kernel.h declared eos_task_delete() and eos_task_suspend() without saying what handle 0 is. It is the idle task, it is permanent, and both calls return EOS_KERN_INVALID for it -- as they do for an out-of-range handle or an empty slot. The doc comments now say so, in the header's Doxygen style. eos_schedule() has an early return for "no runnable task" that is unreachable by construction: slot 0 cannot be deleted or suspended, the idle task only spins or waits for an interrupt and so never blocks, and find_next_task() can select it because of the "best < 0 ||" term. The comment records that reasoning, and that the return is self-healing if it were ever taken: the outgoing task stays READY and the next eos_schedule() marks it RUNNING. task.c also carried two stray blank lines at EOF from the re-apply of embeddedos-org#131; with them gone, the diff against 3a00bd9 is embeddedos-org#130's wake_armed hunk plus this comment and nothing else.
…rootfs is_path_safe() refuses NULL, but eos_busybox_install_to_rootfs() read bb->source_dir to hand it to the predicate, so a NULL bb reached the dereference before the guard could see it: a crash rather than the -1 every other refusal in this file produces. Both arguments are now checked first, before either is read. test_linux_security_paths gains a case for it: (NULL, dir) and (&bb, NULL) both return -1, and the directory gets no /init. With the guard removed and the target rebuilt, the suite dies with SIGSEGV (exit 139) on the first call; with it restored, the suite passes.
Records under Unreleased what embeddedos-org#144 did and why: the "#else after #else" that stopped master compiling after the 09-08 batch merge, the guards the replays walked back (two suites re-registered, four test_kernel tests wired into main(), windows-test in the CI gate, the two path-filtered simulation workflows classified), and the NULL guard in eos_busybox_install_to_rootfs(). The scheduler change gets its own Changed entry because it is a contract: eos_task_delete(0) and eos_task_suspend(0) return EOS_KERN_INVALID again, as embeddedos-org#131 specified before embeddedos-org#130's replay replaced its task.c. Changed sits above Fixed, the order the older releases in this file use.
…end checks The eos_schedule() comment claimed the early return was self-healing. It is not: find_next_task() scans every slot including the outgoing one, so next < 0 means nothing is READY, and the return leaves the state exactly as the canary check left it. The comment now says so, and that recovery is a fault handler's job rather than the scheduler's. eos_task_suspend() refuses a slot whose entry is NULL, which is what the doc comment now says instead of the looser "holds no task"; a slot the canary check marked DELETED still has its entry set and is accepted. tests/CMakeLists.txt: the reason test_linux_security_paths is POSIX-only is that the shell-building bodies are compiled out on Windows, not that the predicate is skipped there.
…refused one qSupported announced PacketSize=400 -- 1024 bytes -- over a 512-byte receive buffer. GDB sends packets up to the size it is told, so any packet longer than 510 bytes was one the stub had promised to accept and then NACKed. recv_packet() made it worse: it stopped reading at the buffer's end, took the next two body bytes for the checksum, and left the rest of the body in the stream to be scanned for the next '$'. And the dispatch loop read every negative result as a dead transport, so the first corrupt or oversized packet ended the debug session; GDB, having been sent '-', would resend into a stub that had stopped listening. The size is derived from the buffer now (GDB_PKT_MAX, reported as PacketSize=1ff), so the two cannot drift apart again. A body that does not fit is drained to its '#', its checksum consumed, and the packet refused with '-', so the next packet is read from where it really starts. A refused packet returns GDB_RECV_BAD_PACKET and the loop waits for the resend; GDB_RECV_TRANSPORT_GONE is the only result that ends the session. Tests, in tests/test_debug.c through the scripted transport: a packet of exactly the advertised size is acknowledged and answered, and the D after it handled; one 40 bytes longer is refused and the D after it still answered; a packet with a bad checksum is refused and the D after it still answered. Against the unfixed stub the first fails at the ack.
srpatcha
left a comment
There was a problem hiding this comment.
Review — eos#158 "fix(gdb_stub): accept a packet of the advertised size, and survive a refused one"
head: 20c0b45 author: Kartikey1306 ci: pass — 30 green, 0 red; Create GitHub Release and assign skipped
Verdict: First look at 9569226..20c0b45 (one commit — debug/src/gdb_stub.c +36/−12,
tests/test_debug.c +85, CHANGELOG.md +1) as the body asks; the other nine files belong to
#144, reviewed separately at this exact base. The diagnosis is right and the three claimed
behaviours all reproduce: I built the tree, ran ctest (43/43), and confirmed by negative
control that first_ack() == '+' fails against the unfixed stub. But the fix trades a
desync for a hang. Replacing while (i < maxlen - 1) with for (;;) removed the only bound
on the body loop: a stream that never contains # now reads forever. I measured it — 2,000,000
reads and still going, against 514 on the code this replaces.
Findings
| # | Severity | File:line | Finding | Recommended fix |
|---|---|---|---|---|
| 1 | High (P1) | debug/src/gdb_stub.c:71-79 |
The body loop is now unbounded, so a peer that never sends # hangs the exception handler forever. The old loop was while (i < maxlen - 1), which terminated after at most 511 iterations — badly (it mistook body bytes for the checksum), but it terminated. The new for (;;) exits only on # or on a failed read. On a blocking UART or TCP transport neither happens for a peer that keeps sending: eos_gdb_handle_exception() never returns, and the faulted CPU never proceeds. Measured, not inferred: a harness whose io.read returns 'x' forever ran 2,000,000 reads and was still reading against this head (harness cap, exit 42), and returned after 514 reads against 9569226's gdb_stub.c — same harness, same binary layout, only the file swapped. This is malformed-input handling on an externally reachable parser, which .ai/security.md requires defined behaviour for, and .ai/reviewer.md lists timeout among the failure paths a review must check. It is a regression this commit introduces, on the exact code path the commit exists to fix. |
Bound the drain. Keep a separate counter for bytes consumed after the buffer fills and give up once it exceeds a stated limit — e.g. if (++drained > GDB_PKT_MAX) return GDB_RECV_BAD_PACKET; inside the else arm, so a body that is merely oversized is still drained and refused (which is the behaviour the PR wants and its test covers), while a body that never ends is abandoned. GDB_RECV_BAD_PACKET rather than GDB_RECV_TRANSPORT_GONE keeps the session alive for the resend; the stream may then be out of step, but the do { } while (c != '$') resynchroniser at :65-68 is what handles that and it is unchanged. One test: push a body of GDB_PKT_MAX * 2 bytes with no #, assert the call returns. |
| 2 | Medium (P2) | debug/src/gdb_stub.c:110-114 vs :130-134, :146-150 |
The advertised size is now consistent with the receive buffer, but still not with what the memory handlers accept — the same advertise/accept mismatch #157 is about, one layer up. qSupported reports PacketSize=1ff (511), so GDB sizes its bulk m/M transfers from 511 bytes of packet; handle_read_mem() and handle_write_mem() both refuse len > 128 with E01, i.e. a payload of 256 hex digits. A GDB that takes the stub at its word on a M will be told E01 for a request the stub invited — which is the shape of the defect the commit message describes ("GDB sends packets up to the size it is told"). Pre-existing, not introduced here: at PacketSize=400 the gap was wider. But this PR is the one that makes the advertised number mean something, and it leaves the half a user actually hits. Rated Medium rather than High because it degrades a debug session rather than corrupting it — E01 is a clean refusal with a test behind it (gdb rejects M packet with oversized length), not memory damage. |
Derive the advertised size from the tightest real limit, the way GDB_PKT_MAX now derives from the buffer: either raise the m/M cap and the 128-byte buf[]/resp[257] with it, or advertise a PacketSize the memory handlers can actually satisfy. Whichever way, one constant, cited by both — the argument in the comment at :52-55 applies unchanged. If the 128-byte cap is deliberate (it looks it — it is tested), say so in that comment and state that PacketSize is bounded by the transport, not by the memory handlers. |
Not findings, recorded so they are not mistaken for uncovered:
- The three new tests genuinely execute.
ctest --verboseshows[PASS] gdb packet of the advertised size is accepted,… longer than advertised does not desync,… corrupt checksum is NACKed and the session continues, and the file's=== ALL DEBUG TESTS PASSED ===. A test
defined and never called frommain()was the thing worth checking; it is not that. - No check was weakened: no test disabled, no assertion removed, no lint loosened, no
|| true, no permission widened. The commit adds three assertions' worth of coverage and
removes none. The one-line.github/workflows/ci.ymlchange in the 12-file view belongs to
#144, not to this commit. buf[i] = '\0'at:80writesbuf[511]at most, the last byte ofchar pkt[512]. No
overflow; the accept boundary is exactly the advertised one.handle_query's newchar supported[64]withsnprintfis bounded and generous for
PacketSize=1ff;swbreak+;hwbreak+(31 bytes).- The
PacketSizevalue is wire-visible and it changed (400→1ff), so brief item 8
applies. The body and the CHANGELOG both state the change and why, which satisfies it.
Architecture conformance
Conforms. §21 places eos in Tier 1 — Foundation, and debug/ is inside the owning repo,
so §21.1 is not engaged — no repository boundary moves and no subsystem is split out. §5.1's
direction-of-dependency law holds: debug/src/gdb_stub.c includes only eos/gdb_stub.h,
<string.h> and <stdio.h>, and the commit adds no #include, no link line and no manifest
entry, so nothing points up a tier. The stub calls eos_gdb_read_mem/eos_gdb_write_mem,
which are declared in its own header — no new edge toward eAI, eNI, eBrowser, eOffice, health
or aerospace. §8's eBoot TCB rules are not engaged: this is the debug stub, not boot code.
The clause served is §6.1's "Kernel tracing hooks should exist without requiring tracing in
production images" and, more directly, §25.2's "Debugger, trace, memory/flash reporting" as
an MLP requirement — a debug session that ended on the first corrupt packet is not that.
§28's evidence model is respected: the PR claims "Verified" and backs each row with the
scripted-transport harness, which I re-ran.
No proposal appended. Nothing here shows the master design wrong, stale or silent — §6.1
and §25.2 name the requirement correctly and this is the implementation catching up. Finding 2
is a code-level inconsistency, not a design gap.
Proposed changes
1. Bound the drain (finding 1). Inside recv_packet()'s else arm:
else if (++drained > GDB_PKT_MAX) return GDB_RECV_BAD_PACKET;
with `int drained = 0;` beside `overflow`. A test that pushes a body of
2 * GDB_PKT_MAX bytes with no '#' and asserts the call returns.
This is the one that should not merge without an answer.
2. Reconcile PacketSize with the m/M cap, or document why they differ
(finding 2). Not blocking.
No fix PR opened. Finding 1 is High, which is inside the brief's autofix rule, but the
defect exists only on this PR's branch — origin/master's recv_packet() still has the
bounded while (i < maxlen - 1) loop, verified by the negative control above. A branch cut
from the default branch, which is what fix-start.sh produces, would have nothing to patch,
and the brief forbids touching a branch belonging to an existing PR. The repair is delivered as
text and the harness that demonstrates the defect is described precisely enough to reproduce.
Verification performed for this review
The eos checkout has a clean working tree (git status --short empty, before and after)
and was not modified — no worktree, checkout, stash or reset. The PR head was fetched
read-only into refs/autoreview/pr158. Everything below ran against
git archive 20c0b45b | tar -x into a temporary directory outside the repository, with GCC 15
and CMake/Ninja from the host. Nothing was committed or pushed.
| Check | Result |
|---|---|
cmake -B build/host -G Ninja -DCMAKE_BUILD_TYPE=Release -DEOS_BUILD_TESTS=ON -DEOS_PRODUCT=vbox_test |
PASS |
cmake --build build/host --parallel 4 |
PASS |
ctest --output-on-failure --no-tests=error --timeout 120 --parallel 4 |
PASS — 100%, 43/43, 0 failed. Matches the body's "43/43" exactly |
ctest -R test_debug --verbose |
All three new tests execute and pass, plus the 12 pre-existing ones |
Negative control — debug/src/gdb_stub.c restored to 9569226, test_debug rebuilt |
FAIL, as claimed: test_debug.c:219: test_gdb_packet_of_the_advertised_size_is_accepted: Assertion 'first_ack() == '+'' failed, subprocess aborted. Restored afterwards; ctest -R test_debug green again |
Unbounded-drain harness, this head — io.read returns 'x' forever, no # |
2,000,000 reads, still reading (harness cap) — finding 1 |
Same harness, 9569226's gdb_stub.c |
returned after 514 reads — the bound this commit removes |
| Compiler warnings in the changed files | none. gdb_stub.c and test_debug.c produce no warnings. The build emits 89 warnings in total, all pre-existing and all elsewhere (core/src/config.c 32 -Wstringop-truncation, systems/src/rootfs.c 7 -Wformat-truncation, and others) — not this PR's, recorded only so the clean result above is not read as a clean build |
recv_packet() call sites |
exactly one, gdb_stub.c:238, and it is the one taught the new return codes |
PacketSize / buffer references anywhere else |
none outside gdb_stub.c, test_debug.c and CHANGELOG.md; no second copy of the constant to drift |
git show --stat 20c0b45b |
3 files, +122/−12 — the body's scope claim is accurate |
CI on this head (checks.txt) |
30 pass, 0 fail, incl. Build & Test (Linux x86_64), Build & Test (Windows MSVC), Host Tests (x86_64), Cross-compile ARM Cortex-M4 (×2), Cross-compile ARM64 kernel, QEMU Boot Test, QEMU ARM64 simulation (13 TCs), Static Analysis (cppcheck + clang-tidy), CodeQL, Analyze (C/C++), policy / Policy / Linked Issue |
| PR metadata | mergeable: MERGEABLE, mergeStateStatus: BLOCKED, reviewDecision: REVIEW_REQUIRED |
Not checked
- No real GDB was attached. Every behaviour above went through
tests/test_debug.c's
scripted transport or my own harness, both of which are in-process arrays, not a serial line
or a socket. In particular I did not verify GDB's own interpretation ofPacketSize—
whether it counts the$,#and the two checksum bytes, and how much it reserves as
overhead when sizing anm/M. Finding 2's claim that GDB will request more than 128 bytes
of memory atPacketSize=1ffis Inferred from the protocol and the 128-byte cap in the
handlers, not observed. The finding's other half — that the stub advertises 511 and refuses
memory requests above 128 — is read directly from:110-114,:131and:147and is not in
doubt. - The blocking-read assumption behind finding 1 was not tested on a real transport. My
harness'sreadalways succeeds, which models a peer that keeps sending. A transport whose
readreturns 0 on idle — as the test harness's does — yieldsGDB_RECV_TRANSPORT_GONEand
terminates. The hang requires a peer that keeps the bytes coming, or a blocking read; both are
ordinary for UART and TCP, andgdb_stub.h:20'sint (*read)(void*, uint8_t*, int)does not
document which. That ambiguity is itself worth a sentence in the header. - Cross-compilation — NOT RUN locally. Only the host x86_64 build was configured and built.
The ARM Cortex-M4, ARM64 and MSVC legs are green in CI and I did not reproduce them. - QEMU simulation — NOT RUN locally. Green in CI; not reproduced.
- cppcheck / clang-tidy — NOT RUN locally. Not invoked by the host build. The
Static Analysisjob is green on this head, and neither tool is likely to flagfor (;;)
with areturninside it, so finding 1 would not have been caught there either. - The #144 half of the 12-file diff is not reviewed here — reviewed separately at head
95692266, which is this PR's base. If #144 changes, the 43/43 figure no longer describes
what will merge. mergeStateStatus: BLOCKED—REVIEW_REQUIREDplus the #144 stack. Land #144 first, as
the body says.
Automated architecture review of 20c0b45b858b — scheduled, model claude-opus-5, checked against the EmbeddedOS Master Design v2.0. Advisory only: this reviewer never approves, requests changes, or merges. Reply here to discuss or push back — a wrong finding is a bug worth reporting.
…cket size The previous commit replaced `while (i < maxlen - 1)` with `for (;;)` so an oversized body is drained to its '#' rather than mistaken for the checksum. That removed the only bound on the loop: a peer that keeps sending and never a '#' held eos_gdb_handle_exception() forever, on the exact path the change exists to fix. Measured in review: two million reads and counting, against 514 before. The drain counts the bytes it discards and gives up after another GDB_PKT_MAX of them: '-' goes out, GDB_RECV_BAD_PACKET comes back, the session stays up, and the '$' search resynchronises on whatever comes next. A body that is merely too long is still drained and refused. The memory handlers had the same advertise/accept mismatch one layer up: qSupported now says PacketSize=1ff, so GDB sizes an M transfer from 511 bytes of packet, and handle_read_mem()/handle_write_mem() refused len > 128 with E01 -- a request the stub invited. GDB_MEM_MAX is derived from GDB_PKT_MAX the way GDB_PKT_MAX is derived from the buffer: ((511 - 15) / 2) = 248, with 15 the longest "M<addr>,<len>:" header, and the buffers are sized by it. tests/test_debug.c: a read that hands out body bytes until a cap and only then ends the transport; the stub must ack '-' after at most the buffer plus one more body's worth (the unbounded loop wrote nothing at all before the cap). And an M one byte past the cap, computed from the advertised size, is E01 while the cap itself is above the old literal. The accept path cannot run here -- the memory hooks dereference the address -- so tests/unit/test_gdb_stub_limits.py pins the derivations in the source: PacketSize from GDB_PKT_MAX, GDB_MEM_MAX from it, both handlers comparing against it with no literal, and the largest m reply fitting send_packet()'s buffer.
|
Both taken, in Finding 1 — the drain never ends. The drain now counts what it discards and gives up after another Finding 2 — the memory cap. Derived, the way you asked:
|
srpatcha
left a comment
There was a problem hiding this comment.
Reviewed. Approve. A 248-byte memory read is now a 500-byte reply inside the 512-byte buffer, and test_debug aborts on master and passes here. Nit (pre-existing): send_packet() (gdb_stub.c:43-49) does not clamp snprintf's return value to the buffer. Not tested against a real GDB.
Stacked on #144 (master's Python guards are red without it). Review
9569226..337b06dfor this change alone: two commits.20c0b45is the fix --debug/src/gdb_stub.c(+36/−12),tests/test_debug.c(+3 tests),CHANGELOG.md.337b06danswers the review's two findings: the drain is bounded and them/Mcap is derived from the packet size (debug/src/gdb_stub.c,tests/test_debug.c, newtests/unit/test_gdb_stub_limits.py,CHANGELOG.md).Problem (eos #157)
qSupportedansweredPacketSize=400— 1024 bytes — whileeos_gdb_handle_exception()receives intochar pkt[512]. GDB sends packets up to the size it is told, so any packet over 510 bytes was one the stub had promised to accept and then NACKed. Two things made that worse than a wrong number:recv_packet()stopped reading at the buffer's end and took the next two body bytes as the checksum, so the stream was out of step after an oversized packet.recv_packet()result as a dead transport and exited. After the-, GDB resends — into a stub that had stopped listening. The same happened on the first packet with a corrupt checksum.Verified in the scripted-transport harness: on the unfixed stub a packet of exactly the advertised size is NACKed (
first_ack() == '-').Change
GDB_PKT_MAX = GDB_PKT_BUF - 1), andqSupportedreports it (PacketSize=1ff), so the two cannot drift apart again.#, its checksum consumed, and the packet refused with-— the next packet is read from where it really starts.recv_packet()returnsGDB_RECV_BAD_PACKETfor a refused packet andGDB_RECV_TRANSPORT_GONEwhen a read fails; only the latter ends the session. A refused packet is followed by GDB's resend, which the loop now waits for.Nothing else in the protocol handling changes: the M/m/Z/z/q handlers and their existing rejection tests are untouched.
Tests (Verified)
test_gdb_packet_of_the_advertised_size_is_accepted— aq…body of exactlyPacketSizebytes, thenD+, empty reply8, then#forDtest_gdb_packet_longer_than_advertised_does_not_desync— 40 bytes over, thenD-, no reply to it,#forDtest_gdb_corrupt_checksum_is_nacked_and_the_session_continues—#00, thenD-, then#ctest43/43 on this head,pytest tests/unit47 passed (43 + the 4 in the new guard).Review follow-up (
337b06d)for (;;)drain was unbounded; a peer that never sends#held the exception handler foreverGDB_PKT_MAXbytes:-is written,GDB_RECV_BAD_PACKETis returned, the session stays up, and the$search resynchronises. A body that is merely too long is still drained and refused.test_gdb_body_that_never_ends_is_abandoned: a read that hands out$thenxforever up to a 20,000-byte cap and only then reports the transport gone; the stub must write its ack after at most2*PacketSize + 8reads and it must be-. Negative control against20c0b45: no ack is written before the cap (the test fails onreads_at_first_write > 0), which is the hang in miniature.m/Mrefusedlen > 128whilePacketSizeinvites 248GDB_MEM_MAX = ((GDB_PKT_MAX - GDB_MEM_HDR) / 2)withGDB_MEM_HDR = 15(the longestM<addr>,<len>:with a 32-bit address), the buffers sized by it, no literal left. The accept path cannot run intests/test_debug.c-- the memory hooks dereference the 32-bit address on a 64-bit host, as the file's header says -- so it is pinned from two sides:test_gdb_memory_transfer_one_past_the_cap_is_refusedcomputes the cap from the advertised size, asserts it is above the old 128, and that one byte past it isE01on an otherwise well-formed (+-acked) packet;tests/unit/test_gdb_stub_limits.pyasserts in the source thatPacketSizeis fedGDB_PKT_MAX, thatGDB_MEM_MAXis that derivation, that both handlers compare against it with no128/256/257literal, and that the largestmreply fitssend_packet()'s buffer. Against20c0b45, 3 of its 4 fail.Closing issue
Fixes #157