c96fd4426c7f5d7f9ee849ec27f5c6b053ec26fe [CrOS Camera] Validate type and count in GetMetadataEntryAsSpan
Triage note: GetMetadataEntryAsSpan<T> now verifies type==entry_type_of<T> and count*sizeof(T)==data.size() with CheckedNumeric before the reinterpret_cast; RequestManager stops reading int32_t from a 1-byte TYPE_BYTE allocation. Prevents OOB heap reads on malformed camera metadata; tests cover over-range count and type mismatch.
The bug at a glance
Out-of-bounds heap read in the ChromeOS camera capture stack. CameraMetadataEntry.count and .type arrive from the camera HAL over Mojo and are trusted independently of the actual data byte-vector length; a mismatched count lets Chrome reinterpret a small heap allocation as a larger typed span and walk off the end. Reachable from web content via getUserMedia() camera access on CrOS. Info-leak primitive; High.
GetMetadataEntryAsSpan() reinterpreted a Mojo byte buffer as a typed T array using only data.size() % sizeof(T)==0 as a sanity check (via CHECK_EQ), while trusting the separately-transmitted CameraMetadataEntry.count. Because count and the data vector are independent fields, a malformed/compromised CameraModule can set count far larger than the bytes present, and callers that iterate count*tuple elements read past the heap allocation.
Root cause
GetMetadataEntryAsSpan previously only asserted data.size() % sizeof(T) == 0 and then returned a span over reinterpret_cast<T*>(data.data()) without checking (*entry)->type or (*entry)->count. Callers such as CameraDeviceDelegate::GetStreamResolutions and RequestManager trusted CameraMetadataEntry.count to decide how many tuples to iterate, but count is an independent Mojo field, so a HAL-supplied count larger than data.size()/sizeof(T) produced an OOB heap read. The fix adds a base::CheckedNumeric<size_t> expected_bytes = count * sizeof(T) and rejects the entry (returns {}) unless (entry)->type == entry_type_of::value and expected_bytes.IsValid() and expected_bytes.ValueOrDie() == data.size(). request_manager.cc is converted from a raw reinterpret_cast<int32_t> to GetMetadataEntryAsSpan<int32_t>() guarded by partial_count.size()==1.
Key code
camera_metadata_utils.h: bind declared count/type to the actual byte-vector length before reinterpreting
base::CheckedNumeric<size_t> expected_bytes = (*entry)->count;
expected_bytes *= sizeof(T);
if ((*entry)->type != entry_type_of<std::remove_const_t<T>>::value ||
!expected_bytes.IsValid() ||
expected_bytes.ValueOrDie() != (*entry)->data.size()) {
LOG(WARNING) << "Invalid encoding for tag " << static_cast<int>(tag);
return {};
}
Patch walkthrough
media/capture/video/chromeos/camera_metadata_utils.h— Core fix. Replaces the single CHECK_EQ(data.size() % sizeof(T), 0u) with a checked-arithmetic validation: count*sizeof(T) must not overflow and must exactly equal data.size(), and (*entry)->type must equal entry_type_of::value, else return an empty span.media/capture/video/chromeos/camera_metadata_utils.cc— Adds the entry_type_of specialization mapping char to TYPE_BYTE so char-typed callers of the now type-checked helper validate correctly.media/capture/video/chromeos/request_manager.cc— Replaces the raw reinterpret_cast<int32_t*>((*partial_count)->data.data()) read of ANDROID_REQUEST_PARTIAL_RESULT_COUNT with GetMetadataEntryAsSpan<int32_t>() gated on partial_count.size()==1, so a 1-byte TYPE_BYTE entry can no longer be dereferenced as an int32_t.media/capture/video/chromeos/camera_device_delegate_unittest.cc— Adds a malformed_stream_configuration_count_ flag that inflates entry->count by 4 beyond the byte vector, plus OutOfRangeStreamConfigurationCount test expecting an empty result when count mismatches.media/capture/video/chromeos/request_manager_unittest.cc— Adds MalformedPartialResultCountMetadata: sets the partial-result-count entry to TYPE_BYTE, count=1, data={1} and confirms no int32_t is read out of the one-byte allocation.
Background
CameraMetadataEntry (Mojo) — cros::mojom camera metadata entry carrying tag, type (EntryType), count, and a data byte vector. type and count are transmitted independently of data.size(), so they must be validated against the buffer before use.
GetMetadataEntryAsSpan — Templated helper that reinterpret_casts the entry’s data byte vector into a base::span. It is the shared choke point every typed metadata read flows through.
entry_type_of — Trait mapping a C++ type to its expected cros::mojom::EntryType. The fix uses it to reject entries whose declared type does not match T.
ANDROID_SCALER_AVAILABLE_STREAM_CONFIGURATIONS / ANDROID_REQUEST_PARTIAL_RESULT_COUNT — Android camera metadata tags parsed by GetStreamResolutions and RequestManager; both were read using HAL-controlled count without bounds binding.
Vulnerability window
- Pre-patch — GetMetadataEntryAsSpan trusted entry->type/count; GetStreamResolutions and RequestManager iterated count*tuple elements or cast raw data pointers, allowing OOB heap reads on malformed metadata.
- Fix 2026-09-29 — Lands checked count*sizeof(T)==data.size() and type validation (CL 8440916), TEST under cros_asan.
- Validation — capture_unittests gains OutOfRangeStreamConfigurationCount and MalformedPartialResultCountMetadata regression tests under ASan.
Proof of concept
The diff ships concrete triggering tests. OutOfRangeStreamConfigurationCount sets count 4 higher than the stored byte vector and asserts GetStreamResolutions no longer walks past it; MalformedPartialResultCountMetadata declares the partial-result-count entry as TYPE_BYTE/count=1/data={1}. Web reachability (inferred for the full chain): a page calls getUserMedia() to open a CrOS camera; the OOB read is driven by the CameraModule/HAL-supplied metadata, so weaponization requires a malicious/compromised camera HAL rather than pure JS, but the parse is reached by ordinary camera-open from web content.
// camera_device_delegate_unittest.cc: inflate count past the byte vector
if (malformed_stream_configuration_count_) {
entry->count += 4; // claim one more four-int32 tuple than present
}
// request_manager_unittest.cc: int32 read from a 1-byte allocation
partial_count->type = cros::mojom::EntryType::TYPE_BYTE;
partial_count->count = 1;
partial_count->data = std::vector<uint8_t>{1};
Exploitation
- Trigger — A malformed CameraMetadataEntry (count or type inconsistent with data.size()) is delivered through the camera HAL/Mojo path during characteristics parsing; reached when web content opens the camera via getUserMedia (inferred for the end-to-end chain).
- OOB read — GetStreamResolutions iterates count*4 int32 values past the real allocation, or RequestManager reads a 4-byte int32 from a 1-byte buffer, leaking adjacent heap bytes (inferred as an info-leak primitive).
- Escalation — Leaked heap contents could seed a pointer/length disclosure feeding a later corruption primitive (inferred; not demonstrated by the patch).
Detection & hunting
For defenders and detection engineers:
- reinterpret_cast of a Mojo byte vector to a typed pointer/span — Flag any reinterpret_cast<T*>(entry->data.data()) or span construction using an entry->count not validated against data.size(); the fixed pattern computes CheckedNumeric count*sizeof(T) and compares to data.size().
- CHECK_EQ(data.size() % sizeof(T), 0u) as the only guard — A modulo-only check permits count > data.size()/sizeof(T). Treat it as insufficient wherever a separately-supplied element count drives iteration.
- ASan heap-buffer-overflow READ in media/capture/video/chromeos — capture_unittests under cros_asan should catch regressions; watch for OOB reads in GetStreamResolutions / RequestManager::SetUpStreamsAndBuffers.
Audit directions
- Other GetMetadataEntryAsSpan / GetMetadataEntry callers — Enumerate every caller that reads count or casts data.data(); confirm each now relies on the validated span rather than raw count.
- Mojo structs with independent length + buffer fields — Audit other mojom types where a count/type field is transmitted separately from a byte vector; the same trust-the-count bug class applies.
- entry_type_of coverage — Verify every T used with GetMetadataEntryAsSpan has an entry_type_of specialization (the patch added char).