Skip to content

VPAAMP-1542: Heap overflow (read and write) in ClearKey decrypt via untrusted CENC subsample lengths - #321

Open
pstroffolino wants to merge 27 commits into
mainfrom
feature/VPAAMP-1542
Open

pstroffolino wants to merge 27 commits into
mainfrom
feature/VPAAMP-1542

Conversation

@pstroffolino

Copy link
Copy Markdown

Security protection for a malicious ClearKey DASH stream

jfagunde and others added 27 commits August 3, 2026 13:48
Reason for Change: Changes required for DirectRialto

Test Procedure: Build and run AAMP with DirectRialto enabled

Priority: P1

Risks: Medium
Reason for Change: Fix AAMP compilation failure after rebase

Summary of Changes:
- Restore the file DrmSession.cpp
- Revert some of the changes made to DrmSession.h
- Restore fakes, CMake files and references to DrmSession.cpp

Test Procedure: Build AAMP-CLI and AAMP L1 tests

Priority: P1

Risks: Low
IsKeyIdProcessed() only held cachedKeyMutex, so it could observe a slot
another thread had just claimed (before createDrmSession() finished
acquiring the license) and report it as already succeeded. It also only
checked the per-entry isFailedKeyId flag, missing whole-slot failures
tracked in isFailedKeyEntries.

Now also takes mDrmSessionLock (same order createDrmSession() already
uses) so it blocks until any in-flight session creation for that key
has resolved, and checks isFailedKeyEntries too.

Add regression tests to DrmSessionManagerTestCases.cpp.
True when NAL units are length-prefixed (AVCC/HVCC, e.g. avcC/hvcC
sample entries per ISO/IEC 14496-15); false for Annex-B (start-code
delimited) bitstreams such as HLS-TS ES output. Meaningless for
non-NAL-unit codecs (audio/subtitle), where it stays false.
PlayerCCManagerBase::GetId()/Release() implements a usage-counter
pattern that is correct for PlayerSubtecCCManager's shared,
out-of-process connection, but was reused as-is for the Rialto and
direct-Rialto backends, where the "handle" is actually a per-pipeline
object owned by a single playback session. In multi-pipeline mode this
can leave a dangling handle in the singleton after its owning session
is destroyed, since the refcount only tracks session count, not the
validity of a specific handle.

- Add PlayerCCManagerBase::InvalidateHandle(void*), a no-op by default,
  that a handle owner can call to clear its handle from the singleton
  ahead of Release(), independent of the GetId()/Release() count.
- Add PlayerCCManager::HasInstance() so callers can check for an
  existing singleton without creating one.
- Override InvalidateHandle() in PlayerDirectRialtoCCManager and
  PlayerRialtoCCManager to clear m_control/mSubtitleControlHandle only
  when it matches the given handle.

This is a minimal mitigation (Option A); wiring InvalidateHandle() into
PlayerRialtoCCManager's GStreamer-side handle owner, and a larger
structural fix (shared preferences singleton + per-session state), are
tracked separately as follow-up work.
InvalidateHandle() relied on an atomic compare_exchange to clear the
control/handle pointer, but the atomic only protects the pointer value,
not the lifetime of the object it points to. A concurrent SetTrack() /
StartRendering() / StopRendering() call could load the pointer before
the CAS and dereference it after the owner destroyed the object.

- PlayerDirectRialtoCCManager: replace std::atomic<IDirectRialtoCC*>
  with a mutex (m_controlMutex) shared by InvalidateHandle() and all
  accessors, so invalidation blocks until any in-flight call on the old
  handle completes.
- PlayerRialtoCCManager: apply the same mutex-guard fix for
  mSubtitleControlHandle.
- PlayerDirectRialtoCCManager::SetTrack(): return the actual result of
  setTextTrackIdentifier() (0/-1) instead of always returning 0, per
  the base class contract.
- Add SetTrack_ControlRejectsIdentifier_ReturnsFailure L1 test covering
  the failure path.

Testing: PlayerDirectRialtoCCManagerTests (10/10) pass.
…nCDM sessions

DrmSessionManager::getDrmSession() called setKeyId() only inside
already made unconditional. Non-OpenCDM backends (e.g. Direct-Rialto)
never received the key ID. Move the call inside the existing
drmSession != NULL check and drop the guard; setKeyId() defaults to
a no-op, so calling it unconditionally is safe for every backend.
Remove the matching stale #ifdef around the setKeyId mock in
MockDrmSession.h so it stays in sync with the base class.

PlayerDirectRialtoCCManager::Initialize() discarded the return value
of SetTrack() for both the default "CC1" track and the cached-track
re-apply path, always returning 0 even when the control rejected the
identifier. Propagate the failure as -1 in both cases so
PlayerCCManagerBase::Init() correctly reports failure.

Add Initialize_ControlRejectsDefaultTrack_ReturnsFailure and
Initialize_ControlRejectsCachedTrack_ReturnsFailure to cover the new
failure paths.
drm/DrmSessionManager.h and closedcaptions/PlayerCCManager.h added a
default argument to an existing constructor/static method
(DrmSessionManager's 4th param, SetRialto's 2nd param). Since both are
installed public headers, this silently changed the mangled symbol,
breaking binaries linked against the previous release. Restore the
old symbols by keeping a distinct overload that delegates to the new
one:
  - DrmSessionManager(int, void*, callback) delegates to
    DrmSessionManager(int, void*, callback, creator).
  - PlayerCCManager::SetRialto(bool) delegates to
    SetRialto(bool, bool).

PlayerDirectRialtoCCManager::Initialize() left m_control pointing at
the new handle even when SetTrack() failed to apply the track. A
later Initialize() retry with the same handle then saw
changedHandle == false and returned success without ever configuring
the control. Clear the control via a new clearControlOnFailure()
helper whenever SetTrack() fails during Initialize(), so a retry is
treated as a handle change and reattempts SetTrack().

mTrack/mTrackFormat were written in SetTrack() outside m_controlMutex
and read/cleared in Initialize()/ResetState() without any lock. Move
all three accesses under m_controlMutex; Initialize() takes a local
snapshot before releasing the lock to avoid recursive-lock deadlock
when it subsequently calls SetTrack().

Add Initialize_RetryAfterRejectedDefaultTrack_ReappliesTrack to cover
the retry fix.
PlayerDirectRialtoCCManager:
- Fix deadlock in ResetState(): PlayerCCManagerBase::ResetState() calls
  Stop() -> StopRendering(), which re-enters m_controlMutex. Run the
  base call before acquiring the lock instead of wrapping the whole
  function, matching the original (pre-mutex) call order.
- Update m_controlMutex's doc comment to reflect that ResetState()
  clears mTrack/mTrackFormat via the base class before taking the lock.
- Strengthen the test double: PlayerCCManagerBase::ResetState() in
  PlayerDirectRialtoCCManagerBaseStubs.cpp now calls Stop(), mirroring
  the real base class, so ResetState_ClearsControlHandle actually
  exercises the mutex re-entrancy this fix avoids instead of masking it.

DrmSessionManager:
- Revert the 3-arg constructor overload added to preserve the old
  mangled symbol. m_sessionCreator already changed this class's size,
  so a prebuilt binary compiled before that member existed would still
  link successfully against the restored 3-arg symbol, then heap-
  corrupt: operator new sizes the allocation from the caller's old
  (smaller) layout, while the constructor initializes an object sized
  for the new (larger) one. Collapsing back to a single constructor
  with creator defaulted to nullptr forces such callers to fail at
  compile time instead of silently corrupting memory at runtime.

Verified: PlayerDirectRialtoCCManagerTests (13/13), DrmSessionManagerTests
(29/29), DrmAes (33/34, 1 pre-existing skip), DrmHelperTests (160/160),
DrmOcdmTests (96/96).
- PlayerCCManager.h: move InvalidateHandle() to the end of
  PlayerCCManagerBase's virtual interface. It was previously declared
  between Release() and SetStatus(), which shifts every subsequent
  vtable slot and silently mis-dispatches calls from binaries built
  against an older header.

- PlayerCCManager.h/.cpp: split ResetState() into Stop() + a new
  ResetTrackState() helper that only resets the cached track/rendering
  fields, without acquiring any lock itself. PlayerDirectRialtoCCManager
  and PlayerRialtoCCManager's ResetState() now call Stop() unlocked
  (still required to avoid deadlocking through StopRendering()'s
  re-entrant lock), then call ResetTrackState() and clear their handle
  while holding their own control mutex, instead of invoking
  PlayerCCManagerBase::ResetState() directly - removing a race between
  ResetState() and concurrent SetTrack()/Initialize() calls on those
  fields. Updated PlayerDirectRialtoCCManagerBaseStubs.cpp with a
  matching ResetTrackState() stub.

- PlayerRialtoCCManager.cpp/.h: move mTrack/mTrackFormat writes inside
  mControlMutex in SetTrack(), and snapshot them under the same lock as
  mSubtitleControlHandle in Initialize(), removing a data race with
  concurrent SetTrack()/Initialize() calls. Mirrors the fix already
  applied to PlayerDirectRialtoCCManager. Updated the mControlMutex doc
  comment to reflect the additional state it now guards.

- PlayerDirectRialtoCCManager.cpp: only update the cached mTrack/
  mTrackFormat once setTextTrackIdentifier() confirms success. A
  rejected track previously still updated the cache, so a later
  Initialize() with the same handle would see changedHandle == false
  and skip reapplying the last-accepted track. Added
  SetTrack_ControlRejectsIdentifier_CacheKeepsPreviousTrack to cover it.

- DrmSessionManagerTestCases.cpp: add coverage for the DrmSessionCreator
  injection path in getDrmSession(), which previously had no test
  exercising it. Extend TestableDrmSessionManager with a constructor
  overload forwarding a DrmSessionCreator, and add tests verifying (a)
  the creator is invoked and its returned session's ownership is
  transferred into the selected slot, and (b) a null creator result is
  reported as MW_DRM_INIT_FAILED.
PlayerCCManager::SetRialto() had a two-argument version plus a
one-argument overload kept only to preserve a "pre-existing mangled
symbol for binaries linked against older releases." That reasoning
doesn't hold: like DrmSessionManager's DrmSessionCreator parameter,
this library is always rebuilt from source with its consumers, so
there's no stable ABI symbol to preserve. Replace the two overloads
with a single SetRialto(bool bIsRialto, bool bIsDirectRialto = false),
matching the pattern already used for DrmSessionManager. Existing
single-argument call sites continue to compile unchanged.
…sing AcquireLicenseCb

PlayerRialtoCCManager and PlayerDirectRialtoCCManager released their
control mutex between assigning the new handle and applying the cached
track via SetTrack(), letting a concurrent Initialize()/SetTrack() call
interleave and configure the wrong handle. Add a private SetTrackLocked()
in each and hold the mutex for Initialize()'s entire body so handle
assignment and configuration happen atomically. This lets
PlayerDirectRialtoCCManager drop clearControlOnFailure() in favor of an
inline reset, since the lock is now held continuously from handle
assignment through configuration.

Also set err = MW_DRM_INIT_FAILED in DrmSessionManager::createDrmSession()
when AcquireLicenseCb isn't registered, matching sibling failure paths;
previously err was left unset and callers' `if (err != -1)` check
silently swallowed the failure.
RDKEMW-23999 Direct Rialto - Add changes required for Direct Rialto integration
…y caps setting in mp4demux

Reason for change:Use codec info from the manifest rather than lazy caps setting in mp4demux
Risks: p1

Signed-off-by: varshnie <varshniblue14@gmail.com>
…y caps setting in mp4demux

Reason for change:Use codec info from the manifest rather than lazy caps setting in mp4demux without cfg
Risks: p1

Signed-off-by: varshnie <varshniblue14@gmail.com>
VPAAMP-959:[MP4DEMUX]Use codec info from the manifest rather than laz…
VPAAMP-1341: Fix MP4 demux unknown-format stream setup
Reason for change:
EOS only fires when both audio and video have resumed data flow for a normal discontinuity — never for just one side
Test Procedure: Refere ticket
Risks: low
Priority: P1

Co-authored-by: rekha kandhavelan <rekha_kandhavelan@comcast.com>
Reason for change: ClearKeySession::decrypt() trusted the subsample
table from the media segment (senc). Clear/encrypted lengths were never
checked against the sample size, so a crafted ClearKey stream could
cause heap out-of-bounds reads and writes in the gather and scatter
memcpy loops.

Validate the whole subsample table before allocating or copying:
reject truncated tables and tables whose summed ranges (64-bit, so no
wraparound) exceed the sample buffer. Also handle malloc failure.

Test Procedure: New ClearKeySubSampleDecryptTests in DrmOcdmTests cover
valid, exact-fit, oversized, cumulative-overflow, wraparound and
truncated subsample tables. The gst mock can now map real buffer data.
Risks: Low. Valid content follows the existing decrypt path.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings November 1, 2026 01:51
@pstroffolino
pstroffolino requested a review from a team as a code owner November 1, 2026 01:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The change introduces a cross-track data race that can incorrectly suppress EOS and advertises encrypted elementary streams as clear when no DRM system is configured, preventing decryptor selection.

4 open issues
What changed in this PR

This PR hardens ClearKey decryption against malicious CENC subsample lengths. It also extends the player’s codec metadata, DRM session integration, and direct-Rialto closed-caption support.

Changes:

  • Validate subsample ranges before copying encrypted data, with regression tests for oversized, overflowing, and truncated inputs.
  • Carry encryption metadata through pipeline configuration and return thread-safe snapshots of usable DRM keys.
  • Add injectable DRM session creation, direct-Rialto caption controls, and related lifecycle and test updates.
File Description
test/​utests/​tests/​PlayerDirectRialtoCCManagerTests/​PlayerDirectRialtoCCManagerTests.cpp Adds the caption test runner.
test/​utests/​tests/​PlayerDirectRialtoCCManagerTests/​PlayerDirectRialtoCCManagerTestCases.cpp Tests caption selection, failures, muting, and reset.
test/​utests/​tests/​PlayerDirectRialtoCCManagerTests/​PlayerDirectRialtoCCManagerBaseStubs.cpp Supplies caption base-class test stubs.
test/​utests/​tests/​PlayerDirectRialtoCCManagerTests/​CMakeLists.txt Builds the new caption test suite.
test/​utests/​tests/​OCDMSessionAdapter/​FunctionalTests.cpp Replaces an expanded mock call with EXPECT_CALL.
test/​utests/​tests/​OcdmBasicSessionAdapterTests/​FunctionalTests.cpp Updates usable-key mocks to return copies.
test/​utests/​tests/​InterfacePlayerTests/​InterfacePlayerFunctionTests.cpp Tests encryption metadata, caps, and reconfiguration.
test/​utests/​tests/​GstPlayer/​PauseOnPlaybackTests.cpp Migrates pipeline calls to codec metadata.
test/​utests/​tests/​GstPlayer/​FunctionalTests.cpp Updates pipeline configuration arguments.
test/​utests/​tests/​DrmSessionManagerTests/​DrmSessionManagerTestCases.cpp Tests injected session creation and updates key mocks.
test/​utests/​tests/​DrmOcdmTests/​CMakeLists.txt Registers ClearKey subsample tests.
test/​utests/​tests/​DrmOcdmTests/​ClearKeySubSampleDecryptTests.cpp Tests valid and malformed subsample ranges.
test/​utests/​tests/​CMakeLists.txt Registers the direct-Rialto caption suite.
test/​utests/​tests/​ClosedCaptionsTests/​CMakeLists.txt Adds direct-Rialto header access.
test/​utests/​mocks/​MockOpenCdmSessionAdapter.h Returns usable keys by value.
test/​utests/​mocks/​MockDrmSession.h Makes key-ID mocking backend-independent.
test/​utests/​fakes/​Fakeopencdmsessionadapter.cpp Adopts snapshot return semantics.
test/​utests/​fakes/​FakeDRMSessionManager.cpp Matches the injectable constructor signature.
test/​utests/​fakes/​CMakeLists.txt Adds Rialto test include paths.
test/​utests/​drm/​mocks/​gstMocks.h Declares buffer-mapping test helpers.
test/​utests/​drm/​mocks/​gstMocks.c Adds mapped-buffer data and byte-reader positioning.
test/​pi-cli/​commandProcessing.cpp Passes structured codec information.
InterfacePlayerRDK.h Declares the codec-aware pipeline overload.
InterfacePlayerRDK.cpp Updates encrypted caps, stream metadata, and discontinuity handling.
InterfacePlayerPriv.h Stores codec metadata per stream.
drm/​ocdm/​opencdmsessionadapter.h Declares usable-key snapshot returns.
drm/​ocdm/​opencdmsessionadapter.cpp Copies usable keys while holding the mutex.
drm/​DrmSessionManager.h Defines and stores an injectable session creator.
drm/​DrmSessionManager.cpp Uses injected creators and guards missing license callbacks.
drm/​DrmSession.h Extends backend-neutral session APIs.
drm/​DrmSession.cpp Removes the reference-returning key implementation.
drm/​ClearKeyDrmSession.cpp Validates subsample bounds before allocation and copying.
DemuxDataTypes.h Adds NAL framing metadata and grouped track codecs.
CMakeLists.txt Builds direct-Rialto captions and installs their interface.
closedcaptions/​rialto/​PlayerRialtoCCManager.h Declares handle locking and invalidation.
closedcaptions/​rialto/​PlayerRialtoCCManager.cpp Synchronizes control access and cached track updates.
closedcaptions/​PlayerCCManager.h Adds backend selection and handle lifecycle APIs.
closedcaptions/​PlayerCCManager.cpp Creates direct-Rialto managers and separates state reset.
closedcaptions/​direct-rialto/​PlayerDirectRialtoCCManager.h Declares the direct-Rialto caption manager.
closedcaptions/​direct-rialto/​PlayerDirectRialtoCCManager.cpp Implements caption controls and handle lifecycle management.
closedcaptions/​direct-rialto/​IDirectRialtoCC.h Defines the backend-independent caption control interface.

Review effort: Balanced

Comment thread InterfacePlayerRDK.cpp
Comment on lines +106 to +110
if (drmSystem == NULL)
{
MW_LOG_WARN("Skipping encrypted caps conversion: DRM system ID is NULL");
return;
}
Comment thread InterfacePlayerRDK.cpp
if (!codecChange && siblingType != eGST_MEDIATYPE_DEFAULT)
{
gst_media_stream *siblingStream = &interfacePlayerPriv->gstPrivateContext->stream[siblingType];
if (siblingStream->format != GST_FORMAT_INVALID && !siblingStream->firstBufferProcessed)
Comment thread InterfacePlayerRDK.cpp
Comment on lines +3815 to +3817
if ((type == eGST_MEDIATYPE_AUDIO) && (interfacePlayerPriv->gstPrivateContext->stream[eGST_MEDIATYPE_SUBTITLE].source))
{
GstPlayer_SignalEOS(interfacePlayerPriv->gstPrivateContext->stream[eGST_MEDIATYPE_SUBTITLE]);
Comment on lines +3267 to +3268
EXPECT_CALL(*g_mockGstUtils, GetCaps(GST_FORMAT_ISO_BMFF)).WillOnce(Return(&caps));
EXPECT_CALL(*g_mockGStreamer, gst_caps_get_structure(_, _)).Times(0);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants