# 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).
## [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.