Skip to content

Decoder hardening (phase 0) and the AppContainer spike (phase 1) - #72

Merged
GrischaE1 merged 4 commits into
mainfrom
decoder-hardening
Oct 23, 2026
Merged

GrischaE1 merged 4 commits into
mainfrom
decoder-hardening

Conversation

@GrischaE1

Copy link
Copy Markdown
Owner

Phases 0 and 1 of docs/decoder-isolation-design.md (threats T9.1 and T9.2): a device that has been compromised sends crafted video or clipboard images to the native decoders in RemoteHub's process. Phase 0 narrows what reaches those decoders now. Phase 1 checked that the full fix, a sandboxed decoder process, works before building it. The design doc (written 2026-10-22) is part of this PR, with the decisions, the phases and the spike results.

Phase 0 – hardening in RemoteHub's process

Video (AgentDesktopSession.CheckVideo, VideoCodec.cs)

  • A video message is checked before the decoder sees it. These count as bad frames:
    • a picture size outside 1 × 1 to 4096 × 2304
    • a message shorter than its header
    • a message with no picture data
  • A bad frame is logged, the decoder is flushed and a key frame is asked for. The frame is still acknowledged when its id is readable.
  • Before:
    • a short message ended the session through an exception outside the decoder's handler
    • a header of 65535 × 65535 reached the conversion, where w × h × 4 overflows int and the buffer would be too small
  • The decoder's NV12 output is converted only when a row fits its stride, and the console takes no frame above the protocol's limit.

Clipboard images (Services/DeviceImage.cs, used by FinishReceiving and ClipboardOffer.Render)

  • Only PNG, BMP and JPEG, each with its own WIC decoder. WIC's auto-detection, which tries every installed codec, is no longer used.
  • RemoteHub reads the size from the header itself before any decoder runs: PNG IHDR, both BMP headers, the JPEG frame header. The size must be at most 8192 × 8192, and the decoded frame must match it.
  • Colour profiles are ignored, which is one less parser.
  • The pushed image and both formats of an offered one (PNG and CF_DIB) are rebuilt from the pixels. The device's bytes never reach this PC's clipboard, BMP included; until now, BMP bytes went onto the clipboard unread.
  • A pushed image is decoded on the receiving thread instead of the UI thread.
  • A refused image shows "The image copied on the device can't be taken – only PNG, BMP and JPEG up to 8192 × 8192 pixels are." That covers a paste as well.

Phase 1 – spike: the decoder in an AppContainer

A throwaway program (not in the repository) ran on Windows Server 2025 the way §3.2 describes:

  • an AppContainer with no capabilities
  • a job with KILL_ON_JOB_CLOSE, one process, 1 GB and all UI limits
  • an explicit handle list (two pipes and a section)

Result: it works as designed. No change to the design, and no C++ decoder is needed. Details are in §11.

  • Framework-dependent .NET 10 starts in the container in about 0.3 s.
  • Media Foundation's H.264 decoder and WIC work there.
  • Per frame, the cost is the same as in-process:
    • 1080p: 9.4 ms against 9.9 ms in-process
    • 4K: 34.2 ms against 34.8 ms
  • Memory per decoder: 42 MB private at 1080p, 112 MB at 4K.
  • These are blocked from inside the container:
    • the network (loopback, internet, DNS)
    • the user's profile and RemoteHub's database folder
    • writing anywhere
    • writing to HKCU
    • the clipboard
    • starting a process
    • opening RemoteHub's process
    • using other processes' windows
  • A decoder that hangs is ended by closing the job.
  • Not yet tried on Windows 11. Phase 4's lab cases cover that.

Decisions (§9):

  • one decoder per session
  • if no AppContainer can be created, fall back to low integrity, then refuse to show the picture; never decode in-process
  • every clipboard image is re-encoded from pixels (built here)

Tests

  • New DeviceImageTests:
    • PNG, BMP and JPEG are read with the size their header states, and pixels round-trip exactly
    • GIF, TIFF and noise are refused
    • an oversized header (PNG and BMP) is refused before decoding; BMP core and top-down headers are read
    • JPEG without a frame header is refused
    • 300 truncated or bit-flipped images per format give pixels or null, never an exception
    • the DIB is built bottom-up
    • the video checks: limits, short, no data, the id still read
  • ClipboardOfferTests:
    • a pasted BMP with trailing bytes comes back as a PNG RemoteHub made, with the same pixels
    • an offered GIF pastes as nothing
  • Unit tests: 1,133 listed, 1,127 passed, 6 skipped, none failed. The build has no warnings.
  • Lab:
    • Not run on this branch.
    • The normal clipboard and video paths are covered by the existing D-CLIP and D-VID cases.
    • The hostile-device cases (SEC-DEC-01/02) need the rogue agent and come with phase 4.

Docs

  • decoder-isolation-design.md:
    • status line
    • §2 (what changed)
    • §3.6 (built)
    • §9 (decisions made)
    • §10 (phases)
    • §11 (spike results)
  • security-test-lab-design.md: rows T9.1 and T9.2.
  • README: the agent desktop's clipboard.

Video from a device is checked before the decoder sees it: a picture size
outside the protocol's limits, a message shorter than its header or one
without data is a bad frame (logged, key frame requested) instead of ending
the session; the decoder's output is converted only when a row fits its
stride.

Clipboard images from a device (DeviceImage): PNG, BMP and JPEG only, each
with its own WIC decoder; the size read from the header first (at most
8192 x 8192) and checked against the decoded frame; colour profiles ignored.
The pushed image and both formats of an offered one are rebuilt from the
pixels, so the device's bytes (BMP included) never reach the clipboard.

decoder-isolation-design.md: the decisions made, the phases, and the spike:
framework-dependent .NET, Media Foundation H.264 and WIC run in an
AppContainer with no capabilities, a job and a handle list; no measurable
cost per frame; network, profile, clipboard, processes and other windows
are blocked.
@github-actions
github-actions Bot marked this pull request as draft October 23, 2026 17:48
@github-actions

Copy link
Copy Markdown

Pull requests start as drafts here. Mark this one Ready for review when it's done and all checks have passed.

…op flow tests

- Five_thousand_connections_stay_fast takes the faster of two opens: the first open once stalled for 10 s on a runner
  while every save around it took its usual time.
- AgentDesktopFlowTests.WaitFor waits up to 30 s instead of 15 s; a wait still ends as soon as its condition holds.
- The dropped-connection sign-out test waits for the session's record to be forgotten (it goes right after the cover,
  as in the refused sign-out test) and holds 1.5 s instead of 400 ms, well past its 150 ms look.
@GrischaE1
GrischaE1 marked this pull request as ready for review October 23, 2026 18:39
@GrischaE1
GrischaE1 merged commit c626d4d into main Oct 23, 2026
13 checks passed
@GrischaE1
GrischaE1 deleted the decoder-hardening branch October 23, 2026 18:39
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 23, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant