astrid-runtime/astrid · error

named-pipe peer process changed during authentication

Error message

named-pipe peer process changed during authentication

What it means

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.

Source

Thrown at crates/astrid-core/src/local_transport/windows.rs:420

        ));
    }
    Ok(process_id)
}

struct VerifiedPeerProcess {
    process_id: u32,
    user_sid: OwnedSid,
    // Keeping the process object alive prevents its numeric PID from being
    // recycled while security validation runs. We still re-read the PID from
    // the pipe afterward; this is defense in depth beside the descriptor owner
    // check, not the server's effective-token authorization boundary.
    _process: OwnedHandle,
}

impl VerifiedPeerProcess {
    fn ensure_still_peer(&self, stream: &LocalStream) -> io::Result<()> {
        if peer_process_id(stream)? != self.process_id {
            return Err(io::Error::new(
                io::ErrorKind::PermissionDenied,
                "named-pipe peer process changed during authentication",
            ));
        }
        Ok(())
    }
}

fn peer_process_identity(stream: &LocalStream) -> io::Result<VerifiedPeerProcess> {
    let process_id = peer_process_id(stream)?;
    let process = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, process_id) };
    if process.is_null() {
        return Err(last_error("failed to open named-pipe peer process"));
    }
    let process = OwnedHandle(process);
    if unsafe { GetProcessId(process.0) } != process_id {
        return Err(io::Error::new(
            io::ErrorKind::PermissionDenied,

View on GitHub (pinned to affd8760f4)

Solutions

  1. Retry the whole connect/accept handshake from scratch — the identity must be re-verified for the new peer.
  2. Ensure clients hold the connection for the full auth lifetime (no close/reopen between auth steps).
  3. On the server, recreate the pipe instance after this error so a stale peer cannot reuse it.

Example fix

// before
match peer.ensure_still_peer(&stream) {
    Err(_) => return Err(err),
}
// after
match peer.ensure_still_peer(&stream) {
    Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
        drop(stream);
        return connect_fresh(&path); // re-run full authentication
    }
    Err(e) => return Err(e),
    Ok(()) => {}
}
Defensive patterns

Strategy: try-catch

Try / catch

if let Err(e) = peer.ensure_still_peer(&stream) {
    if e.kind() == io::ErrorKind::PermissionDenied {
        // peer changed mid-auth: restart the full handshake
        return authenticate_fresh(&path);
    }
    return Err(e);
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/2a2c81c92d0ee6ae. Report an issue: GitHub.