Skip to content

Slice/principle check reduced dav1d#5

Draft
oinoom wants to merge 8 commits into
ia2from
slice/principle-check-reduced-dav1d
Draft

Slice/principle check reduced dav1d#5
oinoom wants to merge 8 commits into
ia2from
slice/principle-check-reduced-dav1d

Conversation

@oinoom

@oinoom oinoom commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

No description provided.

David Anekstein added 8 commits April 16, 2026 00:21
The rewrite workflow was accidentally replacing the IA2-built dynamic loader
with the host loader. `ldd` prints the interpreter with an absolute path on the
left-hand side, and parse_ldd() treated that entry like an ordinary shared
library dependency. The copy step then overwrote
runtime/libia2/ld-linux-x86-64.so.2, which caused the rewritten binary to fail
before decode even started.

Skip absolute-name ldd entries and explicitly stage the loader produced by the
IA2 glibc build into runtime/libia2. This keeps the interpreter embedded by the
Meson link step and the loader present on disk consistent.

This is a build-side fix only. It does not change dav1d runtime behavior; it
preserves the IA2 loader that the build already intended to use.
After fixing the loader, the first strict-mode crash moved into
___pthread_once() during dav1d_open(). The once-control object was a file-static
pthread_once_t in libdav1d's private compartment, but glibc's pthread_once()
implementation also needs to read and update that state.

Mark only the once-control word as IA2_SHARED_DATA. This keeps the
synchronization object visible to both sides of the call without broadening the
rest of libdav1d initialization state.

This is the narrowest source-side fix for the observed pthread_once() fault.
Sharing the pthread_once_t was not sufficient on its own. The next strict-mode
failure was still in the pthread_once path because the generated
__wrap_pthread_once wrapper used the normal main-compartment PKRU during the
call phase while glibc bookkeeping and the init_internal() callback were both
active.

Patch the generated __wrap_pthread_once block to use the union PKRU for the
callback phase. That keeps libc's internal once machinery accessible while the
callback initializes libdav1d state.

The change stays local to pthread_once in rewrite.py. This is deliberately a
surgical build-side workaround for the minimal reproduction rather than a broad
runtime policy change.
ivf_read() runs in the tools compartment, but dav1d_data_create() would return a
decoder-owned buffer from libdav1d. Passing that private buffer to fread() makes
the tools side write through a pointer into decoder-owned memory, which is the
strict-mode pointee-ownership fault we were hitting.

Allocate the packet payload with shared_malloc() instead and transfer it into
Dav1dData with dav1d_data_wrap() plus an explicit shared_free callback. This
keeps the change narrow: only the demux-to-decoder handoff buffer becomes
shared.

This avoids teaching IA2 about pointee ownership for fread() while keeping the
sharing decision explicit in dav1d.
Dav1d_open() initializes a pthread_attr_t in libdav1d and then passes it through
glibc helpers such as pthread_attr_*(), get_stack_size_internal(), and
pthread_create(). In strict IA2 mode that attribute object is shared mutable
state crossing the libdav1d/libc boundary.

Allocate the pthread_attr_t from the shared allocator and free it explicitly
once thread setup is finished. The change is intentionally limited to the
attribute object; thread stacks and the rest of Dav1dContext stay on their
existing allocation paths.

This keeps glibc's stack-size calculations and thread-creation path from
touching compartment-private memory.
The remaining dlsym() call in get_stack_size_internal() is not a normal libc
call. glibc's dlsym(RTLD_DEFAULT, ...) is caller-sensitive: it records the call
site and uses that identity during symbol resolution. A normal IA2 call gate
changes the apparent caller, while leaving dlsym() unwrapped would make dynamic
loader code run under the active compartment's PKRU.

This lookup is only a glibc-private workaround probe for
__pthread_get_minstack, not part of dav1d's decode logic. Under IA2 we also
switch worker threads onto IA2-managed stacks, so depending on glibc's internal
stack/TLS accounting here is not a good contract.

Disable just this probe when IA2 is enabled and document why. That keeps the
fix narrow and avoids turning dlsym() into a special cross-compartment API
before IA2 has a principled design for it.
After the earlier fixes, strict single-thread decode still failed when dav1d's
memory-pool control blocks lived in the decoder compartment. The pool headers
carry the mutex, refcount, and freelist pointers that are mutated through shared
synchronization paths even though the payload buffers themselves can stay on the
normal aligned allocation path.

Allocate only Dav1dMemPool from shared memory and free it there as well. Buffer
payloads continue to use the existing dav1d allocation flow. rewrite.py also
keeps src/mem.c when constructing the rewritten tree so this source change
survives the minimal reproduction workflow.

With this change in place, the minimal stack reaches successful strict
single-thread decode again.
Two pieces of the previous dav1d single-thread stack turned out to be
broader than necessary.

First, dav1d_open() does not need its static pthread_once_t control word
in shared storage for single-thread decode. The real requirement is the
rewrite.py patch that widens the generated __wrap_pthread_once PKRU mask.
With that wrapper fix in place, strict single-thread decode still passes
when initted is an ordinary file-static pthread_once_t. Reverting only
that storage annotation and rebuilding preserved both dav1d --version and
--threads 1 decode.

Second, rewrite.py no longer needs to copy the IA2-built loader from
external/glibc/sysroot/lib into runtime/libia2. The important loader fix
is keeping parse_ldd() from overwriting the interpreter path with the
host loader when ldd reports an absolute-name left-hand side. With that
guard retained, the IA2 build already recreates the correct loader at
runtime/libia2 during the rewrite/build flow.

This was revalidated under the stricter condition that previously made
loader handling fail: delete runtime/libia2/ld-linux-x86-64.so.2, rerun
rewrite.py, then confirm that:

- the recreated runtime/libia2 loader matches the IA2 sysroot loader
- tools/dav1d requests the IA2 runtime loader as its interpreter
- dav1d --version returns 0
- single-thread decode of test.ivf returns 0

No other dav1d single-thread carveouts were removed here because the
other tested candidates all produced concrete crashes:

- private pthread_attr_t crashed in pthread_attr_init
- non-shared IVF payloads crashed in fread/memmove
- re-enabling the glibc minstack dlsym probe crashed in dlsym
- private Dav1dMemPool headers crashed in pthread_mutex_init
- removing the pthread_once wrapper widening crashed in pthread_once
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.

1 participant