Two low-priority follow-ups surfaced while reviewing the cross-worker WebSocket
topics work (#2 / #118). Neither is a correctness, memory-safety, or security
bug — the fuzzer (fuzz_ws_topic, 365k runs clean) and the differential
matcher test (055-topics-matching-oracle.phpt, 144/0) confirm the engine
itself is sound. These are robustness / API-clarity items that can land after
the feature merges.
1. JSON log formatter emits invalid UTF-8 for non-ASCII bytes
Where: src/log/http_log.c:269 (sb_put_json_str), reached for the access
log body at src/log/http_log.c:519.
What: sb_put_json_str escapes control bytes (\u00xx), " and \
correctly, but passes bytes >= 0x80 through verbatim. Request-derived fields
(raw request target, header values, WS topic / close reason) can contain
arbitrary octets, so a client sending e.g. GET /\xff\xfe HTTP/1.1 produces a
"Body":"...\xff..." that is not valid UTF-8 inside a JSON string.
Impact (low): NOT log injection — quotes, backslashes and control bytes
(including \n) are escaped, so a record cannot be forged or split. The only
effect is that a strict JSON log ingester (Elasticsearch, Loki) may reject the
individual record or its batch. Only the json formatter is affected;
plain/pretty already sanitize via sb_put_text_safe (\xXX).
Fix sketch: in the default branch of sb_put_json_str, validate UTF-8
and emit � (or \uXXXX per byte) for invalid sequences, matching what
php_json_escape_string does with PHP_JSON_INVALID_UTF8_SUBSTITUTE.
2. publish() returns only the local delivery count, not cross-worker
Where: src/websocket/php_websocket.c:1240 (RETURN_LONG((zend_long) sent)),
value produced by ws_hub_publish at src/websocket/ws_hub.c:601.
What: ws_hub_publish returns sent, the count delivered to subscribers on
the publisher's own worker only. Remote-worker deliveries are fanned out
asynchronously via the mailbox and are not (and cannot be, synchronously)
counted, so in pool mode publish() under-reports the true number of
recipients.
Impact (low): This is an API/semantics clarity issue, not a bug — delivery
is correct; only the returned tally is partial. subscriberCount() already
does a synchronous cross-worker query and returns the true total, which makes
the asymmetry more surprising.
Fix sketch: decide and document the contract. Either (a) document in
stubs/WebSocket.php that publish() returns the local delivery count only
(cheapest), or (b) return the number of workers the message was fanned out to,
or (c) leave delivery async but expose a separate synchronous count. (a) is the
least invasive.
Found during the review in #118; tests for the feature are already merged into
the branch.
Two low-priority follow-ups surfaced while reviewing the cross-worker WebSocket
topics work (#2 / #118). Neither is a correctness, memory-safety, or security
bug — the fuzzer (
fuzz_ws_topic, 365k runs clean) and the differentialmatcher test (
055-topics-matching-oracle.phpt, 144/0) confirm the engineitself is sound. These are robustness / API-clarity items that can land after
the feature merges.
1. JSON log formatter emits invalid UTF-8 for non-ASCII bytes
Where:
src/log/http_log.c:269(sb_put_json_str), reached for the accesslog body at
src/log/http_log.c:519.What:
sb_put_json_strescapes control bytes (\u00xx),"and\correctly, but passes bytes >= 0x80 through verbatim. Request-derived fields
(raw request target, header values, WS topic / close reason) can contain
arbitrary octets, so a client sending e.g.
GET /\xff\xfe HTTP/1.1produces a"Body":"...\xff..."that is not valid UTF-8 inside a JSON string.Impact (low): NOT log injection — quotes, backslashes and control bytes
(including
\n) are escaped, so a record cannot be forged or split. The onlyeffect is that a strict JSON log ingester (Elasticsearch, Loki) may reject the
individual record or its batch. Only the
jsonformatter is affected;plain/pretty already sanitize via
sb_put_text_safe(\xXX).Fix sketch: in the
defaultbranch ofsb_put_json_str, validate UTF-8and emit
�(or\uXXXXper byte) for invalid sequences, matching whatphp_json_escape_stringdoes withPHP_JSON_INVALID_UTF8_SUBSTITUTE.2.
publish()returns only the local delivery count, not cross-workerWhere:
src/websocket/php_websocket.c:1240(RETURN_LONG((zend_long) sent)),value produced by
ws_hub_publishatsrc/websocket/ws_hub.c:601.What:
ws_hub_publishreturnssent, the count delivered to subscribers onthe publisher's own worker only. Remote-worker deliveries are fanned out
asynchronously via the mailbox and are not (and cannot be, synchronously)
counted, so in pool mode
publish()under-reports the true number ofrecipients.
Impact (low): This is an API/semantics clarity issue, not a bug — delivery
is correct; only the returned tally is partial.
subscriberCount()alreadydoes a synchronous cross-worker query and returns the true total, which makes
the asymmetry more surprising.
Fix sketch: decide and document the contract. Either (a) document in
stubs/WebSocket.phpthatpublish()returns the local delivery count only(cheapest), or (b) return the number of workers the message was fanned out to,
or (c) leave delivery async but expose a separate synchronous count. (a) is the
least invasive.
Found during the review in #118; tests for the feature are already merged into
the branch.