JSON output (-J) emits two top-level "error" keys on server-relayed errors
Context
Version of iperf3: 3.21 (cJSON 1.7.15), git
d39cf41526626b4e5a130f115d931cd6cbdffc19. The relevant code paths are unchanged on currentmaster, so earlier 3.x releases are very likely affected as well.Hardware: x86_64 (immaterial — this is in the JSON-assembly logic, not platform-specific).
Operating system (and distribution, if any): Linux. Platform-independent.
Bug Report
- Expected Behavior
With -J, an error that is relayed from the server should produce a single top-level "error" member in the JSON document.
- Actual Behavior
When the client receives a SERVER_ERROR control message, the -J document contains two "error" members in the top-level object — one with a SERVER ERROR - prefix, one without:
"intervals": [],
"end": {
},
"error": "SERVER ERROR - client's requested duration exceeds the server's maximum permitted limit",
"error": "client's requested duration exceeds the server's maximum permitted limit"RFC 8259 §4 permits duplicate names but states that in that case "the behavior of software that receives such an object is unpredictable." In practice the result is parser-dependent: last-wins parsers (Python json, JS JSON.parse, Go, Rust serde_json) surface only the bare message and silently drop the SERVER ERROR - prefix; a first-wins reader (including cJSON's own cJSON_GetObjectItem) keeps the prefixed one. A consumer can't reliably tell which "error" string it will get.
- Steps to Reproduce
# terminal 1 — server that refuses tests longer than 2s
iperf3 -s -1 --server-max-duration 2 -p 5301
# terminal 2 — client requests a 10s test, which exceeds the limit
iperf3 -c 127.0.0.1 -p 5301 -t 10 -JThe server relays SERVER_ERROR (IEMAXSERVERTESTDURATIONEXCEEDED) and the client's -J document ends with the two "error" members shown above.
- Root Cause
Two independent code paths each add "error" to the same test->json_top, and cJSON_AddStringToObject() appends a node rather than replacing an existing key:
src/iperf_client_api.c:406— theSERVER_ERRORcase iniperf_handle_message_client()callsiperf_err(test, "SERVER ERROR - %s", iperf_strerror(i_errno)).iperf_err()writes the first"error"atsrc/iperf_error.c:61, then the handler returns -1.- The -1 unwinds into
cleanup_and_fail(), which atsrc/iperf_client_api.c:895adds"error"a second time (the bareiperf_strerror(i_errno)) before callingiperf_json_finish().
Neither path is aware the other has already annotated the object.
- Suggested Fix
Make the second write idempotent — e.g. in cleanup_and_fail() use cJSON_ReplaceItemInObjectCaseSensitive() (or check cJSON_GetObjectItem(test->json_top, "error") before adding), or have the SERVER_ERROR handler report via a non-JSON-emitting path so only cleanup_and_fail() writes the key. Either yields a single, unambiguous "error" member.
Source: esnet/iperf