Skip to content
This repository was archived by the owner on Jun 19, 2026. It is now read-only.

Slice/principle check reduced ia2#678

Closed
oinoom wants to merge 6 commits into
mainfrom
slice/principle-check-reduced-ia2
Closed

Slice/principle check reduced ia2#678
oinoom wants to merge 6 commits into
mainfrom
slice/principle-check-reduced-ia2

Conversation

@oinoom

@oinoom oinoom commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Problem

  • IA2 retags TLS pages with per-compartment pkeys.
  • On x86_64, dav1d single-thread decode can fault in TLS resolution/access paths (including __tls_get_addr) when required %fs-relative static TLS pages near the thread pointer/TCB are tagged compartment-private.

What this changes

  • Add ia2_unprotect_thread_pointer_mapping() (x86_64):
    • Reads /proc/self/maps.
    • Finds writable mappings overlapping the thread-pointer neighborhood.
    • Retags that neighborhood to shared pkey 0.
  • Invoke that helper during runtime init and thread startup:
    • ia2_start() unprotects the TCB page and surrounding mapping.
    • ia2_thread_begin() re-applies mapping unprotect after per-thread TLS setup.
  • Update TLS carve-out logic in protect_tls_pages():
    • Track/dedupe/sort shared TLS pages.
    • Carve out pages containing active ia2_stackptr_N slots.
    • Keep the static TLS prefix up to the TCB page shared when it overlaps a module TLS range.
  • Update pthread_create wrapper:
    • Switch to PKRU(0) around __real_pthread_create, then restore prior PKRU.

Why this fixes it

  • The TCB/static-TLS neighborhood is accessed via %fs across compartment transitions.
  • Keeping those pages shared removes PKRU-dependent TLS faults while preserving compartment tagging for unrelated TLS ranges.

Validation

  • In a clean build/run flow wired to this IA2 tree, the parent commit fails single-thread decode, while this commit restores successful dav1d --threads 1 decode.

David Anekstein added 4 commits April 17, 2026 12:26
Strict dav1d single-thread decode on x86_64 was still failing in IA2 libc
compartment mode because a small amount of writable loader/libc state is
process-global ABI state, not compartment-private state.

Keep only the state that must remain shared:
- writable ld.so heap mappings tagged with IA2_LDSO_HEAP_MARKER
- the ld.so TLS-generation page read during __tls_get_addr resolution
- the libc tuning pages holding the x86_64 memmove/memset threshold globals

The intent is to replace broader carveouts with narrow runtime policy rooted in
what the loader and libc actually read and write during the dav1d reproduction.
ia2_unprotect_loader_heap_maps() handles the writable ld.so heap mappings at
startup, and protect_pages() adds the page-level carveouts before retagging the
rest of each segment.

This is the runtime-only part of the minimal main-based IA2 delta. The paired
dav1d strict reproduction still reaches `dav1d --version` and decodes test.ivf
successfully with these carveouts in place.
The minimal dav1d stack needs calls to shared_malloc(), shared_free(), and the
other shared allocator helpers to route to the shared allocator compartment.
Encoding that rule only in the rewriter makes the policy implicit and
application-specific.

Annotate the allocator declarations themselves with ia2_extern_pkey:1 and add a
libia2 wrapper header that exposes those declarations from the runtime include
 tree. This moves the ownership policy into the API contract: code that includes
these declarations can say directly that the entrypoints live in the shared
pkey.

This commit intentionally changes interface metadata only. The rewriter change
that consumes these annotations follows next.
The rewriter previously learned external pkeys only from system-header handling
and hardcoded exceptions. That was enough to get a working dav1d branch, but it
kept shared allocator routing as tool-specific knowledge instead of making it a
declaration-driven rule.

Teach SourceRewriter to parse ia2_extern_pkey:<n> annotations on declarations
and record the annotated pkey even when the declaration would otherwise be
ignored for rewriting. This lets external APIs such as the shared allocator
state their required compartment directly in headers.

Together with the previous commit, this preserves the generated callgate routing
needed by the minimal strict dav1d decode stack while removing the need for a
dav1d-specific `shared_*` exception in the rewriter.
Single-thread dav1d decode still passes after removing the
ia2_extern_pkey annotation machinery from ia2_allocator.h.

This was worth testing because the original minimal stack carried two
extra IA2 commits whose stated purpose was to annotate shared allocator
entrypoints and teach the rewriter to honor those annotations. In the
current dav1d single-thread path, those annotations are not what makes
shared_malloc/shared_free work:

- the generated dav1d binaries still reference shared_malloc and
  shared_free as direct dynamic symbols rather than wrapped callgates
- disabling annotated-extern-pkey handling in SourceRewriter.cpp did not
  change single-thread decode behavior
- after no-oping the annotation macro and restoring the normal rewriter,
  strict single-thread decode still passed

So for this reduced branch pair, the annotation syntax in the header is
currently dead scaffolding. Remove only the macro expansion logic and
leave the allocator API surface unchanged.

Validation:
- rebuilt dav1d through rewrite.py against this IA2 tree
- dav1d --version returned 0
- single-thread decode of test.ivf returned 0
@oinoom oinoom force-pushed the slice/principle-check-reduced-ia2 branch from 4e3bc6b to ec00474 Compare April 17, 2026 16:39
David Anekstein added 2 commits April 17, 2026 14:01
Problem
- The reduced principle-check IA2 branch currently makes strict single-thread
  dav1d decode pass by retagging writable loader-heap TLS state to shared pkey
  0 during startup.
- We wanted to know whether the older e664-style x86_64 TLS policy could still
  support the reduced dav1d branch without falling back to the broader
  loader-heap carveout.
- Restoring only the TCB/static-TLS neighborhood sharing was not enough.
  `dav1d --version` succeeded, but `dav1d --threads 1` still crashed in
  `__tls_get_addr+16` while the IVF handoff path entered
  `dav1d_data_wrap -> malloc -> PartitionAlloc::ScopedDisallowAllocations`.
- GDB showed the exact mismatch at the failing read:
  - `%fs` base was on `[anon: ia2-loader-heap]` with `pkey 0`
  - `*(%fs:8)` (the DTV pointer) was on the adjacent
    `[anon: ia2-loader-heap]` mapping with `pkey 1`
  - the faulting `cmp %rax,(%rdx)` used that DTV pointer as `%rdx`

What this changes
- Keep the x86_64 PT_TLS prefix below the TCB page shared in
  `protect_tls_pages()` so `%fs`-relative ABI/TLS state that lives below the
  thread pointer remains accessible across compartment transitions.
- Reintroduce `ia2_unprotect_thread_pointer_mapping()` and call it from
  `ia2_start()` so the startup thread's TCB neighborhood is retagged shared
  after IA2 has finished compartment setup.
- Add `ia2_unprotect_thread_dtv_page()` and call it from `ia2_start()` so the
  startup thread's DTV header page is explicitly retagged shared as well.
- Leave the newer targeted runtime follow-ups from the reduced branch in place:
  the ld.so TLS-generation page carveout and the libc memmove tuning-page
  carveouts remain unchanged.
- Stop depending on `ia2_unprotect_loader_heap_maps()` for this reproduction.

Why this fixes it
- The targeted TCB/static-TLS carveout fixes `%fs`-relative accesses in the TCB
  neighborhood, but `__tls_get_addr` also dereferences `THREAD_DTV()` via
  `%fs:8`.
- On this reduced single-thread dav1d path, PartitionAlloc reaches that
  `__tls_get_addr` fast path during the IVF packet handoff allocation path.
  Leaving the DTV header page compartment-private therefore still faults even
  though the TCB page itself is shared.
- Sharing the DTV header page closes that remaining gap without going back to a
  blanket retag of every writable IA2 loader-heap mapping.
- The current single-thread reproduction does not need the larger thread-start
  changes from the earlier e664 line. In particular, the extra
  `pthread_create()` PKRU wrapper, the per-thread post-TLS retag in
  `ia2_thread_begin()`, and the standalone one-page
  `ia2_unprotect_thread_pointer_page()` startup call were all removed while the
  strict dav1d reproduction continued to pass.
- The resulting policy is narrower and better motivated: share the startup
  thread's TCB/static-TLS neighborhood and DTV page, rather than all marked
  loader-heap VMAs.

Validation
- rebuilt `principle_check/dav1d-ia2` through `rewrite.py` against this IA2
  tree and dav1d `66a21b9`
- `tools/dav1d --version` returned 0
- `tools/dav1d -i /home/davidanekstein/immunant/test.ivf -o /dev/null --muxer
  null --threads 1` returned 0
- decode completed: `Decoded 2/2 frames (100.0%)`
Problem
- `loader_minimal_malloc` was written against the older assumption that every
  VMA tagged `ia2-loader-heap` would remain compartment-private with `pkey 1`.
- The reduced single-thread dav1d branch no longer has that policy. It
  intentionally retags part of the loader heap to shared `pkey 0` so the startup
  thread can access the TCB/TLS/DTV state required by the strict decode path.
- The old test only looked at the first `ia2-loader-heap` entry in
  `/proc/self/smaps` and asserted that one mapping had `ProtectionKey: 1`.
  Once one shared loader-heap mapping appeared first, the test failed even when
  other loader-heap mappings still remained private with `pkey 1`.

What this changes
- Parse `/proc/self/smaps` entry-by-entry and collect every mapping tagged with
  `IA2_LDSO_HEAP_MARKER` instead of stopping at the first match.
- Record the address range and `ProtectionKey` for each loader-heap mapping.
- Print the full loader-heap pkey breakdown so the test output shows the actual
  mixed policy when debugging regressions.
- Change the assertion from "the first loader-heap mapping has pkey 1" to
  "at least one loader-heap mapping still has pkey 1".

Why this fixes it
- The old test was overfitting to a single mapping order rather than checking
  the actual security/property question.
- On the current branch, the useful invariant is that the loader heap must not
  collapse entirely to shared `pkey 0`. Some pages may be shared for startup TLS
  reasons, but other loader-heap mappings should still remain compartment-
  private.
- Enumerating all loader-heap mappings lets the test distinguish
  "mixed-policy carveout working as intended" from "everything got retagged
  shared".

Observed breakdown on this branch
- direct run of the updated test reported:
  - `0x74e9292fe000-0x74e929307000 pkey=0`
  - `0x74e929307000-0x74e92930a000 pkey=1`
  - `0x74e929338000-0x74e92933a000 pkey=1`
  - `0x74e929685000-0x74e929687000 pkey=1`
- That is exactly why the old test was stale: the loader heap now has a mixed
  pkey distribution, not a uniform one.

Validation
- rebuilt `tests/loader_minimal_malloc/loader_minimal_malloc`
- direct run printed the mixed pkey breakdown above and exited 0
- `ctest --test-dir build/x86_64 -R loader_minimal_malloc --output-on-failure`
  passed
- strict single-thread dav1d decode still passed after the runtime simplification
  and test update
@oinoom oinoom force-pushed the slice/principle-check-reduced-ia2 branch from 0a96d08 to 3b814d2 Compare April 17, 2026 18:02
@oinoom oinoom closed this Apr 20, 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