From 3bf84b10adf18a6f0136c0528d48afe1e1d6df65 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Wed, 12 Aug 2026 17:10:44 -0700 Subject: [PATCH] fix(wtv): resynchronise instead of trusting an implausible element length get_data() reads a 32-byte element header and believes whatever 32-bit length it finds. On one recording the parser walks 182,902 elements correctly and then meets 8800 bytes it cannot interpret; the "length" read there is 0x8A9E4344. That value is passed to skip_sized_buffer(), which hands it to buffered_seek() as an int, turning a forward skip into a 1.8 GB backward seek and a segfault. Reject a length that cannot describe an element (below the 32-byte header, or implausibly large) and hunt forward for the next header instead. Most GUIDs that open a timeline element share the same 15 trailing bytes and differ only in the first, which makes them a dependable anchor; the scan keeps a rolling 32-byte window so the caller ends up positioned exactly as after a normal header read. The end-of-file marker is exempt from the length test. It legitimately carries a zero length and is handled further down, so treating it as garbage ended the parse early and changed the last cue of every WTV recording. On the affected file the parser resynchronises after 8800 bytes -- matching the gap measured independently between the last valid element and the next one -- and carries on to the end of the recording. Verified byte for byte over the 45 local WTV samples, master vs this branch: byte-identical output : 44 differing : 0 crash fixed : 1, cues 100 -> 151 Coverage on that recording goes from 228s to 356s of a 358s file, and the resync fires exactly once across the whole corpus. --- src/lib_ccx/wtv_functions.c | 71 +++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/src/lib_ccx/wtv_functions.c b/src/lib_ccx/wtv_functions.c index 31d20131f..c5c854084 100644 --- a/src/lib_ccx/wtv_functions.c +++ b/src/lib_ccx/wtv_functions.c @@ -132,6 +132,58 @@ void skip_sized_buffer(struct ccx_demuxer *ctx, struct wtv_chunked_buffer *cb, u ctx->past = cb->filepos; } +// Largest element we are willing to believe. Real WTV elements are a few KB; a +// length beyond this means we are no longer looking at an element header. +#define WTV_MAX_ELEMENT_LEN (16 * 1024 * 1024) +// How far to hunt for the next element header before giving up. +#define WTV_RESYNC_LIMIT (4 * 1024 * 1024) + +// The bulk of the GUIDs that start a timeline element share these 15 trailing +// bytes and differ only in the first (data, sync, index, stream1, stream2...), +// which makes them a dependable anchor when re-synchronising. +static const uint8_t wtv_guid_tail[15] = { + 0xC3, 0xD2, 0xC2, 0x7E, 0x9A, 0xDA, 0x11, 0x8B, 0xF7, 0x00, 0x07, 0xE9, 0x5E, 0xAD, 0x8D}; + +static int wtv_len_plausible(uint32_t raw_len) +{ + return raw_len >= 32 && raw_len <= WTV_MAX_ELEMENT_LEN; +} + +static int wtv_is_anchor(const uint8_t *hdr) +{ + uint32_t raw_len; + if (memcmp(hdr + 1, wtv_guid_tail, sizeof(wtv_guid_tail)) != 0) + return 0; + memcpy(&raw_len, hdr + 16, 4); + return wtv_len_plausible(raw_len); +} + +// Walk forward until a plausible element header shows up, keeping a rolling +// 32-byte window so the caller ends up positioned exactly as it would after a +// normal header read. Returns 1 when re-synchronised. +static int wtv_resync(struct ccx_demuxer *ctx, struct wtv_chunked_buffer *cb, uint8_t *hdr) +{ + uint64_t scanned = 0; + + while (scanned < WTV_RESYNC_LIMIT) + { + memmove(hdr, hdr + 8, 24); + get_sized_buffer(ctx, cb, 8); + if (cb->buffer == NULL) + return 0; + memcpy(hdr + 24, cb->buffer, 8); + scanned += 8; + if (wtv_is_anchor(hdr)) + { + mprint("\nWTV: lost element alignment, resynchronised after %llu bytes.\n", + (unsigned long long)scanned); + return 1; + } + } + mprint("\nWTV: lost element alignment and could not resynchronise.\n"); + return 0; +} + // get_sized_buffer will alloc and set a buffer in the passed wtv_chunked_buffer struct // it will handle any meta data chunks that need to be skipped in the file // Will print error messages and return a null buffer on error. @@ -364,23 +416,36 @@ LLONG get_data(struct lib_ccx_ctx *ctx, struct wtv_chunked_buffer *cb, struct de uint32_t stream_id; // Read the 32 bytes containing the GUID and length and stream_id info + uint8_t hdr[32]; + uint32_t raw_len; + get_sized_buffer(ctx->demux_ctx, cb, 32); if (cb->buffer == NULL) return CCX_EOF; + memcpy(hdr, cb->buffer, 32); + + // A length we cannot believe means we are no longer aligned to an element + // boundary -- reading on would hand a bogus size to the seek/skip helpers. + // Hunt for the next header instead of trusting it. The end-of-file marker is + // exempt: it legitimately carries a zero length and is handled further down. + memcpy(&raw_len, hdr + 16, 4); + if (memcmp(hdr, WTV_EOF, 16) != 0 && !wtv_len_plausible(raw_len) && + !wtv_resync(ctx->demux_ctx, cb, hdr)) + return CCX_EOF; - memcpy(&guid, cb->buffer, 16); // Read the GUID + memcpy(&guid, hdr, 16); // Read the GUID for (x = 0; x < 16; x++) dbg_print(CCX_DMT_PARSE, "%02X ", guid[x]); dbg_print(CCX_DMT_PARSE, "\n"); - memcpy(&len, cb->buffer + 16, 4); // Read the length + memcpy(&len, hdr + 16, 4); // Read the length len -= 32; dbg_print(CCX_DMT_PARSE, "len %X\n", len); pad = len % 8 == 0 ? 0 : 8 - (len % 8); // Calculate the padding to add to the length // to get to the next GUID dbg_print(CCX_DMT_PARSE, "pad %X\n", pad); - memcpy(&stream_id, cb->buffer + 20, 4); + memcpy(&stream_id, hdr + 20, 4); stream_id = stream_id & 0x7f; // Read and calculate the stream_id dbg_print(CCX_DMT_PARSE, "stream_id: 0x%X\n", stream_id);