astrid-runtime/astrid · error

Windows named-pipe endpoints are kernel-owned and cannot be

Error message

Windows named-pipe endpoints are kernel-owned and cannot be removed while live

What it means

On Windows, named-pipe server endpoints created via CreateNamedPipe are owned by the kernel, not the filesystem, so there is no file to unlink. remove_endpoint refuses to fake a delete and returns PermissionDenied when the endpoint still exists, succeeding only when it is already gone (e.g. after all handles were closed).

Source

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

        EndpointState::Available => Ok(()),
        EndpointState::Absent => Err(io::Error::new(
            io::ErrorKind::NotFound,
            "Windows named-pipe endpoint is absent",
        )),
        EndpointState::BusyOrDenied => Err(io::Error::new(
            io::ErrorKind::WouldBlock,
            "Windows named-pipe endpoint is occupied but unavailable",
        )),
    }
}

pub(super) fn endpoint_is_present(path: &Path) -> io::Result<bool> {
    Ok(!matches!(endpoint_state(path)?, EndpointState::Absent))
}

pub(super) fn remove_endpoint(path: &Path) -> io::Result<()> {
    if endpoint_is_present(path)? {
        return Err(io::Error::new(
            io::ErrorKind::PermissionDenied,
            "Windows named-pipe endpoints are kernel-owned and cannot be removed while live",
        ));
    }
    Ok(())
}

pub(super) fn remove_stale_endpoint(path: &Path) -> io::Result<bool> {
    // Unlike a Unix socket pathname, a named-pipe endpoint cannot remain stale
    // after its final server handle closes. Never delete or replace an object
    // merely because its DACL makes it inaccessible.
    let _ = endpoint_state(path)?;
    Ok(false)
}

pub(super) fn peer_is_current_user(stream: &LocalStream) -> io::Result<bool> {
    if matches!(&stream.inner, StreamInner::Server(_)) {
        return Ok(effective_client_user_sid(stream)?.equals(&current_user_sid()?));

View on GitHub (pinned to affd8760f4)

Solutions

  1. Drop (or close) the server-side stream/listener first so all pipe handles are released, then call remove_endpoint.
  2. If you only want cleanup, make the remove best-effort: ignore PermissionDenied since the kernel deletes the pipe automatically once the last handle closes.
  3. Restructure so remove_endpoint is called only after confirming endpoint_is_present returns false.

Example fix

// before
server.shutdown();
transport.remove_endpoint(&path)?;
// after
drop(server); // releases the kernel-owned pipe
if transport.endpoint_is_present(&path)? {
    transport.remove_endpoint(&path)?;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// check before attempting removal
if !endpoint_is_present(&path)? { /* nothing to remove; skip */ }

Type guard

fn can_remove(t: &Transport, p: &Path) -> bool {
    !t.endpoint_is_present(p).unwrap_or(true)
}

Try / catch

match transport.remove_endpoint(&path) {
    Ok(()) => {}
    Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
        // pipe still kernel-owned; it disappears when the last handle drops
        eprintln!("endpoint still live; skipping unlink");
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling remove_endpoint(path) while a bound NamedPipeServer (or any handle to the pipe instance) is still open; endpoint_state reports the pipe as present.

Common situations: Cleanup code in Drop or shutdown handlers that removes the endpoint file before/without dropping the server listener; ported Unix code that unlinks a Unix domain socket path on shutdown; leaked server handles keeping the pipe alive after the app 'finished'.

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/35cfcfd9ac381d04. Report an issue: GitHub.