Files
rea/tests/fixtures/cutter_bridge_protocol_test.py
Mohamed Ayoub Eleuch 8a2a1a2b8b fix(cutter): preserve mixed Unicode output within the frame limit
Count real UTF-8 bytes in bounded chunks and retain authenticated omission for malformed Unicode. Preserve existing wire/resource limits and completion semantics.

Validation: producer/client regressions, fresh real Linux Cutter CLI/MCP output and Evidence recovery, and complete exact-head CI passed. Hosted unrelated CLI timeout and unchanged rerun results remain recorded in the PR.

Fixes #3330.
2026-10-17 07:30:30 +00:00

600 lines
27 KiB
Python

"""Producer-level regression tests for Cutter's authenticated local protocol."""
import hashlib
import hmac
import importlib.util
import json
import os
import socket
import sys
import tempfile
import threading
import time
import types
import unittest
import uuid
from pathlib import Path
from unittest.mock import patch
def _install_host_stubs():
cutter = types.ModuleType("cutter")
cutter.CutterPlugin = type("CutterPlugin", (), {})
sys.modules["cutter"] = cutter
qtcore = types.ModuleType("PySide6.QtCore")
qtcore.QObject = type("QObject", (), {})
qtcore.Signal = lambda *_args: types.SimpleNamespace(connect=lambda _handler: None)
qtcore.QCoreApplication = types.SimpleNamespace(applicationVersion=lambda: "Cutter test host")
pyside = types.ModuleType("PySide6")
pyside.QtCore = qtcore
sys.modules["PySide6"] = pyside
sys.modules["PySide6.QtCore"] = qtcore
_install_host_stubs()
MODULE_PATH = Path(__file__).parents[2] / "bridge" / "cutter" / "rea_bridge.py"
spec = importlib.util.spec_from_file_location("rea_cutter_bridge_test", MODULE_PATH)
bridge = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = bridge
spec.loader.exec_module(bridge)
def _frame(connection):
data = bytearray()
while b"\n" not in data:
data.extend(connection.recv(4096))
if len(data) > bridge.MAX_FRAME_BYTES:
raise AssertionError("oversized test frame")
line, remainder = bytes(data).split(b"\n", 1)
if remainder:
raise AssertionError("unexpected pipelined frame")
return json.loads(line)
def _send(connection, value):
connection.sendall(json.dumps(value, separators=(",", ":")).encode() + b"\n")
class Dispatcher:
generation = 4
current_file = "/tmp/sample.bin"
cutter_version = "test"
def __init__(self):
self.dispatch = self
self.commands = []
self.completed_commands = {}
self.lock = threading.Lock()
self.command_started = threading.Event()
self.finish_command = threading.Event()
self.status_queued = threading.Event()
def _read_current_file(self):
return self.current_file
def can_accept_command(self):
return len(self.completed_commands) < bridge.MAX_COMPLETION_RECEIPTS
def emit(self, request, reply):
def run():
kind = request.get("kind")
if kind == "command":
self.command_started.set()
elif kind == "status":
self.status_queued.set()
with self.lock:
if kind == "command":
self.finish_command.wait(2)
self.commands.append(kind)
if kind == "status":
reply.update({"ok": True, "session_id": request["session_id"], "document_generation": self.generation,
"current_file": self.current_file, "cutter_version": self.cutter_version,
"identity_status": "partial", "completed_commands": [
{"request_id": request_id, "execution_state": state}
for request_id, state in self.completed_commands.items()
]})
elif kind == "ack":
reply.update({"ok": True, "acknowledged": self.completed_commands.pop(request["request_id"], None) is not None})
else:
reply.update({"ok": True, "execution_state": "complete"})
if kind == "command" and isinstance(request.get("request_id"), str):
self.completed_commands[request["request_id"]] = "complete"
request["_event"].set()
threading.Thread(target=run, daemon=True).start()
class ServeDispatcher:
"""Small UI-dispatcher stand-in for real producer/client interoperability."""
generation = 4
current_file = "/tmp/cutter-interop-雪.bin"
cutter_version = "fixture"
def __init__(self):
self.dispatch = self
self.completed_commands = {}
self.fail_next_ack = False
self.malformed_next_ack = False
self.hide_receipts_after_ack_failure = False
def can_accept_command(self):
return len(self.completed_commands) < bridge.MAX_COMPLETION_RECEIPTS
def _read_current_file(self):
return self.current_file
def emit(self, request, reply):
if request.get("kind") == "status":
reply.update({
"ok": True,
"session_id": request["session_id"],
"document_generation": self.generation,
"current_file": self.current_file,
"cutter_version": self.cutter_version,
"identity_status": "partial",
"completed_commands": [
{"request_id": request_id, "execution_state": state}
for request_id, state in self.completed_commands.items()
] if not self.hide_receipts_after_ack_failure else [],
})
elif request.get("kind") == "ack":
if self.malformed_next_ack:
self.malformed_next_ack = False
reply.update({"ok": True, "session_id": request["session_id"]})
request["_event"].set()
return
if self.fail_next_ack:
self.fail_next_ack = False
self.hide_receipts_after_ack_failure = True
reply.update({"ok": False, "acknowledged": False})
request["_event"].set()
return
reply.update({
"ok": True,
"acknowledged": self.completed_commands.pop(request["request_id"], None) is not None,
})
self.hide_receipts_after_ack_failure = False
elif request.get("kind") == "command" and request.get("expected_generation") != self.generation:
reply.update({
"ok": False,
"error": "active-document-generation-changed",
"execution_state": "not_started",
})
self.completed_commands[request["request_id"]] = "not_started"
elif request.get("kind") == "command" and request.get("command") == "fixture-unknown":
reply.update({
"ok": False,
"error": "fixture-response-lost-after-completion",
"execution_state": "unknown",
})
self.completed_commands[request["request_id"]] = "complete"
elif request.get("kind") == "command" and request.get("command") == "fixture-fail-ack":
reply.update({
"ok": True,
"output": "ack will fail once",
"current_file": self.current_file,
"document_generation": self.generation,
"cutter_version": self.cutter_version,
"identity_status": "partial",
"execution_state": "complete",
})
self.completed_commands[request["request_id"]] = "complete"
self.fail_next_ack = True
elif request.get("kind") == "command" and request.get("command") == "fixture-malformed-ack":
reply.update({
"ok": True,
"output": "ack reply will be malformed once",
"current_file": self.current_file,
"document_generation": self.generation,
"cutter_version": self.cutter_version,
"identity_status": "partial",
"execution_state": "complete",
})
self.completed_commands[request["request_id"]] = "complete"
self.malformed_next_ack = True
else:
reply.update({
"ok": True,
"output": 'round-trip: 雪 ☃️ quote=" slash=\\\\ line=\\n tab=\\t',
"current_file": self.current_file,
"document_generation": self.generation,
"cutter_version": self.cutter_version,
"identity_status": "partial",
"execution_state": "complete",
})
if request.get("kind") == "command":
self.completed_commands[request["request_id"]] = "complete"
request["_event"].set()
def run_serve_mode():
server = bridge._BridgeServer(ServeDispatcher())
server.start()
try:
deadline = time.monotonic() + 5
while not server.descriptor.exists() and time.monotonic() < deadline:
time.sleep(0.005)
if not server.descriptor.exists():
raise RuntimeError("bridge descriptor was not published")
print("ready", flush=True)
sys.stdin.buffer.readline()
finally:
server.stop()
server.join(timeout=3)
class ResponseEncodingTest(unittest.TestCase):
def setUp(self):
self.server = bridge._BridgeServer.__new__(bridge._BridgeServer)
self.server.token = "test-token"
self.server.session_id = str(uuid.uuid4())
def assert_authenticated_response(self, frame):
expected = hmac.new(
self.server.token.encode(),
f"rea-cutter-response-v2:{self.server.session_id}:{'a' * 64}:{'b' * 64}:{frame['response']}".encode(),
hashlib.sha256,
).hexdigest()
self.assertTrue(hmac.compare_digest(frame["proof"], expected))
return json.loads(frame["response"])
def test_large_mixed_unicode_output_fits_the_authenticated_frame(self):
for suffix in ("a", "é", "雪", "😀"):
with self.subTest(suffix=suffix):
output = "a" * (5 * 1024 * 1024 - 1) + suffix
value = {"ok": True, "output": output, "execution_state": "complete"}
encoded = self.server._encode_authenticated_response(value, "a" * 64, "b" * 64)
self.assertIsNotNone(encoded)
self.assertLessEqual(len(encoded), bridge.MAX_FRAME_BYTES)
frame = json.loads(encoded)
self.assertEqual(self.assert_authenticated_response(frame), value)
self.assertIsNone(self.server._encode_authenticated_response({
"ok": True, "output": "😀" * (5 * 1024 * 1024),
"execution_state": "complete",
}, "a" * 64, "b" * 64))
def test_unencodable_output_preserves_authenticated_completion(self):
for index, surrogate in ((0, "\ud800"), (5 * 1024 * 1024 - 1, "\udfff")):
with self.subTest(index=index):
output = "a" * index + surrogate + "a" * (5 * 1024 * 1024 - index - 1)
value = {"ok": True, "output": output, "execution_state": "complete"}
sender, receiver = socket.socketpair()
with sender, receiver:
receiver.settimeout(2)
self.server._send_authenticated(sender, value, "a" * 64, "b" * 64)
response = self.assert_authenticated_response(_frame(receiver))
self.assertTrue(response["ok"])
self.assertEqual(response["execution_state"], "complete")
self.assertIsNone(response["output"])
self.assertTrue(response["output_truncated"])
class DocumentIdentityTest(unittest.TestCase):
def test_document_config_drives_generation_and_command_admission(self):
first = "/tmp/Cutter 雪 #1?.elf"
second = "/tmp/another document.elf"
configuration = {"file.path": first}
core = types.SimpleNamespace(
getConfig=configuration.__getitem__,
)
commands = []
with patch.object(bridge.cutter, "core", lambda: core, create=True), patch.object(
bridge.cutter, "cmd", lambda command: commands.append(command) or "observed", create=True
):
dispatcher = bridge._RequestDispatcher()
self.assertEqual(dispatcher.current_file, first)
self.assertEqual(dispatcher.generation, 0)
def handle(**values):
request = {**values, "_event": threading.Event(), "_cancelled": threading.Event()}
reply = {}
dispatcher._handle(request, reply)
self.assertTrue(request["_event"].is_set())
return reply
same = handle(kind="status")
self.assertEqual(same["current_file"], first)
self.assertEqual(same["document_generation"], 0)
configuration["file.path"] = second
changed = handle(kind="status")
self.assertEqual(changed["current_file"], second)
self.assertEqual(changed["document_generation"], 1)
stale = handle(kind="command", request_id=str(uuid.uuid4()), expected_generation=0, command="iI")
self.assertEqual(stale["error"], "active-document-generation-changed")
self.assertEqual(stale["execution_state"], "not_started")
self.assertEqual(commands, [])
current = handle(kind="command", request_id=str(uuid.uuid4()), expected_generation=1, command="iI")
self.assertEqual(current["output"], "observed")
self.assertEqual(current["document_generation"], 1)
self.assertEqual(commands, ["iI"])
def test_absent_or_unreadable_document_config_remains_unknown(self):
class UnreadableCore:
def getConfig(self, _key):
raise RuntimeError("document config unavailable")
for core in [
types.SimpleNamespace(getConfig=lambda _key: ""),
types.SimpleNamespace(getConfig=lambda _key: None),
types.SimpleNamespace(getConfig=lambda _key: object()),
types.SimpleNamespace(),
UnreadableCore(),
]:
with self.subTest(core=type(core).__name__), patch.object(
bridge.cutter, "core", lambda: core, create=True
):
self.assertIsNone(bridge._RequestDispatcher._read_current_file())
class HostVersionTest(unittest.TestCase):
def test_host_version_reaches_status_and_command_without_backend_attribution(self):
backend_reads = []
core = types.SimpleNamespace(
getConfig=lambda _key: None,
getVersionInformation=lambda: backend_reads.append(True) or "Rizin 0.9.1",
)
application = sys.modules["PySide6.QtCore"].QCoreApplication
with patch.object(bridge.cutter, "core", lambda: core, create=True), patch.object(
application, "applicationVersion", return_value="2.5.0-HEAD-fixture"
), patch.object(bridge.cutter, "cmd", lambda _command: "observed", create=True):
dispatcher = bridge._RequestDispatcher()
self.assertEqual(dispatcher.cutter_version, "2.5.0-HEAD-fixture")
for values in [
{"kind": "status"},
{"kind": "command", "request_id": str(uuid.uuid4()), "expected_generation": 0, "command": "iI"},
]:
request = {**values, "_event": threading.Event(), "_cancelled": threading.Event()}
reply = {}
dispatcher._handle(request, reply)
self.assertTrue(request["_event"].is_set())
self.assertEqual(reply["cutter_version"], "2.5.0-HEAD-fixture")
self.assertEqual(backend_reads, [])
def test_unavailable_host_version_stays_unknown_without_backend_fallback(self):
core = types.SimpleNamespace(
getConfig=lambda _key: None,
getVersionInformation=lambda: "Rizin 0.9.1",
)
application = sys.modules["PySide6.QtCore"].QCoreApplication
with patch.object(bridge.cutter, "core", lambda: core, create=True):
for value in ["", None, object()]:
with self.subTest(value=type(value).__name__), patch.object(
application, "applicationVersion", return_value=value
):
self.assertIsNone(bridge._RequestDispatcher().cutter_version)
with patch.object(application, "applicationVersion", side_effect=RuntimeError("host unavailable")):
self.assertIsNone(bridge._RequestDispatcher().cutter_version)
class ProtocolProducerTest(unittest.TestCase):
def test_rejects_legacy_peers_before_ui_dispatch(self):
with socket.create_connection(("127.0.0.1", self.server.port), timeout=2) as connection:
connection.sendall((json.dumps({"kind": "rea-auth-v1", "nonce": "a" * 64}) + "\n").encode())
with connection.makefile("rb") as reader:
reply = json.loads(reader.readline())
self.assertEqual(reply, {"ok": False, "error": "authentication-failed"})
self.assertEqual(self.dispatcher.commands, [])
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
os.environ["REA_CUTTER_BRIDGE_DIR"] = self.temp.name
self.dispatcher = Dispatcher()
self.server = bridge._BridgeServer(self.dispatcher)
bridge.SOCKET_TIMEOUT_SECONDS = 0.1
self.server.start()
deadline = time.monotonic() + 2
while not self.server.descriptor.exists() and time.monotonic() < deadline:
time.sleep(0.005)
self.assertTrue(self.server.descriptor.exists())
def tearDown(self):
self.server.stop()
self.server.join(timeout=2)
os.environ.pop("REA_CUTTER_BRIDGE_DIR", None)
self.temp.cleanup()
def exchange(self, request):
token = self.server.token.encode()
client_nonce = os.urandom(32).hex()
with socket.create_connection(("127.0.0.1", self.server.port), timeout=1) as connection:
_send(connection, {"kind": "rea-auth-v2", "nonce": client_nonce})
challenge = _frame(connection)
self.assertEqual(challenge["session_id"], self.server.session_id)
server_nonce = challenge["nonce"]
expected_server = hmac.new(token, f"rea-cutter-server-v2:{self.server.session_id}:{client_nonce}:{server_nonce}".encode(), hashlib.sha256).hexdigest()
self.assertTrue(hmac.compare_digest(challenge["proof"], expected_server))
request_bytes = json.dumps(request, separators=(",", ":"))
proof = hmac.new(token, f"rea-cutter-client-v2:{self.server.session_id}:{client_nonce}:{server_nonce}:{request_bytes}".encode(), hashlib.sha256).hexdigest()
_send(connection, {"request": request_bytes, "proof": proof})
frame = _frame(connection)
if "response" not in frame:
return frame, client_nonce
response = frame["response"]
expected_response = hmac.new(token, f"rea-cutter-response-v2:{self.server.session_id}:{client_nonce}:{server_nonce}:{response}".encode(), hashlib.sha256).hexdigest()
self.assertTrue(hmac.compare_digest(frame["proof"], expected_response))
return json.loads(response), client_nonce
def test_request_session_uuid_is_checked_after_secret_proof(self):
spoofed = {"kind": "status", "session_id": "client-chosen-session"}
response, _ = self.exchange(spoofed)
self.assertEqual(response["error"], "authentication-failed")
self.assertEqual(self.dispatcher.commands, [])
def test_client_chosen_session_uuid_without_secret_proof_is_not_authenticated(self):
client_nonce = os.urandom(32).hex()
with socket.create_connection(("127.0.0.1", self.server.port), timeout=1) as connection:
_send(connection, {"kind": "rea-auth-v2", "nonce": client_nonce})
_frame(connection)
request_bytes = json.dumps({"kind": "status", "session_id": "client-chosen-session"}, separators=(",", ":"))
_send(connection, {"request": request_bytes, "proof": "0" * 64})
self.assertEqual(_frame(connection)["error"], "authentication-failed")
self.assertEqual(self.dispatcher.commands, [])
def test_cancelled_command_receipt_survives_later_command_until_acknowledged(self):
dispatcher = object.__new__(bridge._RequestDispatcher)
dispatcher.generation = 4
dispatcher.current_file = "/tmp/sample.bin"
dispatcher.cutter_version = "test"
dispatcher.completed_commands = {}
dispatcher._receipt_lock = threading.Lock()
dispatcher._refresh_document_identity = lambda: None
bridge.cutter.cmd = lambda command: f"output:{command}"
def handle(value):
request = {
**value,
"_event": threading.Event(),
"_cancelled": threading.Event(),
}
reply = {}
bridge._RequestDispatcher._handle(dispatcher, request, reply)
self.assertTrue(request["_event"].is_set())
return reply
request_a = "4b18ce4d-bf31-456e-ae31-e8c547b2f9db"
request_b = "8ca93d11-107d-41ed-8b40-31dd53cd26a6"
for request_id, command in ((request_a, "A"), (request_b, "B")):
result = handle({
"kind": "command",
"request_id": request_id,
"expected_generation": 4,
"command": command,
})
self.assertTrue(result["ok"])
status = handle({"kind": "status"})
self.assertEqual(
{receipt["request_id"] for receipt in status["completed_commands"]},
{request_a, request_b},
)
acknowledged = handle({"kind": "ack", "request_id": request_a})
self.assertTrue(acknowledged["acknowledged"])
after_ack = handle({"kind": "status"})
self.assertEqual(
[receipt["request_id"] for receipt in after_ack["completed_commands"]],
[request_b],
)
def test_stale_generation_rejections_are_terminal_and_acknowledged(self):
dispatcher = object.__new__(bridge._RequestDispatcher)
dispatcher.generation = 4
dispatcher.current_file = "/tmp/sample.bin"
dispatcher.cutter_version = "test"
dispatcher.completed_commands = {}
dispatcher._receipt_lock = threading.Lock()
dispatcher._refresh_document_identity = lambda: None
def handle(value):
request = {
**value,
"_event": threading.Event(),
"_cancelled": threading.Event(),
}
reply = {}
bridge._RequestDispatcher._handle(dispatcher, request, reply)
self.assertTrue(request["_event"].is_set())
return reply
invalid_id = str(uuid.UUID(int=bridge.MAX_COMPLETION_RECEIPTS + 10))
invalid = handle({
"kind": "command",
"request_id": invalid_id,
"expected_generation": 4,
"command": "",
})
self.assertEqual(invalid["execution_state"], "not_started")
self.assertTrue(handle({"kind": "ack", "request_id": invalid_id})["acknowledged"])
for index in range(bridge.MAX_COMPLETION_RECEIPTS + 1):
request_id = str(uuid.UUID(int=index + 1))
response = handle({
"kind": "command",
"request_id": request_id,
"expected_generation": 3,
"command": "iI",
})
self.assertEqual(response["execution_state"], "not_started")
self.assertEqual(response["error"], "active-document-generation-changed")
self.assertTrue(handle({"kind": "ack", "request_id": request_id})["acknowledged"])
self.assertEqual(dispatcher.completed_commands, {})
self.assertTrue(dispatcher.can_accept_command())
def test_poisoned_rejection_is_definitively_not_started_without_dispatch(self):
self.server.poisoned = True
response, _ = self.exchange({
"kind": "command",
"session_id": self.server.session_id,
"request_id": "4b18ce4d-bf31-456e-ae31-e8c547b2f9db",
})
self.assertEqual(response["error"], "session-poisoned-after-ui-timeout")
self.assertEqual(response["execution_state"], "not_started")
self.assertEqual(self.dispatcher.commands, [])
def test_receipt_capacity_rejects_before_ui_dispatch(self):
self.dispatcher.can_accept_command = lambda: False
response, _ = self.exchange({
"kind": "command",
"session_id": self.server.session_id,
"request_id": "4b18ce4d-bf31-456e-ae31-e8c547b2f9db",
})
self.assertEqual(response["error"], "completion-receipt-capacity-exceeded")
self.assertEqual(response["execution_state"], "not_started")
self.assertEqual(self.dispatcher.commands, [])
def test_status_probe_recovers_only_after_timed_out_ui_operation_finishes(self):
timed_out, _ = self.exchange({"kind": "command", "session_id": self.server.session_id})
self.assertEqual(timed_out["error"], "ui-thread-request-timeout")
self.assertTrue(self.server.poisoned)
self.assertTrue(self.dispatcher.command_started.wait(1))
result = {}
status_thread = threading.Thread(target=lambda: result.setdefault(
"response", self.exchange({"kind": "status", "session_id": self.server.session_id})[0]
))
status_thread.start()
self.assertTrue(self.dispatcher.status_queued.wait(1))
self.dispatcher.finish_command.set()
status_thread.join(timeout=2)
self.assertFalse(status_thread.is_alive())
response = result["response"]
self.assertTrue(response["ok"])
self.assertEqual(response["session_id"], self.server.session_id)
self.assertEqual(self.dispatcher.commands, ["command", "status"])
def test_escape_expansion_uses_a_signed_truthful_truncation_fallback(self):
sender, receiver = socket.socketpair()
try:
self.server._send_authenticated(sender, {
"ok": True,
"output": '"' * 6_000_000,
"execution_state": "complete",
"current_file": "/tmp/sample.bin",
"document_generation": 4,
"identity_status": "partial",
}, "a" * 64, "b" * 64)
frame_bytes = receiver.recv(bridge.MAX_FRAME_BYTES + 1)
self.assertLessEqual(len(frame_bytes), bridge.MAX_FRAME_BYTES)
frame = json.loads(frame_bytes)
response = frame["response"]
expected = hmac.new(
self.server.token.encode(),
f"rea-cutter-response-v2:{self.server.session_id}:{'a' * 64}:{'b' * 64}:{response}".encode(),
hashlib.sha256,
).hexdigest()
self.assertTrue(hmac.compare_digest(frame["proof"], expected))
value = json.loads(response)
self.assertEqual(value["ok"], True)
self.assertEqual(value["execution_state"], "complete")
self.assertTrue(value["output_truncated"])
self.assertIsNone(value["output"])
finally:
sender.close()
receiver.close()
if __name__ == "__main__":
if sys.argv[1:] == ["--serve"]:
run_serve_mode()
else:
unittest.main()