The 64 MiB reply-budget refusal rendered as "exceeds64 MiB" because the message word and the limit were joined. The pwntools sibling names the same budget as "exceeds the 64 MiB reply budget"; use that wording. The output-limit path had no producer regression, so the pwndbg suite gains one that drives the real 64 MiB boundary, and the suite is now part of test:python instead of running only when invoked by hand. Co-authored-by: XiaoBinGan <XiaoBinGan@users.noreply.github.com>
127 lines
6.2 KiB
Python
127 lines
6.2 KiB
Python
"""Producer regressions with controlled GDB state, not native debugger qualification."""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import runpy
|
|
import sys
|
|
import tempfile
|
|
import types
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
|
|
BRIDGE = Path(__file__).with_name("core_context.py")
|
|
|
|
|
|
class CoreContextTests(unittest.TestCase):
|
|
def inspect(self, failure=None, connection="core", symbols=None, architecture="x86_64-little-endian", objfile="recorded-executable"):
|
|
commands, reads, queries = [], [], []
|
|
state = {"executable": None, "symbols": None, "connection": None}
|
|
gdb = types.ModuleType("gdb")
|
|
gdb.VERSION = "16.3"
|
|
gdb.selected_inferior = lambda: types.SimpleNamespace(
|
|
connection=state["connection"], pid=123)
|
|
gdb.current_progspace = lambda: types.SimpleNamespace(filename=state["symbols"])
|
|
|
|
def execute(command, to_string):
|
|
self.assertTrue(to_string)
|
|
commands.append(command)
|
|
if command == failure:
|
|
raise RuntimeError("controlled command failure")
|
|
if command == "exec-file /proc/self/exe":
|
|
state["executable"] = "/proc/self/exe"
|
|
elif command == "core-file ../object.snapshot":
|
|
if state["executable"] is None:
|
|
reads.append("recorded-executable")
|
|
state["executable"] = state["symbols"] = "recorded-executable"
|
|
state["connection"] = types.SimpleNamespace(type=connection)
|
|
elif command == "file":
|
|
state["executable"] = None
|
|
state["symbols"] = symbols
|
|
|
|
gdb.execute = execute
|
|
pwndbg = types.ModuleType("pwndbg")
|
|
pwndbg.aglib = types.ModuleType("pwndbg.aglib")
|
|
pwndbg.aglib.vmmap = types.ModuleType("pwndbg.aglib.vmmap")
|
|
pwndbg.lib = types.ModuleType("pwndbg.lib")
|
|
pwndbg.lib.version = types.ModuleType("pwndbg.lib.version")
|
|
pwndbg.lib.version.__version__ = "2026.09.15"
|
|
|
|
def maps():
|
|
self.assertIsNone(state["executable"], "Helper executable must be discarded before mapping queries")
|
|
queries.append("maps")
|
|
return [types.SimpleNamespace(start=4096, end=8192, offset=0, flags=5, objfile=objfile)]
|
|
|
|
pwndbg.aglib.vmmap.get = maps
|
|
modules = {module.__name__: module for module in [
|
|
gdb, pwndbg, pwndbg.aglib, pwndbg.aglib.vmmap, pwndbg.lib, pwndbg.lib.version]}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
stage = root / "debugger"
|
|
stage.mkdir()
|
|
plugin = root / "gdbinit.py"
|
|
plugin.write_text("")
|
|
request = stage / "request.json"
|
|
reply = stage / "reply.json"
|
|
request.write_text(json.dumps({"gdbinit": str(plugin), "architecture": architecture, "snapshot_path": str(root / "object.snapshot"), "reply_path": str(reply)}))
|
|
with patch.dict(sys.modules, modules), patch.dict(os.environ, REA_CORE_CONTEXT_REQUEST=str(request)):
|
|
runpy.run_path(str(BRIDGE))
|
|
result = json.loads(reply.read_text())
|
|
return result, commands, reads, queries
|
|
|
|
def test_core_load_never_opens_recorded_executable_and_queries_only_after_discard(self):
|
|
result, commands, reads, queries = self.inspect()
|
|
self.assertTrue(result["ok"], result)
|
|
self.assertEqual(reads, [])
|
|
self.assertEqual(queries, ["maps"])
|
|
self.assertEqual(commands[-4:], ["exec-file /proc/self/exe", "set architecture i386:x86-64", "core-file ../object.snapshot", "file"])
|
|
self.assertLess(commands.index("set debug-file-directory ./no-libraries"), commands.index("core-file ../object.snapshot"))
|
|
self.assertIsNone(result["value"]["executable"])
|
|
self.assertEqual(result["value"]["maps"][0]["current_file_identity"], "unknown")
|
|
|
|
def test_failed_guard_core_load_or_discard_never_queries_mappings(self):
|
|
for command in ["exec-file /proc/self/exe", "set architecture i386:x86-64", "core-file ../object.snapshot", "file"]:
|
|
with self.subTest(command=command):
|
|
result, commands, reads, queries = self.inspect(failure=command)
|
|
self.assertFalse(result["ok"])
|
|
self.assertEqual(reads, [])
|
|
self.assertEqual(queries, [])
|
|
if command in ["exec-file /proc/self/exe", "set architecture i386:x86-64"]:
|
|
self.assertNotIn("core-file ../object.snapshot", commands)
|
|
|
|
def test_cross_architecture_uses_the_validated_decoder_profile(self):
|
|
result, commands, reads, queries = self.inspect(architecture="aarch64-little-endian")
|
|
self.assertTrue(result["ok"], result)
|
|
self.assertEqual(reads, [])
|
|
self.assertEqual(queries, ["maps"])
|
|
self.assertLess(commands.index("set architecture aarch64"), commands.index("core-file ../object.snapshot"))
|
|
|
|
def test_unknown_architecture_refuses_before_loading_core(self):
|
|
result, commands, reads, queries = self.inspect(architecture="unknown")
|
|
self.assertFalse(result["ok"])
|
|
self.assertEqual(result["reason"], "unsupported")
|
|
self.assertNotIn("core-file ../object.snapshot", commands)
|
|
self.assertEqual(reads, [])
|
|
self.assertEqual(queries, [])
|
|
|
|
def test_output_limit_message_separates_the_words_and_names_the_limit(self):
|
|
result, _, reads, queries = self.inspect(objfile="m" * (64 * 1024 * 1024))
|
|
self.assertFalse(result["ok"])
|
|
self.assertEqual(result["reason"], "output-limit")
|
|
self.assertNotIn("exceeds64", result["message"])
|
|
self.assertEqual(result["message"], "Complete debugger context exceeds the 64 MiB reply budget.")
|
|
self.assertEqual(reads, [])
|
|
self.assertEqual(queries, ["maps"])
|
|
|
|
def test_wrong_connection_or_remaining_symbols_never_queries_mappings(self):
|
|
for options in [{"connection": "native"}, {"symbols": "unexpected-symbols"}]:
|
|
with self.subTest(options=options):
|
|
result, _, reads, queries = self.inspect(**options)
|
|
self.assertFalse(result["ok"])
|
|
self.assertEqual(reads, [])
|
|
self.assertEqual(queries, [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|