Fix unterminated emsg infinite loop and scheme parsing - #7968
Conversation
parseEmsg walked scheme_id_uri and value with bin2str(data.subarray(offset, offset + 1)). Past the end of the box that subarray is empty and bin2str returns '', which never equals '\0', so the loop had no exit and spun forever incrementing offset. findBox clamps a box to the bytes actually present, so a segment truncated mid-box reaches this, as does a packager that omits the terminator. The demux path has no config gate and runs in the transmuxer worker by default, so the worker wedges with no error raised. Read both strings with a bounded helper instead. The version 0 branch also never skipped the FullBox version and flags, so it read scheme_id_uri from the version byte and always produced "\0", then took timescale, presentation_time_delta, event_duration and id from hardcoded offsets that fall inside the URI text. Start it at offset 4 and read the fields in order, and leave presentationTime undefined so getEmsgStartTime uses the delta.
|
Thanks for the contribution @Jaybhade. If you have the time, could you add a test to cover a regression for a properly terminated scheme followed by an improper unterminated value, but the buffer length is matches so readCString ends properly. I'm open to your thoughts on what the behaviour should be. But as it's technically not a valid If you know what the behaviour was on |
Both versions end the string at the end of the box, so the parser returns the trailing bytes as the value and an empty payload. On master the v1 case never returned at all.
A string that runs off the end of the box is not a string, and a partial scheme id uri can still match the ID3 or KLV scheme test, which would push a metadata sample built from a box that never fully arrived. Both strings now read as empty in that case and the rest of the box is consumed, so version 1 keeps the fields that precede the strings and version 0, whose fields all follow them, keeps nothing.
|
Thanks for looking at it. Test added, and I agree with your read — I've pushed both, as two commits so the behaviour change reverts on its own if you'd rather keep it out. f2c907a adds the regression you asked for, for both versions: a properly terminated What
What I think it should be, and what's now in the branch. Your instinct matches mine, so 7be40ac implements it:
Before that commit the parser handed back the trailing bytes as a truncated string, and the concrete reason I'd argue against that: a partial One related thing worth your call: when only the value is truncated, v1 still returns a valid scheme with an empty payload, so
|
|
On the truncated value, a warning log and pushing the empty value so as to be sure to not leak any corrupted data from the improper ID3 makes sense to me. The warn will let developers know their stream is broken. |
|
Applied milestone is dependent on issue confirmation and review approval. Added it so we don't let such an escape slip. |
|
It would be good to know, as part of the issue description/analysis, if this issue exists in 1.6.x or if it is a recent regression in master. |
An unterminated string means the stream is broken, so say so rather than silently dropping the value and the payload.
|
@itsjamie Warn added in ff7684e, on both versions, naming which string ran off the end and what got dropped: The empty value was already what the branch pushed; this makes it audible. The two existing unterminated-value tests now assert the warn fires exactly once — and they're load-bearing rather than decorative: with just the two @robwalch It exists in 1.6.x — it is not a master regression. It has been there since emsg ID3 support was added in a5c669a (Oct 2021), first shipped in v1.2.0, and the two loops are byte-for-byte identical from that commit through v1.6.17. Measured rather than read, against the published bundles: I lifted
So every supported line hangs on this input. Worth noting for severity: The second defect in the PR — v0 never doing Gates after ff7684e: type-check, eslint, prettier, 1163/1163 karma unit tests. |
This PR will...
Stop
parseEmsg()from looping forever when anemsgbox ends before the null terminator of itsscheme_id_uriorvalue, and read the version 0 layout from the correct offset.Why is this Pull Request needed?
1. A truncated or non-conformant
emsgbox hangs the demuxer permanentlyBoth string fields are walked a byte at a time:
Once
offsetreaches the end of the box,data.subarray(offset, offset + 1)is an emptyUint8Array, sobin2str()returns''— which never equals'\0'. The loop has no other exit and spins forever incrementingoffset.findBox()clamps a box to the bytes that are actually present (const boxEnd = Math.min(endbox, end)), so a segment whose delivery stops mid-box produces exactly this input; so does a packager that writes an unterminated string.parseEmsg()runs fromMP4Demuxer.extractID3Track()on everyemsgbox in an fMP4 segment, with no config gate.enableWorkerdefaults totrue, so the transmuxer worker wedges at 100% CPU — playback stops, noERRORevent is emitted, and there is no state to recover from.Three inputs that never return on
master:masterschemeIdUri: ''scheme_id_urihas no terminatorschemeIdUri: 'urn:a'emsgclamped byfindBox()to the bytes receivedschemeIdUri: ''Worth noting for comparison:
mux.js's equivalent loop indexes withdata[index], so past the end it readsundefined,String.fromCharCode(undefined)is'\0', and the loop terminates. Reading a byte viasubarray()instead is what removes that exit.2. Version 0 has never parsed
The version 1 branch steps over the FullBox version and flags with
offset += 4; the version 0 branch does not, so it starts readingscheme_id_uriat the version byte. That byte is0, so the string terminates immediately andschemeIdUriis always"\0". It then readstimescale,presentation_time_delta,event_durationandidfrom hardcoded offsets 12/16/20/24, which land inside the URI text for any real box.A well-formed v0 box carrying
https://aomedia.org/emsg/ID3,VALUE=1, timescale 90000, delta 4500, duration 180000, id 7:Because
"\0"matches neitheremsgSchemePatternnorconfig.emsgKLVSchemaUri, every version 0emsgbox is dropped downstream today, so correcting it cannot regress a stream that currently works.Are there any points in the code the reviewer needs to double check?
readCString()returns the terminator as part of the string so callers advance by.length. That preserves the previous output:schemeIdUriandvaluestill carry their trailing\0.presentationTimeis now leftundefinedfor version 0 rather than defaulting to0.getEmsgStartTime()branches onNumber.isFinite(presentationTime), so a0default would time every version 0 event at 0 instead oftimeOffset + presentationTimeDelta / timeScale. The field is already optional onIEmsgParsingData.mux.json every box it accepts.parseEmsg— that is the bug they cover.Resolves issues:
None open — found while reading the fMP4 in-band metadata path.
Checklist