# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- **spawn**: `SessionExitPolicy` on `SpawnOptions`. Decides what natural
completion (the leader exiting on its own) does with a still-live
isolated/pty session. `Sweep` (default) gates completion on the session
sweep emptying `/proc`, with **leader-reap as the sweep trigger** (not
master EOF), so a background member holding the pty slave open can no
longer stall completion (previously hung forever); the sweep SIGKILLs such
contained members. `LetMembersSurvive` reports completion on leader-reap
without signaling the session — nohup-style background jobs keep running.
Mirrored into the blocking `wait_loop`.
- **spawn**: D-state give-up is now keyed on *either* SIGKILL-sent-elapsed
*or* sweep-started-elapsed, with no `status.is_none()` requirement, in both
`poll_completion` and `wait_loop`: a reaped leader with a D-state member no
longer re-enumerates `/proc` forever; the session is handed to the orphan
reaper (starttime-gated).
- **spawn/io**: `Reactor::mod_` — `EPOLL_CTL_MOD` interest toggle preserving
the token; `FdSlot` now carries per-direction flags; `pause_stdout` /
`resume_stdout` toggle interest on one registration instead of del+add, so
pausing output no longer removes the fd (and its writable interest) from the
reactor. `DrainState::write_input_nonblock` / `RunningProcess::write_input_nonblock`
/ `ManagedProcess::write_input_nonblock` — non-blocking pty writes returning
partial count or `None` on `EAGAIN`.
- **binder**: `ParcelReader::read_bytes` — reads a raw byte array (Java
`writeByteArray`) as owned bytes; `None` for the null marker (`-1`).
Mirrored host-stub in `binder/mod.rs`. `read_byte_array` vtable slot +
`AParcel_readByteArray` dlsym bind.
- **proc**: `starttime(pid)` / `parse_stat_starttime` — `/proc/<pid>/stat`
field 22, the process-incarnation identity used to pin orphaned-session
sids against pid recycling.
- **spawn**: opt-in `SpawnOptions::pdeath_signal` — `PR_SET_PDEATHSIG`, armed
in `child_entry` **before** any other setup with a `getppid()` recheck that
`_exit`s the child if the parent died in the fork→prctl window. Leader-only;
the signal fires when the parent **thread** that created the task exits.
### Changed
- **binder**: `StringBuf::finish` no longer truncates at the first NUL byte.
It records the AOSP allocator length (`utf16_to_utf8_length() + 1`,
terminator included) and drops only the trailing terminator, **preserving
embedded NULs** so callers (spawn argv, daemon `NulInArg`) can reject them
instead of silently executing a NUL-truncated *different* argument.
**Behavior change**: embedded-NUL strings now reach the caller; read_string
callers that relied on silent truncation must handle them.
- **spawn**: reaper session arm prunes by confirmed-reaped pids only (never a
stale snapshot intersection) and starttime-verifies every registered sid
before each sweep iteration; a sid whose leader is gone/recycled is dropped
without killing.
- **spawn**: `ManagedProcess::Drop` for session-like spawns with a reaped
leader now runs the session sweep (freshness-gated) and hands non-converging
sessions to the orphan reaper, instead of silently skipping (recycled-pid
guard retained for non-session spawns).
### Removed
- **spawn**: the dead `CLONE_NEWPID` fail-closed guard + `pid_ns_isolated`
helper + its test from `clone3.rs` (nothing ever set the flag; the "kernel
silently drops the flag" premise was a probe constant bug — the probe used
`CLONE_PTRACE`'s value `0x0000_2000`, real `CLONE_NEWPID` is `0x2000_0000`).
Comment preserved with the correct constants and the namespaced-child
footgun warning.
- **spawn**: `deorphan_child` (zero callers).
### Fixed
- **spawn**: natural pty completion hung forever when a background member held
the pty slave open (no master EOF → the sweep, gated behind
`io_done || paused`, never ran). See `SessionExitPolicy` above.
## [2.23.1] - 2026-08-17
### Fixed
- **binder**: host-stub `ParcelWriter::write_bytes` missing, so non-Android
builds that `impl` a `write_bytes` trait method over `ParcelWriter` silently
resolved to the trait method (unconditional recursion). The stub now carries
the method (unsupported-platform error, matching its siblings).
## [2.23.0] - 2026-08-17
### Added
- **binder**: `ParcelWriter::write_bytes`. Writes a raw byte array (Java
`writeByteArray`) via `AParcel_writeByteArray` — `None` writes the null
byte-array marker (`-1` length, which `Parcel.readByteArray` reads back as
null), so byte payloads can round-trip losslessly where `write_string`
previously forced a lossy UTF-8 re-encode. Bound-capped at 1 MiB like
strings. Used by the daemon's exec stream to carry pty chunk bytes intact.
## [2.22.0] - 2026-08-17
### Added
- **spawn**: pty stdin. `RunningProcess::write_input` / `ManagedProcess::write_input`
write bytes to the pty master (the child's stdin on a pty spawn). The master is
`O_NONBLOCK`, so a full write waits for `POLLOUT` (bounded at 2 s) when the tty
input buffer is full, and returns once the line discipline has accepted every
byte. Refused with `EINVAL` for non-pty spawns or a closed master.
## [2.21.1] - 2026-08-17
### Fixed
- **spawn/io**: the `TIOCSWINSZ` and `TIOCSCTTY` ioctl request arguments were
cast to `c_ulong`, which only matches libc's `Ioctl` type on some targets
(gnu). On Android/musl `Ioctl` is `c_int`, so the aarch64 build failed to
compile. Both casts now use `libc::Ioctl`, matching the existing `drm.rs`
usage.
## [2.21.0] - 2026-08-17
### Added
- **spawn**: pty spawn option. `SpawnOptionsBuilder::pty()` spawns the child
on a pseudo-terminal: `Pipes` opens `/dev/ptmx` (`grantpt`/`unlockpt`/
`ptsname_r`) and the child setup path (`child_entry`, shared by
fork/vfork/clone3) `setsid`s, dup2s the slave to fd 0/1/2, and claims it
with `TIOCSCTTY`. The master is drained as the child's single merged
stdout+stderr stream; new `RunningProcess::resize_pty` /
`ManagedProcess::resize_pty` apply `TIOCSWINSZ`.
- **io**: pty-master EOF semantics. A pty master reports EOF as `EIO` (once
the session leader and all slave holders close), so `DrainState` now maps a
stdout `EIO` to a clean EOF when constructed with the new `pty_master` flag;
otherwise `EIO` still surfaces as an error.
- **spawn**: `ChildSetupOp::TtyCtl` (op 11) for the child-setup error
handshake, reporting `TIOCSCTTY`/pty dup failures distinctly.
### Changed
- **spawn**: `DrainState::new` takes a `pty_master: bool` parameter (last
argument); existing pipe-mode callers pass `false`.
- **spawn**: pty validation — rejected for `PosixSpawn` (no child setup step),
without an isolated process group (a session is required before
`TIOCSCTTY`), and combined with a stdin buffer (pty stdin/write is not yet
supported).
## [2.19.0] - 2026-08-16
### Fixed
- **binder**: registration/observer order (finding-26 remainder). The uid-
observer path was fixed in 2.18.x; `am.rs::open_with_observer`,
`am.rs::open_with_fgproc_observer`, and the display callback still registered
*before* publishing the reader/codes/eventfd statics, so a callback landing
in that window saw a matching code with unset state and returned
`STATUS_UNKNOWN_TRANSACTION`, or missed the eventfd write entirely. All
three now publish statics + the core-owned eventfd before the register
transaction.
- **binder**: process-global thread pool is no longer capped at a single
thread. `set_thread_pool_max(0)` plus the one joined thread serialized every
served transaction and every observer/fps/task-stack callback against a
single pool thread; a served handler blocked on its rendezvous
(`recv_timeout`) stalled callback delivery process-wide. The pool now joins
one process-lifetime thread and lets the framework spawn up to
`BINDER_POOL_MAX_THREADS` (8) workers, strictly above the served admission
gate (`DEFAULT_MAX_INFLIGHT` = 4) so four blocked served handlers cannot
occupy every pool thread.
- **binder**: the uid-observer `ForUids` fallback no longer registers
unfiltered (finding 24), and a rejected registration is surfaced instead of
silently running with a dead observer (finding 25) — both from the 2.18.x
remediation batch.
### Changed
- **binder**: the manager's `call("sendBinder")` reply now carries a
`{"status": int}` bundle (`HANDOFF_STATUS_OK`/`REJECTED_UID`/
`REJECTED_DESCRIPTOR`), and `Handoff::send_binders` reads it. A rejected
handoff — previously invisible because the reply was `null` and the daemon
read only the leading `EX_NONE` — now returns `Err(call:handoff_rejected)`,
so the caller keeps its dedup record stale and retries/logs instead of
pinning a uid for a capability the app never received. A legacy manager
that returns a null reply is still accepted. The reply shape is byte-pinned
by host tests (a `BundleSource` read surface shared by `ParcelReader` and
the test `ByteCursor`).
- **MSRV** raised to 1.88 (from the 2.18.x remediation batch).
## [2.18.3] - 2026-08-16
### Added
- **spawn**: new opt-in `session_containment()` on `SpawnOptionsBuilder`. A
seccomp filter installed in the child (after the daemon's own
`setsid`/`setpgid`, before `execve`) denies `setsid`, `setpgid`, `setpgrp`,
`unshare`, and `setns`. Filters are inherited across `fork` and `execve` and
can only be tightened, never loosened, so the child and every descendant are
locked into the process group/session the daemon placed them in — making
`kill_group` (timeout/cancel deactivation) total even against a hostile root
child that tries to escape by daemonizing or switching process groups.
Requires an isolated process group
(`ProcessGroup::new(None, true)`); rejected on `PosixSpawn` (no child setup
step) and on exec-style backends without isolation. The child setup error
protocol gains `ChildSetupOp::Seccomp` so a failed filter install fails the
spawn closed instead of running an uncontained child.
## [2.18.2] - 2026-08-16
### Fixed
- **binder**: the A1 two-binder handoff bundle length was 4 bytes short. The
`"exec"` String16 key is 16 bytes on the wire (`4`-byte UTF-16 length int +
`(4+1)*2` bytes incl. NUL, padded to 4) — not 12 as the old `BUNDLE_LENGTH_TWO`
comment assumed — so the two-entry bundle length was `100`, not the real
`104`. `BaseBundle.readFromParcelInner` used the wrong advertised length and
the server-side parse corrupted the trailing binder. A new host-runnable wire
model (`src/binder/wire.rs`) now *derives* the bundle length by running the
exact write sequence through an AOSP-verified byte counter, and the same
`write_bundle_body` emits the on-device bytes — the advertised length and the
emitted bytes can no longer disagree. Byte math verified field-by-field
against `AParcel_writeString`/`writeStrongBinder` (android-14.0.0_r1) and
pinned by host tests (`single_bundle_body_is_56`, `two_bundle_body_is_104`,
`write_path_emits_exactly_the_advertised_length`).
- **android**: corrects the `registerUidObserverForUids` doc comment — it was
added in Android 14 / API 34, not "S"; the AIDL at android-11..14 tags shows
it absent through API 33 (relevant to the uid-observer fallback range).
## [2.14.0] - 2026-08-14
### Changed
- **binder**: `ActivityManager::open_with_uid_observer` now takes the watched
uid and registers via `registerUidObserverForUids` (scoping events to that
uid at the framework) instead of the global `registerUidObserver`, so
unrelated app churn no longer wakes the consumer's eventfd. Falls back to the
global register when the ForUids code is absent. `ParcelWriter` gains
`write_int32_array` for the `uids` argument.
## [2.8.5] - 2026-08-13
### Fixed
- **spawn**: `ManagedProcess::pid()` no longer panics after completion — the
pid is captured at spawn time and remains queryable once the running handle
is consumed.
- **spawn**: output-limit overflow on the forced-close path (timeout or
cancellation with a wedged pipe) now returns the partial output with the
`timed_out` flag instead of `EOVERFLOW`, matching blocking `spawn`.
- **spawn**: dropping an unfinished `ManagedProcess` no longer blocks forever
when the child is stuck in uninterruptible sleep — the reap wait is bounded,
and a child already reaped by `poll_completion` is never signalled (its pid
may have been recycled).
- **spawn**: `Process::kill` and `Process::kill_group` reject non-positive
pids/pgids with `EINVAL`; `kill(-0)` would have signalled the caller's own
process group.
- **spawn**: `ProcessGroup` with `isolated=true` and a non-zero custom leader
is rejected up front with `EINVAL` on the fork backend — `setsid` puts the
child in a new session, so `setpgid` to an outside leader always failed with
a confusing `EPERM` at spawn time.
- **spawn**: `fork` fd policies (`CloseFrom3`, `Allowlist`) are now enforced
in the child after `fork()` via `getdents64` into a fixed buffer, closing
the TOCTOU where a descriptor opened between the parent-side snapshot and
`fork()` leaked into every child. The child scan reads every batch through
EOF (an fd in a later batch can never leak, however many descriptors are
open), retries an interrupted scan, and aborts the spawn on a scan failure
instead of failing the policy open.
- **spawn**: a setup-error report interrupted by a signal is retried instead
of aborting with a partial message, which the parent would have misread as a
successful spawn.
- **io**: `DrainState::register_with_reactor` is idempotent and no longer
destroys a slot (closing its fd and losing the stream) when a second
registration is attempted or a reactor add fails.
- **binder**: dropping a `DeathRecipient` is now memory-safe even when its
`on_died` delivery is racing the drop on a binder pool thread. The callback
box lives in a process-wide slab behind an `Arc` (the framework cookie is a
stable slot index), so the NDK's asynchronous `unlink` can no longer free a
callback the trampoline is executing; a stale delivery finds an empty slot
and no-ops.
## [2.8.6] - 2026-08-14
### Fixed
- **binder**: `serve` dispatch now holds a per-`ServeCtx` mutex while invoking
the user handler instead of sharing one unsynchronized `ServeHandler` across
binder pool threads. UID/PID are captured before the lock (readers never wait
behind it), the lock is dropped before the kernel reply is transmitted, and a
poisoned mutex is recovered in place.
- **binder**: the attribution header wire layout is now documented and verified
against AOSP SDK 34 — `BUNDLE_LENGTH` is 52 (length excludes the parcel
magic, matching `writeToParcelInner` backpatching), and the SDK 34
`AttributionSourceState` carries **no** `deviceId` field.
- **spawn**: a child stuck in uninterruptible sleep (D-state) no longer spins
the reap loop forever. A SIGKILL is sent only while the child is unreaped,
and after `D_STATE_REAP_BOUND` (500ms) of D-state the spawn returns
`timed_out` with partial output; the kernel reaps the child once it leaves
D-state. Both the blocking and `ManagedProcess` paths are covered, and the
cancel/drop paths never signal a child already reaped (pid-recycle guard).
- **spawn**: pipe ends are created blocking and relocated to fd >= 3 via
`F_DUPFD_CLOEXEC` before dup2 into the child, so child stdio 0/1/2 is never
clobbered; only the parent-facing drain ends are flipped to `O_NONBLOCK`
after spawn (the child must never inherit a non-blocking stdio — it would
silently truncate output on `EAGAIN`).
- **spawn**: `make_pipe` / `make_cloexec_pipe` no longer leak descriptors when
the second relocation fails under fd pressure (EMFILE) — the relocated first
end and the still-open original are closed on the error path.
- **reactor**: the signalfd-mask restore in `Drop` now checks `pthread_equal`
against the setup thread and only touches the mask on the thread that
installed it. A cross-thread drop leaves the setup thread's SIGCHLD block in
place (delivery is not lost) and never mutates the dropping thread's mask.
- **dex**: parsing is now bounded against hostile inputs: encoded values are
skipped with a 64-level depth cap, `method_param_types` caps the parameter
count to the remaining buffer, and string/type/field/proto/method/class-def
section sizes are clamped to `room / element-size` instead of trusting the
declared count (which previously could abort on a `with_capacity` OOM).
Legitimate DEX files are unaffected — the caps only bound absurd declared
counts.
- **property**: `system_set` now returns `io::Error::last_os_error()`. Bionic's
`__system_property_set` returns `-1` and sets `errno` (it does **not** return
a negated errno), so the previous `from_raw_os_error(status)` constructed an
`EINVAL`-ish error and swallowed the real cause (e.g. `EACCES`).
- **fs**: `mmap_madvise_raw` stats the fd and clamps the mapping length to
`st_size - offset` before mapping, so a `touch` at the end of the file can
never raise `SIGBUS` past EOF. The alignment check still runs first, and the
post-clamp empty mapping is handled.
### Security
- **dex**: the `with_capacity` OOM abort on a hostile declared section count
could crash the daemon (DoS); the depth cap similarly bounds recursion on a
deeply nested encoded value. Both are now hard-bounded (see above).
## [2.7.0] - 2026-08-13
### Added
- **transport**: the command/watcher transport seam. `TransportEvent<C, R>`
(`Command { cmd, calling_uid, reply }`, `WatcherClosed`, `WatcherDied`),
`ReplySink<R>` (one-shot reply target, consumed exactly once), and the
persistent `WatcherId` / `WatcherSink<U>` pair. The message-shaped boundary
between transport-agnostic channel logic and transport backends (the
abstract-socket daemon today, a Binder backend later); generic over the
domain command and reply types. Client (`ReplySink`, one-shot) and watcher
(`WatcherId`, long-lived) are distinct concepts; a watch registration is a
command, so watcher-registry mutation is single-owner by construction.
## [2.6.0] - 2026-08-13
### Added
- **binder::Handoff**: the content-provider handoff client. Resolves an app's
exported provider through `IActivityManager.getContentProviderExternal`,
walks the `ContentProviderHolder` reply parcel (the full ProviderInfo →
ApplicationInfo chain, pinning the app's `uid` from `ApplicationInfo.uid`
with no extra IPC), hands a service binder to it via
`IContentProvider.call("sendBinder", …)`, and releases it with
`removeContentProviderExternalAsUser`. Tx codes for the lifecycle are
resolved fresh from `framework.jar` DEX (`dex::resolve_handoff_codes`);
`CALL_TRANSACTION` is the stable constant 21. Wire target: Android 14 /
SDK 34. See `docs/BINDER-EXEC-DILEMMA.md`.
- **binder::sys**: general parcel-skip primitives shared by the handoff walk —
`skip_bundle`, `skip_char_sequence`, `skip_sparsearray`,
`skip_typed_object_array`, `skip_string8_array`, and a `skip_value` covering
the full `Parcel.writeValue` tag table (`VAL_IBINDER = 15`,
`VAL_INTARRAY = 18`, …); `ParcelReader::owned` constructor for reply
cursors.
### Changed
- **binder**: module wiring for `handoff` and `serve` (server-side primitives),
with matching non-Android stubs so the crate stays type-checkable off-device.
## [2.5.1] - 2026-08-13
### Changed
- rustfmt normalization across the split binder/spawn modules (import
ordering, line wrapping, module declaration ordering, one duplicated
`#[cfg]` attribute). Cosmetic only; no behavior change.
## [2.5.0] - 2026-08-13
### Added
- **spawn**: `spawn_managed` and `ManagedProcess` let a caller-owned reactor
drive the full child lifecycle without blocking. Core retains ownership of
stdout/stderr draining, exact output-limit reporting, timeout and explicit
cancellation, TERM-to-KILL escalation, process-group signaling, and
`waitpid` reaping; callers route readiness events and poll at
`next_deadline()`.
### Changed
- **spawn**: split into per-backend modules — `exec` (exec-family helpers),
`posix`, and `fork`. The public API is unchanged; only the module layout
moved.
- **binder**: split into per-service modules — `sys`, `am` (activity
manager), `display`, `fps`, `task_stack`, and `raw`. The public API is
unchanged; only the module layout moved.
### Fixed
- **spawn**: dropping an unfinished `ManagedProcess` now sends SIGKILL to its
configured process target and reaps the child. Reactor-registration failures
and other abandoned setup paths cannot leave a live child or zombie behind.
- **spawn**: process-group leader `0` is normalized to the spawned child PID
when signaling. POSIX `setpgroup(0)` creates a child-owned process group;
treating zero as a literal kill target could signal the caller's own group.
## [1.2.40] - 2026-08-11
### Changed
- **Binder**: the `FpsListener` wake eventfd is now created with
`EFD_NONBLOCK`, matching the obs/fgproc observer eventfds. The callback only
ever writes to it (an eventfd write never blocks), while epoll-based
consumers register it edge-triggered and drain to EAGAIN — a blocking fd
wedged the consumer's drain loop (observed on-device: the fps reactor
stopped serving its socket and never streamed a sample). Consumers that
still prefer blocking reads can clear `O_NONBLOCK` on their own dup.
## [1.2.36] - 2026-08-10
### Fixed
- **Binder**: a genuine idle 0-FPS report is no longer mistaken for "no report
yet". `last_fps` used the value's bit pattern as its own sentinel —
`0.0f32` is bit pattern 0, so an idle `onFpsReported(0)` read as `None`:
the sample was dropped for watchers, and (downstream) the first-report-after-
task-swap drop stayed armed and discarded the next real measurement. A
publish-time `FPS_SEEN` flag now disambiguates the states; the decision is
factored into a testable helper with a regression test.
## [1.2.35] - 2026-08-10
### Fixed
- **spawn**: a timed-out child with a custom process-group leader is now
actually signaled. `wait_loop` targets the child's effective pgid
(the configured leader, or the child's own pid after `setsid`) instead of
`kill(-pid)`, which addressed a group that did not contain the child — the
SIGTERM/SIGKILL escalation never reached it and the wait could hang
forever. `Process::kill_pgroup` now delegates to a new explicit-group
`Process::kill_group`.
## [1.2.34] - 2026-08-10
### Fixed
- **Binder**: input parcels on the write path are RAII-owned. A failed
`AParcel_write*` between `AIBinder_prepareTransaction` and `AIBinder_transact`
previously leaked the prepared parcel; `transact_write` now drops it on the
error path and records the transfer into `AIBinder_transact` (the framework
deletes the input parcel even on failure), so the wrapper never deletes
twice. Reworked the observer, foreground-process observer, display-callback,
FPS, and task-stack transactions onto it.
### Security
- **Binder**: parcel string reads are length-bounded. `string_alloc` now
refuses negative or > 1 MiB advertised lengths instead of driving an
unbounded `reserve_exact` (OOM abort); the read surfaces as an error.
### Internal
- **drm**: `wait_vblank_ioctl_matches_known_constant` writes the expected
ioctl via a `u32` literal so the test compiles on the Android target (where
`libc::Ioctl` is `i32`).
## [1.2.33] - 2026-08-10
### Fixed
- **Binder**: `FpsListener` wake routing is now actually functional. The
per-instance eventfd was handed to `AIBinder_new` as the callback binder's
args, but `fps_on_create` still returned `null`, and `AIBinder_getUserData`
returns exactly what `onCreate` returns — so the wake could never fire. The
callback `onCreate` now passes the userdata through (matching
`TaskStackListener`) and `onDestroy` reclaims the box. Released 1.2.32's
per-task FPS reporting regressed to silent (event never signalled); this
restores it.
## [1.2.32] - 2026-08-10
### Fixed
- **Binder**: `FpsListener` now routes its wake eventfd **per-instance**
through the callback binder's userdata (`AIBinder_getUserData`), matching
`TaskStackListener`. The old process-wide `FPS_EVENTFD` static was
overwritten on every `open()`, so a re-opened listener rewired an earlier
registration's `onFpsReported` wake into the newest eventfd (or dropped
it). Each instance now owns its own fd and `AIBinder_new` failure reclaims
it instead of leaking.
- **Binder**: `FpsListener` and `TaskStackListener` now **deregister
best-effort on drop**, so a dropped listener no longer leaves the framework
delivering `onFpsReported` / task-stack wakes forever. The local strong ref
on the callback binder is intentionally not released (safe-by-leak) so the
per-binder userdata can never be reclaimed while a callback is in flight.
## [1.2.31] - 2026-08-10
### Fixed
- **Binder**: `TaskStackListener` now routes its wake eventfd **per-instance**
through the callback binder's userdata instead of a process-wide
`TASK_STACK_EVENTFD` static. Previously every `open()` overwrote the
shared static, so when a single process hosted two listeners (the
foreground task source and the fps channel) all wake writes went to the
listener opened last and the other's eventfd never fired. Each listener now
owns its own fd, resolved in `on_transact` via `AIBinder_getUserData`, and
reclaimed in `on_destroy`; the `AIBinder_new` failure path no longer leaks.
## [1.2.30] - 2026-08-09
### Added
- **Binder**: New `TaskStackListener` — a push-based task-stack change wake-up
registered with `IActivityTaskManager.registerTaskStackListener`. The
`ITaskStackListener` server object signals an eventfd on any stack event
(`onTaskStackChanged`, `onTaskMovedToFront`, …) and parses **nothing**;
consumers re-query `getFocusedRootTaskInfo` (txn 31) for the authoritative
`(taskId, pkg)`. `dex::resolve_task_stack_codes` resolves
`TRANSACTION_registerTaskStackListener`/`unregisterTaskStackListener` for the
legacy `ITaskStackListener` interface — the newer
`ITaskChangeListener`/`registerTaskChangeListener` pair is absent on the
target ROM.
## [1.2.25] - 2026-08-09
### Added
- **DRM**: New `drm` module with `DrmCard::open` (card node by path) and
`DrmCard::wait_vblank` — a blocking `DRM_IOCTL_WAIT_VBLANK` wait that
returns the monotonic unblock instant. Reply timestamps are ignored because
`msm` display stacks zero `t_sec`/`t_usec`.
- **Reactor**: `Fd::dup` for owned descriptor duplication (fan-out eventfd
wakeups across threads).
## [1.1.2] - 2026-05-12
### Fixed
- **Spawn**: Removed a 1ms busy-wait loop that occurred after child process termination while waiting for final I/O drainage.
- **Signals**: Corrected `SignalRuntime::unblock_all` to use `pthread_sigmask` for thread-safe consistency.
- **Documentation**: Resolved duplicated documentation headers in the `unix_socket` module.
## [1.1.1] - 2026-05-12
### Fixed
- **Reactor**: Fixed a bug where `EPOLLERR` events were not folded into `readable`/`writable` flags, potentially causing hangs in callers that do not explicitly check `error`.
- **Spawn**: Fixed a potential hang in the process wait loop by explicitly handling `EPOLLHUP` (hangup) events.
- **Documentation**: Cleaned up redundant lines in `proc` module documentation.
## [1.1.0] - 2026-05-12
### Added
- **Reactor**: Exposed `add_with_flags` for custom epoll registration.
- **Unix Sockets**: Added `accept_timeout` for millisecond-based timeouts.
- **Unix Sockets**: Added `peer_cred` support for retrieving peer process identity.
- **Signals**: Added `register_handler` for arbitrary process-wide signal handlers.
- **Signals**: Added `signalfd_new` for reactor-compatible signal reception.
## [1.0.0] - 2026-05-12
### Added
- **Logging**: Refactored to a backend-agnostic facade with `Logger` instances.
- **Reactor**: Added `hangup` field to `Event`.
- **Documentation**: Comprehensive behavioral contract and architectural invariant documentation.
### Changed
- **Spawn**: `SpawnBackend` is now mandatory at builder construction.
- **Layering**: Renamed `blocklist_fingerprint` to `path_fingerprint`.
### Stability Contract
The 1.x series guarantees stability for the following behavioral contracts. Changes to these are considered **breaking changes**:
- **Reactor Events**: The mapping of epoll flags to `Event` fields (`EPOLLERR` -> `error`, `EPOLLHUP` -> `hangup`).
- **Reactor Timeouts**: The `-1`/`0`/`positive` millisecond contract in `Reactor::wait`.
- **Descriptor Defaults**: The use of `O_CLOEXEC` / `SOCK_CLOEXEC` for all Core-created descriptors.
- **Signal Compatibility**: The requirement to use `signalfd` (via `signalfd_new`) for reactor-compatible signal handling.
- **Policy Neutrality**: The architectural commitment to provide primitives without policy.
## [0.3.0] - 2026-05-11
### Added
- Added Unix socket peer credential support through `SO_PEERCRED`.
## [0.2.0] - 2026-05-10
### Added
- Added low-level mmap/madvise preload primitive with page-aligned offset validation.
## [0.1.0] - 2026-05-04
### Added
- Initial official CoreShift Core release.
- Primitive Linux/Android APIs for process spawning, process lifecycle, process
I/O draining, procfs parsing, filesystem helpers, UID/GID/path identity,
readahead, signals, inotify, epoll/reactor use, eventfd, timerfd, signalfd,
and Unix domain sockets.
- Explicit spawn backends: `Fork` and `PosixSpawn`.
- Explicit file descriptor inheritance policy through `SpawnFdPolicy`.
- Low-level abstract and pathname Unix stream socket primitives.
### Notes
- Core is policy-free and runs the exact argv it is given.
- Core does not choose shell, root, package, foreground, daemon, fallback, or
product behavior.
- Unsupported backend/option combinations return errors instead of selecting a
different backend.