From 68a805563fa8baeeebbe6b48637635d5d9be8d29 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:30:53 +0100 Subject: [PATCH] fix(telemetry): Reject a JSON reply or a weights argument that is not an object Both workload scripts decoded JSON and went straight to .get() or .items(). An array or a scalar decodes fine and then raises AttributeError, which is not a ValueError, so the handler around the weights parsing could not catch it and the operator saw a traceback naming neither the command nor what arrived. Four sites across the two files, all four guarded, and the weights handler now also catches TypeError for a non-numeric weight value. Negative weights are left alone: they do not raise, the type is dropped from the mix silently, and changing that changes accepted input. --- .../telemetry/workload/rpc_load_generator.py | 23 +++++++++++++++++- docker/telemetry/workload/tx_submitter.py | 24 ++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/docker/telemetry/workload/rpc_load_generator.py b/docker/telemetry/workload/rpc_load_generator.py index cdd9156e0b..4fecef9fc8 100644 --- a/docker/telemetry/workload/rpc_load_generator.py +++ b/docker/telemetry/workload/rpc_load_generator.py @@ -411,6 +411,14 @@ async def _recv_matching_reply( raise asyncio.TimeoutError(f"{command} (id {request_id}) got no reply") raw = await asyncio.wait_for(conn.ws.recv(), timeout=remaining) reply = json.loads(raw) + # A reply that decodes to an array or a scalar has no .get(), and the + # AttributeError that follows names neither the command nor what + # arrived. Reject it here so the message carries both. + if not isinstance(reply, dict): + raise TypeError( + f"{command} (id {request_id}) got a JSON " + f"{type(reply).__name__} reply, expected an object" + ) # A reply with no id counts as this request's: a few xrpld error paths # answer before the id is parsed, and treating those as stale would # turn a reported error into a timeout. @@ -769,6 +777,19 @@ def main() -> None: if args.weights: try: custom = json.loads(args.weights) + # A JSON array or scalar decodes fine and then has no .items(). + # AttributeError is not a ValueError, so the handler below would not + # catch it: report the type that arrived instead of a traceback. + if not isinstance(custom, dict): + logger.error( + "Invalid --weights: expected a JSON object of command to " + "weight, got a JSON %s: %s", + type(custom).__name__, + args.weights, + ) + sys.exit(1) + # TypeError covers a non-numeric weight value (null, a list, an + # object), which int() rejects with TypeError rather than ValueError. weights = {k: int(v) for k, v in custom.items()} if not weights or sum(weights.values()) <= 0: logger.error( @@ -777,7 +798,7 @@ def main() -> None: ) sys.exit(1) logger.info("Using custom weights: %s", weights) - except (json.JSONDecodeError, ValueError) as exc: + except (json.JSONDecodeError, TypeError, ValueError) as exc: logger.error("Invalid --weights JSON: %s", exc) sys.exit(1) diff --git a/docker/telemetry/workload/tx_submitter.py b/docker/telemetry/workload/tx_submitter.py index 8a987770d3..55b8ac7f21 100644 --- a/docker/telemetry/workload/tx_submitter.py +++ b/docker/telemetry/workload/tx_submitter.py @@ -281,6 +281,7 @@ async def ws_request( Raises: TimeoutError: If no reply carrying this request's ``id`` arrived within RECV_TIMEOUT_S. + TypeError: If a reply decodes to something other than a JSON object. """ request_id = next(_request_ids) request: dict[str, Any] = {"command": command} @@ -311,6 +312,14 @@ async def ws_request( raise TimeoutError(f"{command} (id {request_id}) got no reply") raw = await asyncio.wait_for(ws.recv(), timeout=remaining) resp = json.loads(raw) + # A reply that decodes to an array or a scalar has no .get(), and the + # AttributeError that follows names neither the command nor what + # arrived. Reject it here so the message carries both. + if not isinstance(resp, dict): + raise TypeError( + f"{command} (id {request_id}) got a JSON " + f"{type(resp).__name__} reply, expected an object" + ) reply_id = resp.get("id") if reply_id is None or reply_id == request_id: break @@ -1211,6 +1220,19 @@ def main() -> None: if args.weights: try: custom = json.loads(args.weights) + # A JSON array or scalar decodes fine and then has no .items(). + # AttributeError is not a ValueError, so the handler below would not + # catch it: report the type that arrived instead of a traceback. + if not isinstance(custom, dict): + logger.error( + "Invalid --weights: expected a JSON object of transaction " + "type to weight, got a JSON %s: %s", + type(custom).__name__, + args.weights, + ) + sys.exit(1) + # TypeError covers a non-numeric weight value (null, a list, an + # object), which int() rejects with TypeError rather than ValueError. weights = {k: int(v) for k, v in custom.items()} if not weights or sum(weights.values()) <= 0: logger.error( @@ -1219,7 +1241,7 @@ def main() -> None: ) sys.exit(1) logger.info("Using custom weights: %s", weights) - except (json.JSONDecodeError, ValueError) as exc: + except (json.JSONDecodeError, TypeError, ValueError) as exc: logger.error("Invalid --weights JSON: %s", exc) sys.exit(1)