Skip to content

Post-#118 follow-ups: JSON log invalid-UTF-8 + publish() return semantics (#2) #130

Description

@EdmondDantes

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.

Activity

  1. added
    bugSomething isn't working
    documentationImprovements or additions to documentation
    on Jul 14, 2026
  2. self-assigned this
    on Jul 14, 2026
  3. added a commit that references this issue on Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdocumentationImprovements or additions to documentation

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions