feat(panel): inbound family gating and the mapping check
The receiving half of bilateral gating, plus the constructor fix review found. The eight G6/G7/G8 witness rows and the GPU side are next; the full eleven-stage `--protocol` gate runs once they discriminate. **THE FAMILY IS DECIDED FIRST, FROM THE AUTHENTICATED SESSION.** `peer_uses_mapped_panel_family` reads `session_state(source)`, never the payload's `frontend_id` --- that field is untrusted on every inbound variant, and looking negotiation up by it would let a peer claim another session's family. The order is stated at both arms: family, then the existing epoch ladder, then the mapping generation, then dispatch. It cannot depend on the variant's contents, because it decides which variant is admissible at all. Both wrong-family cases are REFUSALS, not fallbacks. A `>= v25` session sending the bare `PanelPointer` is dropped rather than handled under legacy semantics --- handling it would leave the mapping hole reachable by choosing a discriminant, which is the entire bypass. A `<= v24` session sending `PanelPointerMapped` is dropped too, even though a peer compiled from this crate can encode the discriminant: negotiation is a gate, not a sender convention. `panel_mapping_is_current` is the ladder's finest rung. `buffer_id` catches an A->B replacement, `panel_epoch` a close/reopen, `geometry_epoch` a declaration race, and this catches the text under the cell changing --- a foreign edit, a fold, a reload, none of which moves an epoch. Zero is refused outright, and the check reads through the same accessor projection stamps with, so the two cannot drift. **And `new()` contradicted its own contract.** It documents a current-build peer and enables every other current capability, but I initialised `peer_knows_mapped_panel` to `false` --- so an implicitly current peer was sent the LEGACY family. Set true, with the doc extended to say the assumption covers later capabilities too. **Four daemon rows broke, and that is G8a firing.** They drive `FrontendEvent::PanelPointer` through sessions negotiated at `PROTOCOL_VERSION`, which is now 25 --- so the bare variant is refused, correctly. They negotiate `LEGACY_PANEL_VERSION` now, which both fixes them and makes them explicit legacy positive controls rather than rows that happened to pass. Note for the eventual rebase: this branch is based on main, where `dispatch_semantic_panel_pointer` still takes four arguments. Threading `mods` is R-a, owned by `panel-pointer-replay`; the mapped arm destructures `mods` into `..` here and will pass it through when the lanes meet. Verified: focused suite 37/37, `cargo test --lib` 1945 green, clippy clean. **Amended.** The first version of this message claimed the lib suite green when it was not: I piped `cargo test` through `tail`, so the pipeline exited 0 and my `&&` chain committed on a failure I had not read. The four rows above are what it was reporting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
parent
7da2f34b94
commit
0fff640d15
137
src/daemon.rs
137
src/daemon.rs
|
|
@ -70,8 +70,8 @@ use crate::protocol::{
|
||||||
ADVERTISED_PROTOCOL_VERSION, AttachRequest, FrontendEvent, FrontendId, GoodbyeReason, Hello,
|
ADVERTISED_PROTOCOL_VERSION, AttachRequest, FrontendEvent, FrontendId, GoodbyeReason, Hello,
|
||||||
InitialTarget, InitialTargetResult, InstanceCapabilities, InstanceIdentity, InstanceMessage,
|
InitialTarget, InitialTargetResult, InstanceCapabilities, InstanceIdentity, InstanceMessage,
|
||||||
InstanceSignal, MAX_INITIAL_TARGET_ERROR_BYTES, MAX_INITIAL_TARGET_PATH_BYTES,
|
InstanceSignal, MAX_INITIAL_TARGET_ERROR_BYTES, MAX_INITIAL_TARGET_PATH_BYTES,
|
||||||
PANEL_MIN_VERSION, PointerKind, SelectionSnapshot, SessionBootstrapRequest,
|
PANEL_MAPPING_MIN_VERSION, PANEL_MIN_VERSION, PointerKind, SelectionSnapshot,
|
||||||
TEXT_INPUT_MAX_BYTES, TEXT_INPUT_MIN_VERSION,
|
SessionBootstrapRequest, TEXT_INPUT_MAX_BYTES, TEXT_INPUT_MIN_VERSION,
|
||||||
};
|
};
|
||||||
use crate::socket_path::{SocketPathError, ensure_runtime_subdir};
|
use crate::socket_path::{SocketPathError, ensure_runtime_subdir};
|
||||||
use crate::transport::{read_message, write_message};
|
use crate::transport::{read_message, write_message};
|
||||||
|
|
@ -1033,6 +1033,53 @@ fn panel_event_epochs_are_current(
|
||||||
/// The claimed `frontend_id` in the payload is never consulted anywhere:
|
/// The claimed `frontend_id` in the payload is never consulted anywhere:
|
||||||
/// routing is by the authenticated transport `source`, so a forged id
|
/// routing is by the authenticated transport `source`, so a forged id
|
||||||
/// addresses nothing.
|
/// addresses nothing.
|
||||||
|
/// §5b — which panel-pointer family this session speaks.
|
||||||
|
///
|
||||||
|
/// **Read from the AUTHENTICATED source, never from the payload's
|
||||||
|
/// `frontend_id`.** That field is untrusted on every inbound variant,
|
||||||
|
/// and looking the negotiation up by it would let a peer claim another
|
||||||
|
/// session's family — the forged cross-session claim G8e mutates for.
|
||||||
|
///
|
||||||
|
/// Consulted BEFORE the payload is trusted, before any generation is
|
||||||
|
/// validated and before any mutation: the family decides which variant
|
||||||
|
/// is even admissible, so it cannot depend on the variant's contents.
|
||||||
|
fn peer_uses_mapped_panel_family(session_registry: &SessionRegistry, source: FrontendId) -> bool {
|
||||||
|
session_registry
|
||||||
|
.session_state(source)
|
||||||
|
.is_some_and(|state| state.negotiated_protocol_version >= PANEL_MAPPING_MIN_VERSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// §5b — whether an echoed mapping generation still names the mapping
|
||||||
|
/// the daemon holds.
|
||||||
|
///
|
||||||
|
/// The last rung of the ladder and the finest: `buffer_id` catches an
|
||||||
|
/// A→B replacement, `panel_epoch` a close/reopen, `geometry_epoch` a
|
||||||
|
/// declaration race, and this catches **the text under that cell
|
||||||
|
/// changing** — a foreign edit, a fold, a reload, none of which moves
|
||||||
|
/// an epoch.
|
||||||
|
///
|
||||||
|
/// **Zero is refused outright.** It is what a default-constructed or
|
||||||
|
/// half-initialised sender produces, so accepting it would let a peer
|
||||||
|
/// opt out of the check by sending nothing.
|
||||||
|
///
|
||||||
|
/// Read through the SAME accessor projection stamps with, so "what the
|
||||||
|
/// frontend was shown" and "what the daemon checks" cannot drift.
|
||||||
|
fn panel_mapping_is_current(
|
||||||
|
editor: &EditorState,
|
||||||
|
semantic_states: &mut HashMap<FrontendId, crate::semantic_render::SemanticRenderState>,
|
||||||
|
source: FrontendId,
|
||||||
|
echoed: u64,
|
||||||
|
) -> bool {
|
||||||
|
if echoed == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let snapshot = editor.panel_mapping_snapshot(source);
|
||||||
|
semantic_states
|
||||||
|
.get_mut(&source)
|
||||||
|
.and_then(|state| state.panel_mapping_generation(snapshot))
|
||||||
|
.is_some_and(|current| current == echoed)
|
||||||
|
}
|
||||||
|
|
||||||
fn peer_may_send_panel_events(
|
fn peer_may_send_panel_events(
|
||||||
editor: &EditorState,
|
editor: &EditorState,
|
||||||
session_registry: &SessionRegistry,
|
session_registry: &SessionRegistry,
|
||||||
|
|
@ -2438,7 +2485,19 @@ fn handle_dispatcher_event(
|
||||||
// the daemon's own state inside the dispatcher. Any
|
// the daemon's own state inside the dispatcher. Any
|
||||||
// failure drops the event before any view, controller,
|
// failure drops the event before any view, controller,
|
||||||
// selection, menu, or PTY mutation.
|
// selection, menu, or PTY mutation.
|
||||||
if peer_may_send_panel_events(editor, session_registry, semantic_states, source)
|
// §5b G8a — the family is decided FIRST, from the
|
||||||
|
// AUTHENTICATED session, and a `>= v25` session
|
||||||
|
// sending the bare variant is REFUSED rather than
|
||||||
|
// handled under legacy semantics. Handling it would
|
||||||
|
// leave the mapping hole reachable by choosing a
|
||||||
|
// discriminant, which is the whole of the bypass.
|
||||||
|
if !peer_uses_mapped_panel_family(session_registry, source)
|
||||||
|
&& peer_may_send_panel_events(
|
||||||
|
editor,
|
||||||
|
session_registry,
|
||||||
|
semantic_states,
|
||||||
|
source,
|
||||||
|
)
|
||||||
&& panel_event_epochs_are_current(
|
&& panel_event_epochs_are_current(
|
||||||
editor,
|
editor,
|
||||||
semantic_states,
|
semantic_states,
|
||||||
|
|
@ -2450,6 +2509,50 @@ fn handle_dispatcher_event(
|
||||||
editor.dispatch_semantic_panel_pointer(source, buffer_id, coord, kind);
|
editor.dispatch_semantic_panel_pointer(source, buffer_id, coord, kind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
FrontendEvent::PanelPointerMapped {
|
||||||
|
geometry_epoch,
|
||||||
|
panel_epoch,
|
||||||
|
buffer_id,
|
||||||
|
coord,
|
||||||
|
kind,
|
||||||
|
mapping_generation,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
// §5b — the mapped family, in this order:
|
||||||
|
//
|
||||||
|
// 1. family, from the AUTHENTICATED session
|
||||||
|
// 2. the existing epoch ladder
|
||||||
|
// 3. the mapping generation
|
||||||
|
// 4. only then, dispatch
|
||||||
|
//
|
||||||
|
// G8c is the first line: a `<= v24` session sending
|
||||||
|
// this variant is refused even though a peer built
|
||||||
|
// from this crate can encode the discriminant.
|
||||||
|
// Negotiation is a gate, not a sender convention.
|
||||||
|
if peer_uses_mapped_panel_family(session_registry, source)
|
||||||
|
&& peer_may_send_panel_events(
|
||||||
|
editor,
|
||||||
|
session_registry,
|
||||||
|
semantic_states,
|
||||||
|
source,
|
||||||
|
)
|
||||||
|
&& panel_event_epochs_are_current(
|
||||||
|
editor,
|
||||||
|
semantic_states,
|
||||||
|
source,
|
||||||
|
geometry_epoch,
|
||||||
|
panel_epoch,
|
||||||
|
)
|
||||||
|
&& panel_mapping_is_current(
|
||||||
|
editor,
|
||||||
|
semantic_states,
|
||||||
|
source,
|
||||||
|
mapping_generation,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
editor.dispatch_semantic_panel_pointer(source, buffer_id, coord, kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
FrontendEvent::Pointer {
|
FrontendEvent::Pointer {
|
||||||
buffer_id,
|
buffer_id,
|
||||||
byte,
|
byte,
|
||||||
|
|
@ -5971,6 +6074,14 @@ mod tests {
|
||||||
///
|
///
|
||||||
/// The session is registered because the dispatcher drops any event
|
/// The session is registered because the dispatcher drops any event
|
||||||
/// from an uninstalled session before it reaches a handler.
|
/// from an uninstalled session before it reaches a handler.
|
||||||
|
/// §5b — a session that negotiated the LEGACY panel family.
|
||||||
|
///
|
||||||
|
/// These rows drive `FrontendEvent::PanelPointer`, which a `>= v25`
|
||||||
|
/// session may not send at all, so they must say which family they
|
||||||
|
/// are exercising. Naming it here also makes them explicit legacy
|
||||||
|
/// positive controls rather than tests that happened to pass.
|
||||||
|
const LEGACY_PANEL_VERSION: u32 = PANEL_MAPPING_MIN_VERSION - 1;
|
||||||
|
|
||||||
fn dispatch_panel_event(
|
fn dispatch_panel_event(
|
||||||
editor: &mut crate::editor::EditorState,
|
editor: &mut crate::editor::EditorState,
|
||||||
fid: FrontendId,
|
fid: FrontendId,
|
||||||
|
|
@ -6271,7 +6382,7 @@ mod tests {
|
||||||
dispatch_panel_event(
|
dispatch_panel_event(
|
||||||
&mut editor,
|
&mut editor,
|
||||||
fid,
|
fid,
|
||||||
PROTOCOL_VERSION,
|
LEGACY_PANEL_VERSION,
|
||||||
&mut semantic_states,
|
&mut semantic_states,
|
||||||
&mut HashMap::new(),
|
&mut HashMap::new(),
|
||||||
event,
|
event,
|
||||||
|
|
@ -6286,7 +6397,7 @@ mod tests {
|
||||||
dispatch_panel_event(
|
dispatch_panel_event(
|
||||||
&mut editor,
|
&mut editor,
|
||||||
fid,
|
fid,
|
||||||
PROTOCOL_VERSION,
|
LEGACY_PANEL_VERSION,
|
||||||
&mut semantic_states,
|
&mut semantic_states,
|
||||||
&mut HashMap::new(),
|
&mut HashMap::new(),
|
||||||
down(geometry_epoch, panel_epoch),
|
down(geometry_epoch, panel_epoch),
|
||||||
|
|
@ -6342,7 +6453,7 @@ mod tests {
|
||||||
dispatch_panel_event(
|
dispatch_panel_event(
|
||||||
&mut editor,
|
&mut editor,
|
||||||
fid,
|
fid,
|
||||||
PROTOCOL_VERSION,
|
LEGACY_PANEL_VERSION,
|
||||||
&mut semantic_states,
|
&mut semantic_states,
|
||||||
&mut HashMap::new(),
|
&mut HashMap::new(),
|
||||||
FrontendEvent::PanelPointer {
|
FrontendEvent::PanelPointer {
|
||||||
|
|
@ -6369,7 +6480,7 @@ mod tests {
|
||||||
dispatch_panel_event(
|
dispatch_panel_event(
|
||||||
&mut editor,
|
&mut editor,
|
||||||
fid,
|
fid,
|
||||||
PROTOCOL_VERSION,
|
LEGACY_PANEL_VERSION,
|
||||||
&mut semantic_states,
|
&mut semantic_states,
|
||||||
&mut HashMap::new(),
|
&mut HashMap::new(),
|
||||||
FrontendEvent::PanelPointer {
|
FrontendEvent::PanelPointer {
|
||||||
|
|
@ -6587,7 +6698,7 @@ mod tests {
|
||||||
dispatch_panel_event(
|
dispatch_panel_event(
|
||||||
&mut editor,
|
&mut editor,
|
||||||
fid,
|
fid,
|
||||||
PROTOCOL_VERSION,
|
LEGACY_PANEL_VERSION,
|
||||||
&mut semantic_states,
|
&mut semantic_states,
|
||||||
&mut HashMap::new(),
|
&mut HashMap::new(),
|
||||||
FrontendEvent::PanelPointer {
|
FrontendEvent::PanelPointer {
|
||||||
|
|
@ -6614,7 +6725,7 @@ mod tests {
|
||||||
dispatch_panel_event(
|
dispatch_panel_event(
|
||||||
&mut editor,
|
&mut editor,
|
||||||
fid,
|
fid,
|
||||||
PROTOCOL_VERSION,
|
LEGACY_PANEL_VERSION,
|
||||||
&mut semantic_states,
|
&mut semantic_states,
|
||||||
&mut HashMap::new(),
|
&mut HashMap::new(),
|
||||||
FrontendEvent::PanelResizeRows {
|
FrontendEvent::PanelResizeRows {
|
||||||
|
|
@ -6643,7 +6754,7 @@ mod tests {
|
||||||
dispatch_panel_event(
|
dispatch_panel_event(
|
||||||
&mut editor,
|
&mut editor,
|
||||||
fid,
|
fid,
|
||||||
PROTOCOL_VERSION,
|
LEGACY_PANEL_VERSION,
|
||||||
&mut semantic_states,
|
&mut semantic_states,
|
||||||
&mut HashMap::new(),
|
&mut HashMap::new(),
|
||||||
FrontendEvent::PanelPointer {
|
FrontendEvent::PanelPointer {
|
||||||
|
|
@ -6700,7 +6811,7 @@ mod tests {
|
||||||
dispatch_panel_event(
|
dispatch_panel_event(
|
||||||
&mut editor,
|
&mut editor,
|
||||||
fid,
|
fid,
|
||||||
PROTOCOL_VERSION,
|
LEGACY_PANEL_VERSION,
|
||||||
&mut semantic_states,
|
&mut semantic_states,
|
||||||
&mut HashMap::new(),
|
&mut HashMap::new(),
|
||||||
wheel(buffer_id, geometry_epoch, panel_epoch),
|
wheel(buffer_id, geometry_epoch, panel_epoch),
|
||||||
|
|
@ -6716,7 +6827,7 @@ mod tests {
|
||||||
dispatch_panel_event(
|
dispatch_panel_event(
|
||||||
&mut editor,
|
&mut editor,
|
||||||
fid,
|
fid,
|
||||||
PROTOCOL_VERSION,
|
LEGACY_PANEL_VERSION,
|
||||||
&mut semantic_states,
|
&mut semantic_states,
|
||||||
&mut HashMap::new(),
|
&mut HashMap::new(),
|
||||||
FrontendEvent::PanelPointer {
|
FrontendEvent::PanelPointer {
|
||||||
|
|
@ -6782,7 +6893,7 @@ mod tests {
|
||||||
dispatch_panel_event(
|
dispatch_panel_event(
|
||||||
&mut editor,
|
&mut editor,
|
||||||
fid,
|
fid,
|
||||||
PROTOCOL_VERSION,
|
LEGACY_PANEL_VERSION,
|
||||||
&mut semantic_states,
|
&mut semantic_states,
|
||||||
&mut HashMap::new(),
|
&mut HashMap::new(),
|
||||||
wheel(terminal_buffer, geometry_epoch, panel_epoch),
|
wheel(terminal_buffer, geometry_epoch, panel_epoch),
|
||||||
|
|
|
||||||
|
|
@ -551,7 +551,9 @@ impl SemanticRenderState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fresh session state for frontend `frontend_id`: no viewport
|
/// Fresh session state for frontend `frontend_id`: no viewport
|
||||||
/// declared, nothing sent. Assumes a current-build peer (>= 18);
|
/// declared, nothing sent. Assumes a current-build peer (>= 18, and
|
||||||
|
/// current for every later capability too, including §5b's mapped
|
||||||
|
/// panel family);
|
||||||
/// daemon sessions with a real negotiated version use
|
/// daemon sessions with a real negotiated version use
|
||||||
/// [`Self::for_peer`].
|
/// [`Self::for_peer`].
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
@ -567,7 +569,10 @@ impl SemanticRenderState {
|
||||||
last_menu_prompt: HashMap::new(),
|
last_menu_prompt: HashMap::new(),
|
||||||
last_minibuffer: None,
|
last_minibuffer: None,
|
||||||
peer_knows_minibuffer_rows: true,
|
peer_knows_minibuffer_rows: true,
|
||||||
peer_knows_mapped_panel: false,
|
// A current-build peer, like every other capability here.
|
||||||
|
// Leaving this `false` made `new()` contradict its own doc
|
||||||
|
// and emit the LEGACY family to an implicitly v25 peer.
|
||||||
|
peer_knows_mapped_panel: true,
|
||||||
last_completion_popup: HashMap::new(),
|
last_completion_popup: HashMap::new(),
|
||||||
last_summary: HashMap::new(),
|
last_summary: HashMap::new(),
|
||||||
last_status: HashMap::new(),
|
last_status: HashMap::new(),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue