Skip to content

Commit 36b2f28

Browse files
kriszypclaude
andcommitted
fix: skip timestamp-prefix heuristic for audit values (classic-record collision)
When typed random-access structures are off, records use classic shared structures whose first byte can be 66 (0x42 = structure-id #2). RecordEncoder's rocksdb metadata heuristic treats a leading 66 as a local-timestamp prefix and strips 8 bytes, corrupting the record (decoded as null). The audit store's getValue decodes a value that carries no on-disk timestamp prefix, so it now passes { noMetadata: true } to skip the heuristic entirely. Surfaced by the MQTT "can publish non-JSON" path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent e9786f6 commit 36b2f28

2 files changed

Lines changed: 12 additions & 2 deletions

File tree

resources/RecordEncoder.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,14 @@ export class RecordEncoder extends StructonEncoder {
262262
let nextByte = buffer[start];
263263
let metadataFlags = 0;
264264
try {
265-
if ((this.isRocksDB && nextByte === 66) || (nextByte < 32 && end > 2)) {
265+
// The metadata/timestamp prefix is detected heuristically by the first byte. For rocksdb a
266+
// local-timestamp prefix starts with 66 — but 66 (0x42) is also classic shared-structure
267+
// record-id #2, so a timestamp-less classic record beginning with that id is misread as a
268+
// timestamped record (8 bytes stripped → corrupt). Callers that pass a value known to have no
269+
// prefix (e.g. the audit store's getValue) set options.noMetadata to skip the heuristic. Typed
270+
// structs start at 0x20-0x3f and never hit this, which is why it only surfaces with classic
271+
// structures (typed structures off).
272+
if (!options?.noMetadata && ((this.isRocksDB && nextByte === 66) || (nextByte < 32 && end > 2))) {
266273
// record with metadata
267274
// this means that the record starts with a local timestamp (that was assigned by lmdb-js).
268275
// we copy it so we can decode it as float-64; we need to do it first because if structural data

resources/auditStore.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,10 @@ export function readAuditEntry(buffer: Uint8Array, start = 0, end = undefined):
563563
if (action & HAS_RECORD || (action & HAS_PARTIAL_RECORD && !fullRecord)) {
564564
if (!value) {
565565
value = decodeFromDatabase(
566-
() => store.decoder.decode(buffer.subarray(decoder.position, end)),
566+
// the audit value has no on-disk timestamp/metadata prefix (the audit entry carries
567+
// its own time), so skip the prefix heuristic — otherwise a classic record whose
568+
// structure-id byte is 66 (0x42) is misread as a rocksdb timestamp. See RecordEncoder.decode.
569+
() => store.decoder.decode(buffer.subarray(decoder.position, end), { noMetadata: true }),
567570
store.rootStore
568571
);
569572
}

0 commit comments

Comments
 (0)