host/taint: record the open owner-union/boundedness interaction
Round 11 review, finding 1. Verified correct: round 10's claim that the key union was "strictly additive" was too strong. The same key list feeds owner_is_bounded, and the unresolved-owner sweep is triggered by an UNbounded tainted reader -- so adding the Client's PID can move a reader from unbounded to bounded and switch the sweep off, letting a same-process output leg with an ambiguous Client and a bogus self-claimed PID stay eligible. Cannot leak today (evaluate() is audit-only); becomes live in phase 6. Not fixed in this round, and the doc says why: the blunt repair -- only protected keys bound an owner -- makes every Pulse-emulated app unbounded, which re-triggers the mass over-exclusion the design exists to avoid and would empty the eligible half of the 5.1 matrix. The targeted rule (a node whose Client cannot be resolved at all is not bounded by its own self-claimed PID) is written down along with what it needs structurally, to be implemented with matrix data in hand rather than argued from a whiteboard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -256,6 +256,50 @@ fn keys_of(node: &NodeSnapshot, ctx: &OwnerCtx) -> Vec<(OwnerKey, KeyValue)> {
|
|||||||
/// nothing else relates them. Its sibling output leg cannot be found, so
|
/// nothing else relates them. Its sibling output leg cannot be found, so
|
||||||
/// the engine must fail closed rather than declare it clean
|
/// the engine must fail closed rather than declare it clean
|
||||||
/// (v3.4 §6.1.1, final paragraph).
|
/// (v3.4 §6.1.1, final paragraph).
|
||||||
|
///
|
||||||
|
/// # 🔴 OPEN, phase-6 blocking — the key union can *reduce* taint here
|
||||||
|
///
|
||||||
|
/// **Round 11 review, finding 1. Verified correct; deliberately not fixed in
|
||||||
|
/// that round.** Round 10 made key 4 a union of the node's
|
||||||
|
/// `application.process.id` and its Client's `pipewire.sec.pid`, and the claim
|
||||||
|
/// that this was "strictly additive" was too strong: the same key list also
|
||||||
|
/// feeds *this* predicate, so adding a value can move a node from unbounded to
|
||||||
|
/// bounded, and `propagate_unresolved_owner`'s global sweep is triggered by an
|
||||||
|
/// **un**bounded tainted reader. Concretely:
|
||||||
|
///
|
||||||
|
/// 1. A tainted reader's node claims the pipewire-pulse PID while its Client
|
||||||
|
/// holds a real protected PID `A`. Under `or_else` the node's value won and
|
||||||
|
/// exception 1 suppressed it, leaving the reader unbounded; under the union
|
||||||
|
/// it is bounded by `A`.
|
||||||
|
/// 2. Its process's output leg uses a second Client whose id is **ambiguous**
|
||||||
|
/// (the observer missed a removal), so no protected PID is available — but
|
||||||
|
/// the leg claims a bogus `application.process.id` `B`, which bounds it.
|
||||||
|
/// 3. Neither the bridge nor the sweep fires, and the output stays eligible
|
||||||
|
/// while re-emitting the call.
|
||||||
|
///
|
||||||
|
/// It cannot leak today: `evaluate()` is reached only by the dry-run audit,
|
||||||
|
/// which creates no links. It becomes live when phase 6 consumes eligibility.
|
||||||
|
///
|
||||||
|
/// **Why it is not fixed yet.** The principled repair is provenance: a
|
||||||
|
/// self-claimed `application.process.id` is not a *sound* bound, only the
|
||||||
|
/// protected keys are. But applying that bluntly makes every Pulse-emulated
|
||||||
|
/// app unbounded — their Client's `sec_pid` is the daemon's and suppressed, so
|
||||||
|
/// the node's own claim is their only per-app identity — which re-triggers the
|
||||||
|
/// §6.1.1 mass over-exclusion the whole design is built to avoid, and would
|
||||||
|
/// make the eligible half of the §5.1 matrix empty.
|
||||||
|
///
|
||||||
|
/// The targeted rule that closes the path above without that cost: **a node
|
||||||
|
/// whose Client cannot be resolved at all must not be bounded by its own
|
||||||
|
/// self-claimed PID.** An ambiguous Client already means "we do not know who
|
||||||
|
/// owns this", and a self-claim must not paper over it; a Pulse app's Client
|
||||||
|
/// *is* resolved (to the daemon's PID, then suppressed), so it keeps its
|
||||||
|
/// bound. Implementing it needs `OwnerCtx` to distinguish "resolved" from
|
||||||
|
/// "absent", and `OwnerKeyIndex` to carry boundedness separately from the key
|
||||||
|
/// set, since bridging must keep using the full union.
|
||||||
|
///
|
||||||
|
/// ⚠️ Do this **with the §5.1 matrix data in hand**, not before: the whole
|
||||||
|
/// question is how much over-exclusion the rule actually causes on a real
|
||||||
|
/// graph, and that is measurable rather than arguable.
|
||||||
pub fn owner_is_bounded(node: &NodeSnapshot, ctx: &OwnerCtx) -> bool {
|
pub fn owner_is_bounded(node: &NodeSnapshot, ctx: &OwnerCtx) -> bool {
|
||||||
keys_of(node, ctx)
|
keys_of(node, ctx)
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
Reference in New Issue
Block a user