{"record":{"id":"2a2c81c92d0ee6ae","repo":"astrid-runtime/astrid","slug":"named-pipe-peer-process-changed-during-authenticat","errorCode":null,"errorMessage":"named-pipe peer process changed during authentication","messagePattern":"named-pipe peer process changed during authentication","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-core/src/local_transport/windows.rs","lineNumber":420,"sourceCode":"        ));\n    }\n    Ok(process_id)\n}\n\nstruct VerifiedPeerProcess {\n    process_id: u32,\n    user_sid: OwnedSid,\n    // Keeping the process object alive prevents its numeric PID from being\n    // recycled while security validation runs. We still re-read the PID from\n    // the pipe afterward; this is defense in depth beside the descriptor owner\n    // check, not the server's effective-token authorization boundary.\n    _process: OwnedHandle,\n}\n\nimpl VerifiedPeerProcess {\n    fn ensure_still_peer(&self, stream: &LocalStream) -> io::Result<()> {\n        if peer_process_id(stream)? != self.process_id {\n            return Err(io::Error::new(\n                io::ErrorKind::PermissionDenied,\n                \"named-pipe peer process changed during authentication\",\n            ));\n        }\n        Ok(())\n    }\n}\n\nfn peer_process_identity(stream: &LocalStream) -> io::Result<VerifiedPeerProcess> {\n    let process_id = peer_process_id(stream)?;\n    let process = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, process_id) };\n    if process.is_null() {\n        return Err(last_error(\"failed to open named-pipe peer process\"));\n    }\n    let process = OwnedHandle(process);\n    if unsafe { GetProcessId(process.0) } != process_id {\n        return Err(io::Error::new(\n            io::ErrorKind::PermissionDenied,","sourceCodeStart":402,"sourceCodeEnd":438,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-core/src/local_transport/windows.rs#L402-L438","documentation":"During authentication the library re-checks that the pipe's peer PID still matches the PID of the process that was verified earlier (VerifiedPeerProcess::ensure_still_peer). If GetNamedPipeClientProcessId now returns a different PID, the original authenticated process disconnected and a different process connected on the same pipe instance, so the prior identity verification is stale and the connection is rejected with PermissionDenied.","triggerScenarios":"A client process exits mid-handshake and another process opens the same pipe instance before the server finishes ensure_still_peer; racing reconnects against a permissive first-instance pipe.","commonSituations":"Aggressive client retry loops that reconnect immediately after a crash; service supervisors restarting workers that share one pipe server; TOCTOU-style hijack attempts on local IPC.","solutions":["Retry the whole connect/accept handshake from scratch — the identity must be re-verified for the new peer.","Ensure clients hold the connection for the full auth lifetime (no close/reopen between auth steps).","On the server, recreate the pipe instance after this error so a stale peer cannot reuse it."],"exampleFix":"// before\nmatch peer.ensure_still_peer(&stream) {\n    Err(_) => return Err(err),\n}\n// after\nmatch peer.ensure_still_peer(&stream) {\n    Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {\n        drop(stream);\n        return connect_fresh(&path); // re-run full authentication\n    }\n    Err(e) => return Err(e),\n    Ok(()) => {}\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if let Err(e) = peer.ensure_still_peer(&stream) {\n    if e.kind() == io::ErrorKind::PermissionDenied {\n        // peer changed mid-auth: restart the full handshake\n        return authenticate_fresh(&path);\n    }\n    return Err(e);\n}","preventionTips":["Never close/reopen the pipe between identity verification and use.","Avoid client retry loops that instantly reconnect on the same instance.","Run an integration test with a client that disconnects mid-handshake."],"tags":["windows","named-pipes","security","authentication","race-condition"],"backgroundTag":"permission-denied","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}