Memory safety — dom/media review guidance
Media input (containers, bitstreams, codec data) is attacker-controlled — treat every value derived from it as untrusted until validated.
General Gecko memory-safety
A confirmed use-after-free / out-of-bounds / overflow is the highest-priority class of finding. The subsections below group the recurring shapes by failure mode; check every changed line against the ones that apply to it.
Bounds, sizes & arithmetic
Integer overflow/underflow on sizes/counts/offsets. Arithmetic that feeds an allocation, index, or pointer offset must be overflow-checked — use
CheckedInt<T>rather than raw+/*on values that can come from input. Watch element-count × element-size (a * b) andend - startunderflow. A fixed-point left-shift (dim << 16) is the same hazard: the accepted input range must exclude the boundary value that overflows the signed result — a guard rejecting only> Nstill admitsN, andN << 16can wrap negative.Checked arithmetic is the operation, not a wrapper on the result.
CheckedInt/SafeMulonly helps if the multiply/add runs in the checked type; wrapping an already-computed native product is too late. Everyunwrap()needs anisOk/isSomeguard, and fixing one means sweeping its siblings.A cast is not a bounds check. Sign and range must be established before a value becomes a length or index — including at every narrowing conversion (
uint64_t->uint32_t/int, a 64->32 truncation of an OS-reported length). An “asserted” cast whose assertion compiles out is a barestatic_castin release. Clamp in the wider type —std::min<Narrow>(a, narrowing_cast(b))truncates before it compares. A value bounded only from above must also be proven>= 0at the conversion.Out-of-bounds access. Every index/length is validated against the actual buffer size before use. Check
memcpy/memmove/memset/memcmplengths, off-by-one (<=vs<), andSpan/nsTArrayviews that must not exceed their backing store.Unit consistency in bounds checks. Both operands of a size comparison must be in the same unit — samples vs. bytes, elements vs. bytes, pixels vs. stride. A check that is “coincidentally correct” for a 1-byte format admits an overrun for a multi-byte one. A picture/crop sub-rect must be proven to fit its frame (
x + w <= frameW), not merely each dimension against an absolute cap.A count validated against a stream limit is not validated against your array. A fixed-size array dimensioned by a format maximum must be indexed by a count checked against that extent (
<vs<=), and every parallel array the index walks must be at least as long. For an inline array in an OS/third-party struct, check whether the API switches to a heap “extended” accessor past capacity.When a size has more than one source, cross-check them. An iteration extent and its backing store that come from different sources (two IPC replies; a metadata field vs. a producer-reported geometry) must be validated against each other wherever both are known — each being self-consistent is not enough. And where the same quantity is readable from two places but only one is authoritative (e.g. a per-sample IV size as both a track default and a sample-group value), reading the wrong one over-reads.
External sizes are untrusted. A length an OS/COM read/stat API reports can over-report — clamp it against the buffer you actually allocated, and handle the empty-buffer case (
&arr[0]at length 0). A shared-memory region is only sealed against resize once it has been frozen read-only, so an unfrozen mapping’s size is not a standing guarantee: revalidate it in each consumer rather than caching it (seeipc.mdfor the trust-boundary framing).A safety limit is only enforced where its initializer actually ran. A global cap (max surface/texture size, a limit config) relied on for bounds enforcement is unset — hence effectively unbounded — in any process that skips the platform-init path that populates it. Confirm that init runs in every process performing the bounded operation, or the guard silently passes everything.
Lifetime, ownership & refcounting
Use-after-free / dangling. A pointer, reference, iterator, or
Spanmust not outlive its backing buffer. Raw pointers that should beRefPtr/WeakPtr/UniquePtr; an object destroyed before a callback/runnable/lambda fires; a lambda capturingthisor a raw ref that outlives the owner.Ownership & refcounting.
already_AddRefedconsumed exactly once; no over-/under-release; RAII rather than manualnew/delete; no leak on an early-return/error path.Releasing a self-reference destroys
this. Clearing the member that may hold the last strong reference makes every later member access and virtual call a UAF. Require it to be the final statement, or a local strong ref covering the rest.An ownership transfer invalidates its source, including on failure. An alias into a smart-pointer-owned object dangles once that owner is moved into a by-value sink, which destroys it on every return. A fallible take/steal is the same: the failure can be what emptied the source, so a fallback that re-reads it must re-validate.
A refcount-leak fix can unmask a use-after-free. A removed redundant
AddRefor addedReleasemay have been the only thing keeping the object alive on some path — enumerate the paths that now free earlier.Destroy-order & opaque-handle validity. An owning resource (device, context, allocator) must outlive teardown of every child that dispatches through it. A
!= NULL_HANDLEcheck does not prove an OS/driver handle still refers to a live object across a teardown boundary — only lifetime/ordering does.Freeing a handle, slot, or id another consumer may still use is a confidentiality bug. The allocator reissues the same number immediately, so a stale consumer reads a different, valid resource — possibly another origin’s. Ask what the stale id now names, not whether it crashes.
Cycle collection & registrations
Reference cycles & cycle collection. For a class using
NS_IMPL_CYCLE_COLLECTION*, every member that can join a cycle or holds an external registration — includingMozPromiserequest/holder members — must appear in bothTraverseandUnlink. A member inTraversebut missing fromUnlinkis a classic leak/UAF.Unlinkand the destructor must each be complete alone. An object can hit refcount zero outside a cycle and never runUnlink, so cleanup living only there (or only inShutdown()) is skipped. ConverselyUnlinkthat unregisters from the GC holder map must null every reflector it stops tracing and clear the data driving lazy re-creation — the object may still be script-reachable.Self-registration with a back-reference. When an object registers itself into a longer-lived manager that holds it by strong
RefPtr, and keeps a raw/weak back-pointer to a shorter-lived owner, confirm it is unregistered — or its back-pointer cancelled — on every path that drops or replaces that reference: destructor, every error/cancel/shutdown path, a reseat that swaps in a new owner, and CCUnlink— not only on a happy-path callback that may never fire.Debug-only enforcement is no enforcement. A destructor or assignment operator that only
MOZ_ASSERTs the disconnect/revoke/unregister already happened leaves the registration live in shipping builds;= defaulton a class holding a registration token is the smell. A null check before dereferencing a back-pointer is worthless unless some teardown path clears it.
Uninitialized, stale & disclosed memory
Uninitialized memory. A field/member read before it is set; a struct passed to an OS/codec API without full initialization. A prepare/fill/convert helper with a fallback or early-out must not return success leaving its output buffer unwritten, and a row copy that writes
widthbut advances bystridemust zero the padding, or it leaks when the plane is read as packed data.A recycled buffer is not a zeroed buffer. Pool/free-list output holds the previous tenant’s bytes, so every readable byte must be written or zeroed — a first allocation on fresh zero pages hides this until reuse. Likewise, normalizing a layout descriptor after a conversion must be backed by having written every byte it now declares valid.
A write that bypasses the constructor must re-establish its invariants. A field set post-construction from an untrusted source must re-prove the class invariant against the real allocation, and every field derived from the old value must be recomputed or invalidated at the same site — a path that re-derives geometry from in-band parameter sets leaves a container-sourced picture/crop rectangle that can now exceed the frame. An accessor that short-circuits on “value unchanged” returns the stale derivative.
An over-read reaching a script-readable sink is information disclosure. Decoded audio, frame pixels, canvas readback, shared textures and copy-out APIs are content-observable, so any declared-but-unproduced extent — over-read tail, unwritten padding, recycled contents — is exfiltrable heap. Size the exposure from what was actually produced; zero-fill or reject the rest.
Where the check goes, and what counts as one
Put the check at the shared sink; the odd sibling out is the defect. When branches (fast/slow, hardware/software, read-directly vs. derived) converge on one copy or allocation, move the validation into that chokepoint rather than the branch you happened to audit. Where parallel implementations of the same hook exist, diff them: the one missing a step its siblings perform is the finding.
Do not infer success from a proxy signal. A non-null data pointer does not mean non-empty (empty containers return a shared static sentinel). A clean error state does not mean success when the callee opens its own nested error scope. A “has X” predicate whose
falseconflates “absent” with “malformed” sends the caller into a branch assuming a parsed object. Check the return value and length.
dom/media specifics
MediaDatafamily & buffers.MediaRawData/AudioData/VideoData,AlignedBuffer,MediaByteBuffer— verify capacity vs. length and never read past the valid range; never mutate a buffer that is shared/immutable; and a buffer handed to another thread/TaskQueue must be kept alive for the whole hand-off (strongRefPtr), or it is a UAF when the producer releases it.Bound a pixel/format-conversion copy by the mapped surface, not a logical size. A CPU copy/convert helper must clamp its source read to the extent of the surface it actually mapped/locked, never to
Image::GetSize()or a configured destination size — a zero-copy/GPU-backed image can legitimately map a surface smaller than its logical size.
Parser / demuxer deep-dive
This section can later be split into its own doc loaded only for high-risk parser directories (
dom/media/mp4/**,dom/media/webm/**, platform demuxers). It is the densest source of media memory-safety bugs.
Box/atom/element sizes are hostile. MP4 box sizes, EBML/WebM element sizes, Matroska lace counts, sample-table (
stsz/stco/stsc) entry counts — never trust the declared size; validate it against the remaining bytes in the parent container before reading or allocating.Bitstream reads. NAL/OBU lengths, LEB128/Exp-Golomb reads, SEI payload sizes — every read must be bounds-checked against the reader’s remaining length; a bit reader must refuse to read past its end rather than wrap.
A length trusted twice. A length validated on read but then re-derived or re-cast before use (signed/unsigned, 64->32) is a common regression — verify the checked value is the one actually used.
Accumulated container values. A value summed across boxes/fragments (e.g. a running fragment decode time) is accumulated in
CheckedIntand the fragment rejected on overflow — never allowed to silently wrap.Signedness of “unsigned” fields. A field the spec declares unsigned can carry a two’s-complement negative in practice (some encoders write pre-roll as
2^64 - N); handle the intended signedness deliberately rather than trusting the raw unsigned value.