Problem/Motivation
Dual-venue record for GitHub #7. Found while verifying the fix for #3613960 against a real 2,002-row chain.
json_encode() returns FALSE on a single malformed UTF-8 byte. Both call sites cast that straight to a string:
AuditChainLogger::encodeMetadata()—$json = (string) json_encode($metadata);AuditChainLogger::buildCanonical()—return (string) json_encode($payload, …);
(string) FALSE is ''. So the stored metadata column becomes empty and the canonical payload the hash covers becomes empty. The row is hashed over nothing.
Steps to reproduce
Demonstrated on PHP 8.4:
json_encode(invalid utf8) = false (last_error=5 Malformed UTF-8 characters) (string) cast of that = '' (length 0) canonical with current flags = '' (length 0)
Why this is worse than an unsigned row
An unsigned row is intact and readable; it just is not signed. A row hit by this is destroyed: its metadata is gone rather than unreadable, it can never verify again under any key because the content it was hashed over does not exist, and nothing warns — no error, no status-report entry.
On the real chain, five of 2,002 rows are in this state (ids 307, 545, 640, 695, 830) — all entity_save on nodes, all with metadata of length 0. A truncated multibyte character in one field value was enough. They are unrecoverable.
Proposed resolution
Use JSON_INVALID_UTF8_SUBSTITUTE for both the canonical payload and the stored metadata, and stop casting a FALSE return to a string.
Adding the flag is safe for existing rows, which was checked rather than assumed: a payload that is already valid UTF-8 encodes to exactly the same bytes with or without it, so no historical hash changes. Only payloads that previously failed outright are affected, and those did not verify to begin with.
Remaining tasks
Fixed on the 1.x branch. Rows already written this way cannot be repaired — their content is gone. Worth calling out in release notes, since any site with entity content containing malformed bytes will have some, and the only way to identify them is the signature above (empty metadata plus verifying under no key).
Comments
Comment #2
jmcerdaFixed on the 1.x branch. The canonical payload and stored metadata now use `JSON_INVALID_UTF8_SUBSTITUTE`, so a malformed byte no longer collapses a row to an empty canonical.
Verified before merge that this cannot invalidate history: a payload that is already valid UTF-8 encodes to byte-identical output with and without the flag, so no historical hash changes. Only payloads that previously failed outright are affected, and those did not verify to begin with.
A kernel test asserts that a row carrying invalid UTF-8 both stores non-empty metadata and verifies.
**The five rows already destroyed on the production chain (307, 545, 640, 695, 830) remain unrecoverable** — their content was never stored, so there is nothing to repair. Recording the boundary is covered by the sealing design in [3614137](https://www.drupal.org/project/audit_chain/issues/3614137) rather than here.
Worth a release-notes mention: any site whose entity content contains malformed bytes will have some, and the only way to identify them is the signature above — empty metadata plus verifying under no key at all.
Comment #4
jmcerdaFixed in 1.1.0 (GitHub #7 / PR #6). Canonical payload and stored metadata use JSON_INVALID_UTF8_SUBSTITUTE.
Comment #5
jmcerdaShipped in 1.1.0. Closing after release.