Skip to content

Atomically bind Unix socket to prevent race#102

Merged
leynos merged 5 commits intomainfrom
codex/fix-race-condition-in-unix-socket-handling
Sep 17, 2025
Merged

Atomically bind Unix socket to prevent race#102
leynos merged 5 commits intomainfrom
codex/fix-race-condition-in-unix-socket-handling

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Sep 14, 2025

Summary

  • use an atomic temp-path rename when binding the Unix socket
  • test that concurrent file creation cannot hijack the socket

Testing

  • make fmt
  • make lint
  • make test

closes #6


https://chatgpt.com/codex/tasks/task_e_68c6b288590c8322903d7378ac95afcf

Summary by Sourcery

Use an atomic temporary socket rename when binding a Unix domain socket to eliminate race conditions and add a test that simulates concurrent file creation to verify the fix.

Bug Fixes:

  • Prevent pre-bind race where an attacker could hijack the socket path by removing the stale direct removal and bind approach

Enhancements:

  • Atomically bind the listener by creating a temporary socket with unique name, setting permissions, and renaming it into place

Documentation:

  • Update doc comments to describe the atomic replace behavior when binding the socket

Tests:

  • Add a multithreaded test that repeatedly attempts to create the socket file concurrently to ensure the atomic bind cannot be hijacked

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Sep 14, 2025

Reviewer's Guide

prepare_listener now atomically binds the Unix socket by creating a uniquely named temporary socket file, setting its permissions, and renaming it into place, and introduces a concurrent-race test to ensure no attacker file creation can hijack the socket path.

Sequence diagram for atomic Unix socket binding in prepare_listener

sequenceDiagram
    participant Caller
    participant "prepare_listener()"
    participant "stdfs"
    participant "UnixListener"
    Caller->>"prepare_listener()": call with socket path
    "prepare_listener()"->>"stdfs": get parent directory
    "prepare_listener()"->>"prepare_listener()": generate unique temp socket path
    "prepare_listener()"->>"UnixListener": bind to temp socket
    "prepare_listener()"->>"stdfs": set permissions on temp socket
    "prepare_listener()"->>"stdfs": rename temp socket to target path
    "prepare_listener()"->>Caller: return listener
Loading

Class diagram for updated prepare_listener and test module

classDiagram
    class prepare_listener {
        +prepare_listener(path: &Path) Result<UnixListener>
        -atomic temp-path creation
        -bind to temp socket
        -set permissions
        -rename into place
    }
    class tests {
        +prepare_listener_prevents_pre_bind_race()
        -spawns attacker thread
        -calls prepare_listener
        -verifies socket file type
    }
    prepare_listener <.. tests: uses
Loading

File-Level Changes

Change Details Files
Implement atomic socket bind via temporary file and rename
  • Derive parent directory and generate unique temp filename with PID and timestamp
  • Bind UnixListener to temporary file instead of target path
  • Set permissions (0o660) on the temp socket
  • Rename temp file into place atomically and clean up on error
crates/comenqd/src/listener.rs
Add test to prevent pre-bind race on socket creation
  • Spawn attacker thread repeatedly creating a file at socket path
  • Invoke prepare_listener while attacker runs
  • Verify resulting file is a socket
crates/comenqd/src/listener.rs

Assessment against linked issues

Issue Objective Addressed Explanation
#6 Investigate the current socket file removal and binding logic.
#6 Implement a solution to prevent race conditions at the socket file path using atomic operations or file locking.
#6 Add tests to verify that the race condition is resolved.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 14, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability of socket creation by using an atomic replace strategy, preventing race conditions and stale sockets on restart.
    • Ensures correct socket permissions (660) after creation.
    • Clearer error reporting during startup for easier troubleshooting.
  • Tests

    • Added a unit test to verify atomic socket creation under concurrent conditions.
  • Documentation

    • Updated comments to clarify the socket creation and replacement strategy.
  • Chores

    • Added a UUID dependency to support robust temporary path generation.

Walkthrough

Rewrite prepare_listener to bind to a UUID-named temporary Unix socket in the target directory, set permissions to 0o660, atomically rename the temp socket into place, remove pre-bind unlink logic, add contextual error handling and a unit test that races destination creation and verifies the final path is a socket.

Changes

Cohort / File(s) Summary
Listener implementation & tests
crates/comenqd/src/listener.rs
Replace remove-then-bind with atomic temp-socket workflow: derive parent dir and UUID temp name, bind to temp with contextual errors, chmod 0o660, atomically rename temp → final, best-effort cleanup on rename failure; update docs and add unit test validating race-free socket creation.
Workspace dependency
Cargo.toml
Add uuid = { version = "1", features = ["v4"] } under [workspace.dependencies].
Crate dependency
crates/comenqd/Cargo.toml
Add uuid = { workspace = true, features = ["v4"] } to crate dependencies.
Public re-exports
crates/comenqd/src/daemon.rs
Reorder and preserve manual ordering of listener re-exports to pub use crate::listener::{ handle_client, prepare_listener, run_listener, CLIENT_READ_TIMEOUT_SECS, MAX_REQUEST_BYTES }; with #[rustfmt::skip] and a comment.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Proc as Process
  participant OS as OS/Unix
  participant FS as Filesystem

  Note over Proc,OS: prepare_listener(path)

  Proc->>Proc: derive parent dir and temp name (orig + UUID)
  Proc->>OS: bind UnixListener at temp path
  OS-->>Proc: listener(temp)
  Proc->>OS: chmod(temp, 0o660)
  OS-->>Proc: ok
  Proc->>FS: rename(temp -> final) [atomic]
  alt rename ok
    FS-->>Proc: ok
    Note right of Proc: listener reachable at final path
    Proc-->>Proc: return listener
  else rename fails
    FS-->>Proc: error
    Proc->>FS: unlink temp (best-effort)
    Proc-->>Proc: propagate error
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

Forge a temp with UUID spark,
Bind the socket in the dark.
Chmod, then rename—swift embrace,
No more races in that place.
Tests cheer loud; the socket stays.

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title Check ✅ Passed Confirm the title "Atomically bind Unix socket to prevent race" concisely and accurately summarises the primary change (use of an atomic temporary-socket rename to eliminate the pre-bind race), is specific and free of noise.
Linked Issues Check ✅ Passed Confirm the PR fulfils the coding objectives from linked issue [#6] by replacing the remove-and-bind flow with an atomic temporary-socket creation and atomic rename, updating documentation, adding a multithreaded test that simulates concurrent creation, and adding the uuid dependency to generate unique temp names. Note that CI comments reported an unresolved import for CLIENT_READ_TIMEOUT_SECS and MAX_REQUEST_BYTES; verify the daemon::listener re-exports include those constants so tests import correctly.
Out of Scope Changes Check ✅ Passed Confirm no out-of-scope changes were introduced: the uuid workspace dependency supports the new UUID-based temp-path strategy, the re-export ordering and rustfmt::skip are innocuous and related, and no unrelated functional changes appear in the provided summaries.
Description Check ✅ Passed Confirm the PR description directly relates to the changeset by documenting the atomic temp-path rename, the added concurrency test, the testing commands, and the intent to close issue #6, so it is acceptable.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/fix-race-condition-in-unix-socket-handling

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and found some issues that need to be addressed.

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `crates/comenqd/src/listener.rs:49` </location>
<code_context>
+    let tmp = parent.join(unique);
+    let listener = UnixListener::bind(&tmp)?;
+    stdfs::set_permissions(&tmp, stdfs::Permissions::from_mode(0o660))?;
+    stdfs::rename(&tmp, path).inspect_err(|_| {
+        let _ = stdfs::remove_file(&tmp);
+    })?;
     Ok(listener)
</code_context>

<issue_to_address>
Error handling during rename could be more robust.

Consider handling or logging errors from remove_file to avoid leaving orphaned socket files if rename fails.

Suggested implementation:

```rust
    stdfs::rename(&tmp, path).inspect_err(|_| {
        if let Err(e) = stdfs::remove_file(&tmp) {
            log::error!("Failed to remove orphaned socket file {}: {}", tmp.display(), e);
        }
    })?;

```

If the `log` crate is not already included in your dependencies, add it to your `Cargo.toml`:

```
log = "0.4"
```

Also, ensure that logging is initialized in your application (e.g., with `env_logger` or similar) so that error messages are actually output.
</issue_to_address>

### Comment 2
<location> `crates/comenqd/src/listener.rs:48` </location>
<code_context>
+    );
+    let tmp = parent.join(unique);
+    let listener = UnixListener::bind(&tmp)?;
+    stdfs::set_permissions(&tmp, stdfs::Permissions::from_mode(0o660))?;
+    stdfs::rename(&tmp, path).inspect_err(|_| {
+        let _ = stdfs::remove_file(&tmp);
</code_context>

<issue_to_address>
Setting permissions before renaming may not guarantee final socket permissions.

Set permissions on the final socket path after renaming to ensure the correct mode is applied.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
    let listener = UnixListener::bind(&tmp)?;
    stdfs::set_permissions(&tmp, stdfs::Permissions::from_mode(0o660))?;
    stdfs::rename(&tmp, path).inspect_err(|_| {
        let _ = stdfs::remove_file(&tmp);
    })?;
    Ok(listener)
=======
    let listener = UnixListener::bind(&tmp)?;
    stdfs::rename(&tmp, path).inspect_err(|_| {
        let _ = stdfs::remove_file(&tmp);
    })?;
    stdfs::set_permissions(&path, stdfs::Permissions::from_mode(0o660))?;
    Ok(listener)
>>>>>>> REPLACE

</suggested_fix>

### Comment 3
<location> `crates/comenqd/src/listener.rs:38` </location>
<code_context>
+    let parent = path
+        .parent()
+        .ok_or_else(|| anyhow::anyhow!("socket path missing parent"))?;
+    let unique = format!(
+        ".{}.{}.{}",
+        path.file_name().unwrap().to_string_lossy(),
</code_context>

<issue_to_address>
Consider using a UUID for the temporary socket name and simplifying error handling to reduce boilerplate and improve clarity.

Here’s a much shorter version that keeps atomic replace, drops all of the PID/timestamp boilerplate, and still cleans up on error. It uses `uuid` for a randomized suffix instead of rolling your own:

```rust
use uuid::Uuid;
use anyhow::Context;
use std::{fs, os::unix::fs::PermissionsExt};

pub fn prepare_listener(path: &Path) -> Result<UnixListener> {
    let parent = path
        .parent()
        .with_context(|| "socket path missing parent")?;

    // generate a random “.filename.UUID” temp name
    let tmp = parent.join(format!(
        ".{}.{}",
        path.file_name().unwrap().to_string_lossy(),
        Uuid::new_v4()
    ));

    // bind, set perms, then rename into place
    let listener = UnixListener::bind(&tmp)
        .with_context(|| format!("binding to temp socket {:?}", tmp))?;
    fs::set_permissions(&tmp, fs::Permissions::from_mode(0o660))
        .with_context(|| format!("setting perms on {:?}", tmp))?;

    if let Err(e) = fs::rename(&tmp, path) {
        let _ = fs::remove_file(&tmp);
        return Err(e).context("renaming socket into place");
    }

    Ok(listener)
}
```

What this does:

 1. Drops manual PID/timestamp formatting—UUIDs give you unique names with zero effort.  
 2. Collapses the `inspect_err` into a simple `if let Err(...)` so it’s obvious we clean up on failure.  
 3. Adds only a few lines of error‐context to keep debugging just as straightforward.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread crates/comenqd/src/listener.rs Outdated
Comment thread crates/comenqd/src/listener.rs Outdated
Comment thread crates/comenqd/src/listener.rs Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4bb6f73 and 1e64d02.

📒 Files selected for processing (1)
  • crates/comenqd/src/listener.rs (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Clippy warnings MUST be disallowed.
Fix any warnings emitted during tests in the code itself rather than silencing them.
Where a function is too long, extract meaningfully named helper functions adhering to separation of concerns and CQRS.
Where a function has too many parameters, group related parameters in meaningfully named structs.
Where a function is returning a large error consider using Arc to reduce the amount of data returned.
Write unit and behavioural tests for new functionality. Run both before and after making any change.
Every module must begin with a module level (//! ) comment explaining the module's purpose and utility.
Document public APIs using Rustdoc comments (///) so documentation can be generated with cargo doc.
Prefer immutable data and avoid unnecessary mut bindings.
Handle errors with the Result type instead of panicking where feasible.
Avoid unsafe code unless absolutely necessary and document any usage clearly.
Place function attributes after doc comments.
Do not use return in single-line functions.
Use predicate functions for conditional criteria with more than two branches.
Lints must not be silenced except as a last resort.
Lint rule suppressions must be tightly scoped and include a clear reason.
Prefer expect over allow.
Use rstest fixtures for shared setup.
Replace duplicated tests with #[rstest(...)] parameterised cases.
Prefer mockall for mocks/stubs.
Prefer .expect() over .unwrap().
Use concat!() to combine long string literals rather than escaping newlines with a backslash.
Prefer semantic error enums. Derive std::error::Error (via the thiserror crate) for any condition the caller might inspect, retry, or map to an HTTP status.
Use an opaque error only at the app boundary. Use eyre::Report for human-readable logs; these should not be exposed in public APIs.
Never export the opaque type from a library. Convert to domain enums at API boundaries, and to eyre only in the main main() entrypoint or top-level async task.

Files:

  • crates/comenqd/src/listener.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cyclomatic complexity of functions no more than 12.

  • Adhere to single responsibility and CQRS

  • Place function attributes after doc comments.

  • Do not use return in single-line functions.

  • Move conditionals with >2 branches into a predicate function.

  • Avoid unsafe unless absolutely necessary.

  • Every module must begin with a //! doc comment that explains the module's purpose and utility.

  • Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar

  • Lints must not be silenced except as a last resort.

    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.

  • Use rstest fixtures for shared setup and to avoid repetition between tests.

  • Replace duplicated tests with #[rstest(...)] parameterised cases.

  • Prefer mockall for mocks/stubs.

  • Prefer .expect() over .unwrap()

  • Ensure that any API or behavioural changes are reflected in the documentation in docs/

  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/

  • Files must not exceed 400 lines in length

    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such

    • For testing of functionality depending upon environment variables, dependency injection and the mockable crate are the preferred option.
    • If mockable cannot be used, env mutations in tests ...

Files:

  • crates/comenqd/src/listener.rs
🪛 GitHub Check: build-test
crates/comenqd/src/listener.rs

[failure] 175-175:
no method named is_socket found for struct FileType in the current scope

🪛 GitHub Actions: CI
crates/comenqd/src/listener.rs

[error] 175-175: Rust compile error (E0599): no method named is_socket found for struct FileType in the current scope. Location crates/comenqd/src/listener.rs:175. Command: cargo llvm-cov --workspace --summary-only --lcov --output-path lcov.info

🔍 Remote MCP Ref

Summary — additional repo/PR facts relevant for review

  • Change summary (implementation): prepare_listener now binds a UnixListener to a uniquely-named temporary socket in the same parent directory, sets permissions to 0o660, then atomically renames the temporary socket into the final path; on rename failure the temporary socket is removed and the error is returned. Signature unchanged.

  • Rationale: this replaces the prior remove-then-bind sequence to eliminate the window where another process could create or hijack the final socket path.

  • Tests added: a unit test spawns concurrent attempts to create the destination while prepare_listener runs and asserts the final path is a socket (intended to verify the absence of hijack). Review the test for determinism/OS-specific assumptions.

  • Important reviewer notes:

    • Temp file is created inside the socket's parent directory (avoids cross-filesystem rename issues in normal cases). Rename semantics will atomically replace an existing destination; permission or other rename errors are cleaned up and propagated by the function.
    • Consider verifying behavior on target platforms/CI (permissions, selinux/AppArmor, or other filesystem edge-cases) and whether tests run reliably under heavy concurrency.

Sources consulted

  • PR #102 (files/description).
  • Modified file (feature branch) — crates/comenqd/src/listener.rs.
  • Original file (main branch) — crates/comenqd/src/listener.rs.
  • Linked issue #6 (race-condition context).
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (2)
crates/comenqd/src/listener.rs (2)

22-23: Docs: Correctly state the atomic rename approach.

Retain this; it matches the implementation and closes the remove→bind race.


47-52: Atomic rename + cleanup path looks good.

Binding at a tmp path, fixing permissions, then atomically renaming into place is the right shape. Cleanup on rename error is adequate; RAII will close the unlinked socket on early return.

Comment thread crates/comenqd/src/listener.rs Outdated
Comment thread crates/comenqd/src/listener.rs
Comment thread crates/comenqd/src/listener.rs
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9123abb and 15895a2.

📒 Files selected for processing (1)
  • crates/comenqd/src/listener.rs (4 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Clippy warnings MUST be disallowed.
Fix any warnings emitted during tests in the code itself rather than silencing them.
Where a function is too long, extract meaningfully named helper functions adhering to separation of concerns and CQRS.
Where a function has too many parameters, group related parameters in meaningfully named structs.
Where a function is returning a large error consider using Arc to reduce the amount of data returned.
Write unit and behavioural tests for new functionality. Run both before and after making any change.
Every module must begin with a module level (//! ) comment explaining the module's purpose and utility.
Document public APIs using Rustdoc comments (///) so documentation can be generated with cargo doc.
Prefer immutable data and avoid unnecessary mut bindings.
Handle errors with the Result type instead of panicking where feasible.
Avoid unsafe code unless absolutely necessary and document any usage clearly.
Place function attributes after doc comments.
Do not use return in single-line functions.
Use predicate functions for conditional criteria with more than two branches.
Lints must not be silenced except as a last resort.
Lint rule suppressions must be tightly scoped and include a clear reason.
Prefer expect over allow.
Use rstest fixtures for shared setup.
Replace duplicated tests with #[rstest(...)] parameterised cases.
Prefer mockall for mocks/stubs.
Prefer .expect() over .unwrap().
Use concat!() to combine long string literals rather than escaping newlines with a backslash.
Prefer semantic error enums. Derive std::error::Error (via the thiserror crate) for any condition the caller might inspect, retry, or map to an HTTP status.
Use an opaque error only at the app boundary. Use eyre::Report for human-readable logs; these should not be exposed in public APIs.
Never export the opaque type from a library. Convert to domain enums at API boundaries, and to eyre only in the main main() entrypoint or top-level async task.

Files:

  • crates/comenqd/src/listener.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cyclomatic complexity of functions no more than 12.

  • Adhere to single responsibility and CQRS

  • Place function attributes after doc comments.

  • Do not use return in single-line functions.

  • Move conditionals with >2 branches into a predicate function.

  • Avoid unsafe unless absolutely necessary.

  • Every module must begin with a //! doc comment that explains the module's purpose and utility.

  • Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar

  • Lints must not be silenced except as a last resort.

    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.

  • Use rstest fixtures for shared setup and to avoid repetition between tests.

  • Replace duplicated tests with #[rstest(...)] parameterised cases.

  • Prefer mockall for mocks/stubs.

  • Prefer .expect() over .unwrap()

  • Ensure that any API or behavioural changes are reflected in the documentation in docs/

  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/

  • Files must not exceed 400 lines in length

    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such

    • For testing of functionality depending upon environment variables, dependency injection and the mockable crate are the preferred option.
    • If mockable cannot be used, env mutations in tests ...

Files:

  • crates/comenqd/src/listener.rs
🔍 Remote MCP Ref

Summary of additional review-relevant facts (concise)

  • Change is focused on atomic replacement when creating the Unix domain socket: create & bind to a temporary socket file in the destination directory (using a UUID-based name), chmod it to 0o660, then atomically rename it into place; old remove-then-bind flow was removed. This eliminates the window where an attacker/process could create a file at the socket path.

  • A multithreaded test was added that repeatedly races concurrent creation at the destination path against prepare_listener and asserts the final path is a socket. Check test robustness for CI flakiness (timeouts, retries, platform differences).

  • Dependency changes:

    • Workspace Cargo.toml: adds uuid = { version = "1", features = ["v4"] }.
    • crates/comenqd/Cargo.toml: adds uuid = { workspace = true, features = ["v4"] }.
      Confirm workspace policy and lockfile effects; ensure CI updates lockfile and that adding uuid is acceptable.
  • Review risk/verification checklist (concrete items to verify in review):

    1. Temp file placement: ensure temp socket is created in the same parent directory as the final path (so rename is atomic on the same filesystem). Verify corner cases (path is root, relative paths, symlinked parent directories).
    2. Error/cleanup paths: verify all failure branches attempt cleanup of the temp socket and that concurrent unlink/rename races don't leave stale files. Confirm failures surface helpful context (with_context used).
    3. Permissions & security: confirm chmod to 0o660 is applied after rename and that this behavior is acceptable under target OS policies (SELinux/AppArmor, ACLs). Consider whether umask or directory permissions could affect final socket permissions.
    4. UUID uniqueness and predictability: using uuid v4 is sufficient to avoid predictable collisions; confirm no information leak from temp name in logs.
    5. Tests: run on CI OS matrix (Linux variants, macOS if supported) to ensure rename semantics and socket behavior are consistent. Consider making the test less timing-sensitive if flakes are observed.

Citations of sources/tools used

  • PR page: #102
  • Search for related code/PR context (attempted)
🔇 Additional comments (4)
crates/comenqd/src/listener.rs (4)

7-7: Imports: good additions

Import Context and Uuid to improve error messages and generate unique temp names. LGTM.

Also applies to: 17-17


36-44: Avoid panics on path parts: LGTM

Guard parent() and file_name() without unwrap(). This matches the repo’s error‑handling guidance.


153-187: Race test: sound and targeted

Importing FileTypeExt and using symlink_metadata avoids false positives; yielding in the attacker loop reduces CPU burn. LGTM.


45-47: Call out gotcha: local_addr() may report the temp path

After the rename, getsockname/local_addr() can still return the original temp pathname on some Unix platforms; verify no code depends on local_addr().as_pathname() equalling config.socket_path.

Run this locally to search for risky usages:

rg -n -C2 --hidden -g '!.git' -g '**/*.rs' -e 'local_addr\s*\(' -e '\.as_pathname\b' || git grep -n -E -e 'local_addr\s*\(' -e '\.as_pathname\b' -- '*.rs' || true

If matches exist, update comparisons to use canonicalised paths (std::fs::canonicalize), compare inode/dev, or validate against the final socket path after rename.

Comment thread crates/comenqd/src/listener.rs Outdated
Comment thread crates/comenqd/src/listener.rs Outdated
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Sep 16, 2025

@coderabbitai Please suggest a fix for the issue causing the following lint violation and supply a prompt for an AI coding agent to enable it to apply the fix:

Run leynos/shared-actions/.github/actions/generate-coverage@f9f1c863c8a5bef64aa6779caa746e1a4a6c1ad4
Run astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc
Trying to find version for uv in: /home/runner/work/comenq/comenq/uv.toml
Could not find file: /home/runner/work/comenq/comenq/uv.toml
Trying to find version for uv in: /home/runner/work/comenq/comenq/pyproject.toml
Could not find file: /home/runner/work/comenq/comenq/pyproject.toml
Could not determine uv version from uv.toml or pyproject.toml. Falling back to latest.
Found uv in tool-cache for 0.8.17
Added /home/runner/.local/bin to the path
Added /opt/hostedtoolcache/uv/0.8.17/x86_64 to the path
Set UV_CACHE_DIR to /home/runner/work/_temp/setup-uv-cache
Successfully installed uv version 0.8.17
Searching files using cache dependency glob: /home/runner/work/comenq/comenq/**/pyproject.toml,/home/runner/work/comenq/comenq/**/uv.lock
No matches found for glob
Warning: No file matched to [/home/runner/work/comenq/comenq/**/pyproject.toml,/home/runner/work/comenq/comenq/**/uv.lock]. The cache will never get invalidated. Make sure you have checked out the target repository and configured the cache-dependency-glob input correctly.
Trying to restore uv cache from GitHub Actions cache with key: setup-uv-1-x86_64-unknown-linux-gnu-3.12.3-pruned-no-dependency-glob-action-e92bafb6253dcd438e0484186d7669ea7a8ca1cc
No GitHub Actions cache found for key: setup-uv-1-x86_64-unknown-linux-gnu-3.12.3-pruned-no-dependency-glob-action-e92bafb6253dcd438e0484186d7669ea7a8ca1cc
Run uv run --script "/home/runner/work/_actions/leynos/shared-actions/f9f1c863c8a5bef64aa6779caa746e1a4a6c1ad4/.github/actions/generate-coverage/scripts/detect.py"
Downloading pygments (1.2MiB)
 Downloading pygments
Installed 8 packages in 11ms
Run actions/cache@v4
Cache not found for input keys: Linux-llvmcov-fcf3a98cc19692771e2ac629f10046cadcb1536fbdbc6c4f7b61458fa62c5a70, Linux-llvmcov-
Run uv run --script "/home/runner/work/_actions/leynos/shared-actions/f9f1c863c8a5bef64aa6779caa746e1a4a6c1ad4/.github/actions/generate-coverage/scripts/install_cargo_llvm_cov.py"
Installed 9 packages in 11ms
$ /home/runner/.cargo/bin/cargo install cargo-llvm-cov --force
cargo-llvm-cov installed successfully
Run uv run --script "/home/runner/work/_actions/leynos/shared-actions/f9f1c863c8a5bef64aa6779caa746e1a4a6c1ad4/.github/actions/generate-coverage/scripts/run_rust.py"
Downloading lxml (5.0MiB)
 Downloading lxml
Installed 10 packages in 10ms
$ cargo llvm-cov --workspace --summary-only --lcov --output-path lcov.info
info: cargo-llvm-cov currently setting cfg(coverage) and cfg(coverage_nightly); you can opt-out it by passing --no-cfg-coverage and --no-cfg-coverage-nightly
   Compiling comenq-lib v0.1.0 (/home/runner/work/comenq/comenq)
   Compiling filetime v0.2.25 (/home/runner/work/comenq/comenq/vendor/filetime)
   Compiling test-support v0.1.0 (/home/runner/work/comenq/comenq/test-support)
   Compiling cucumber v0.20.2
   Compiling comenq v0.1.0 (/home/runner/work/comenq/comenq/crates/comenq)
   Compiling notify v5.2.0
   Compiling test-utils v0.1.0 (/home/runner/work/comenq/comenq/crates/test-utils)
   Compiling yaque v0.6.6
   Compiling comenqd v0.1.0 (/home/runner/work/comenq/comenq/crates/comenqd)
error[E0432]: unresolved imports `comenqd::daemon::listener::CLIENT_READ_TIMEOUT_SECS`, `comenqd::daemon::listener::MAX_REQUEST_BYTES`
Error:   --> crates/comenqd/tests/listener.rs:12:33
   |
12 | use comenqd::daemon::listener::{CLIENT_READ_TIMEOUT_SECS, MAX_REQUEST_BYTES, handle_client};
   |                                 ^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^ no `MAX_REQUEST_BYTES` in `daemon::listener`
   |                                 |
   |                                 no `CLIENT_READ_TIMEOUT_SECS` in `daemon::listener`

For more information about this error, try `rustc --explain E0432`.
error: could not compile `comenqd` (test "listener") due to 1 previous error
warning: build failed, waiting for other jobs to finish...
error: process didn't exit successfully: `/home/runner/.rustup/toolchains/nightly-2025-07-22-x86_64-unknown-linux-gnu/bin/cargo test --tests --manifest-path /home/runner/work/comenq/comenq/Cargo.toml --target-dir /home/runner/work/comenq/comenq/target/llvm-cov-target --workspace` (exit status: 101)
cargo llvm-cov --workspace --summary-only --lcov --output-path lcov.info failed with code 1
Error: Process completed with exit code 1.
Run actions/upload-artifact@v4
Warning: No files were found with the provided path: lcov.info. No artifacts will be uploaded.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 16, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

I need to examine the current code structure to understand what constants are available and fix the import issue.

@leynos The compilation error occurs because the test is trying to import constants CLIENT_READ_TIMEOUT_SECS and MAX_REQUEST_BYTES that are defined in crates/comenqd/src/daemon/listener.rs but are not publicly exported.

Looking at the code, these constants are defined as private (const) in the daemon listener module, but the test in crates/comenqd/tests/listener.rs is trying to import them as if they were public.

Fix

Export these constants by making them public in crates/comenqd/src/daemon/listener.rs:

// Change from:
const CLIENT_READ_TIMEOUT_SECS: u64 = 5;
const MAX_REQUEST_BYTES: usize = 1024;

// To:
pub const CLIENT_READ_TIMEOUT_SECS: u64 = 5;
pub const MAX_REQUEST_BYTES: usize = 1024;

AI Agent Prompt

Fix the Rust compilation error by making the constants `CLIENT_READ_TIMEOUT_SECS` and `MAX_REQUEST_BYTES` publicly accessible.

Task: In the file `crates/comenqd/src/daemon/listener.rs`, change the visibility of the constants `CLIENT_READ_TIMEOUT_SECS` and `MAX_REQUEST_BYTES` from private to public by adding the `pub` keyword before each `const` declaration.

Specifically:
1. Find the line containing `const CLIENT_READ_TIMEOUT_SECS: u64 = 5;`
2. Change it to `pub const CLIENT_READ_TIMEOUT_SECS: u64 = 5;`
3. Find the line containing `const MAX_REQUEST_BYTES: usize = 1024;` 
4. Change it to `pub const MAX_REQUEST_BYTES: usize = 1024;`

This will allow the test file `crates/comenqd/tests/listener.rs` to successfully import these constants from the `comenqd::daemon::listener` module.

🏁 Script executed:

#!/bin/bash
# First, let's see what's currently exported from the listener module
cat crates/comenqd/src/listener.rs

Length of output: 7566


🏁 Script executed:

#!/bin/bash
# Let's also check the failing test file to understand what it needs
cat crates/comenqd/tests/listener.rs

Length of output: 2821


🏁 Script executed:

#!/bin/bash
# Let's search for these constants throughout the codebase to see if they exist elsewhere
rg -n "CLIENT_READ_TIMEOUT_SECS|MAX_REQUEST_BYTES" --type=rs

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Let's also check what's in the daemon module structure
find crates/comenqd/src -name "*.rs" | head -20

Length of output: 299


🏁 Script executed:

#!/bin/bash
# Let's see the daemon module structure
ls -la crates/comenqd/src/daemon/

Length of output: 73


🏁 Script executed:

#!/bin/bash
# Check what's in the daemon/listener.rs file
cat crates/comenqd/src/daemon/listener.rs

Length of output: 69


Now I understand the issue. The constants and function exist, but the import path in the test is incorrect.

@leynos The compilation error is due to an incorrect import path in the test file. The listener functionality was moved during the refactoring, and the test is trying to import from the old path.

Analysis

Looking at the code structure:

  • The constants CLIENT_READ_TIMEOUT_SECS and MAX_REQUEST_BYTES are defined in crates/comenqd/src/listener.rs (not in daemon/listener.rs)
  • They are already marked as pub const, so they're publicly accessible
  • The handle_client function is also public in the same file

Fix

Change the import statement in crates/comenqd/tests/listener.rs from:

use comenqd::daemon::listener::{CLIENT_READ_TIMEOUT_SECS, MAX_REQUEST_BYTES, handle_client};

To:

use comenqd::listener::{CLIENT_READ_TIMEOUT_SECS, MAX_REQUEST_BYTES, handle_client};

AI Agent Prompt

Fix the Rust compilation error by correcting the import path in the test file.

Task: In the file `crates/comenqd/tests/listener.rs`, update the import statement on line 12 to use the correct module path.

Specifically:
1. Find the line containing `use comenqd::daemon::listener::{CLIENT_READ_TIMEOUT_SECS, MAX_REQUEST_BYTES, handle_client};`
2. Change it to `use comenqd::listener::{CLIENT_READ_TIMEOUT_SECS, MAX_REQUEST_BYTES, handle_client};`

The issue is that the listener functionality is now directly in the `comenqd::listener` module rather than in `comenqd::daemon::listener`. This change will allow the test to successfully import the required constants and function.

🏁 Script executed:

#!/bin/bash
# Let's check what's in the daemon.rs file to understand the module structure
cat crates/comenqd/src/daemon.rs

Length of output: 1221


🏁 Script executed:

#!/bin/bash
# Let's also check the lib.rs to see how modules are organized
cat crates/comenqd/src/lib.rs

Length of output: 620


Perfect! Now I can see the issue clearly. The daemon::listener module exists but only re-exports specific functions, not the constants.

@leynos The compilation error occurs because the daemon::listener module only re-exports three functions (handle_client, prepare_listener, run_listener) but not the constants CLIENT_READ_TIMEOUT_SECS and MAX_REQUEST_BYTES that the test is trying to import.

Analysis

Looking at crates/comenqd/src/daemon.rs, the pub mod listener section only re-exports:

  • handle_client ✓ (available)
  • prepare_listener
  • run_listener

But the constants CLIENT_READ_TIMEOUT_SECS and MAX_REQUEST_BYTES are not re-exported, even though they exist as public constants in the internal listener module.

Fix

Add the missing constants to the re-exports in crates/comenqd/src/daemon.rs. In the pub mod listener section, change:

pub mod listener {
    pub use crate::listener::{handle_client, prepare_listener, run_listener};
}

To:

pub mod listener {
    pub use crate::listener::{
        handle_client, prepare_listener, run_listener,
        CLIENT_READ_TIMEOUT_SECS, MAX_REQUEST_BYTES
    };
}

AI Agent Prompt

Fix the Rust compilation error by adding missing constant re-exports to the daemon::listener module.

Task: In the file `crates/comenqd/src/daemon.rs`, update the `pub mod listener` section to re-export the missing constants that the test is trying to import.

Specifically:
1. Find the `pub mod listener` section that contains: `pub use crate::listener::{handle_client, prepare_listener, run_listener};`
2. Change it to:
```rust
pub mod listener {
    pub use crate::listener::{
        handle_client, prepare_listener, run_listener,
        CLIENT_READ_TIMEOUT_SECS, MAX_REQUEST_BYTES
    };
}
```

This will make the constants `CLIENT_READ_TIMEOUT_SECS` and `MAX_REQUEST_BYTES` available through the `comenqd::daemon::listener` module path, which is what the test file is trying to import.

Prevent a brief window of overly permissive mode by setting the temporary socket's permissions before renaming into place.

Adds error context for renaming and permission adjustments and reinforces final mode after the rename.
@leynos leynos force-pushed the codex/fix-race-condition-in-unix-socket-handling branch from cbb330b to d6ef3ae Compare September 17, 2025 00:19
@leynos leynos merged commit b00add4 into main Sep 17, 2025
1 of 3 checks passed
@leynos leynos deleted the codex/fix-race-condition-in-unix-socket-handling branch September 17, 2025 00:22
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cbb330b and d6ef3ae.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • Cargo.toml (1 hunks)
  • crates/comenqd/Cargo.toml (1 hunks)
  • crates/comenqd/src/daemon.rs (1 hunks)
  • crates/comenqd/src/listener.rs (4 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
Cargo.toml

📄 CodeRabbit inference engine (AGENTS.md)

Cargo.toml: Use explicit version ranges in Cargo.toml and keep dependencies up-to-date.
Mandate caret requirements for all dependencies. All crate versions specified in Cargo.toml must use SemVer-compatible caret requirements (e.g., some-crate = "1.2.3").
Prohibit unstable version specifiers. The use of wildcard (*) or open-ended inequality (>=) version requirements are strictly forbidden, as are tilde requirements (~) except for documented reasons.

Files:

  • Cargo.toml
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Clippy warnings MUST be disallowed.
Fix any warnings emitted during tests in the code itself rather than silencing them.
Where a function is too long, extract meaningfully named helper functions adhering to separation of concerns and CQRS.
Where a function has too many parameters, group related parameters in meaningfully named structs.
Where a function is returning a large error consider using Arc to reduce the amount of data returned.
Write unit and behavioural tests for new functionality. Run both before and after making any change.
Every module must begin with a module level (//! ) comment explaining the module's purpose and utility.
Document public APIs using Rustdoc comments (///) so documentation can be generated with cargo doc.
Prefer immutable data and avoid unnecessary mut bindings.
Handle errors with the Result type instead of panicking where feasible.
Avoid unsafe code unless absolutely necessary and document any usage clearly.
Place function attributes after doc comments.
Do not use return in single-line functions.
Use predicate functions for conditional criteria with more than two branches.
Lints must not be silenced except as a last resort.
Lint rule suppressions must be tightly scoped and include a clear reason.
Prefer expect over allow.
Use rstest fixtures for shared setup.
Replace duplicated tests with #[rstest(...)] parameterised cases.
Prefer mockall for mocks/stubs.
Prefer .expect() over .unwrap().
Use concat!() to combine long string literals rather than escaping newlines with a backslash.
Prefer semantic error enums. Derive std::error::Error (via the thiserror crate) for any condition the caller might inspect, retry, or map to an HTTP status.
Use an opaque error only at the app boundary. Use eyre::Report for human-readable logs; these should not be exposed in public APIs.
Never export the opaque type from a library. Convert to domain enums at API boundaries, and to eyre only in the main main() entrypoint or top-level async task.

Files:

  • crates/comenqd/src/listener.rs
  • crates/comenqd/src/daemon.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cyclomatic complexity of functions no more than 12.

  • Adhere to single responsibility and CQRS

  • Place function attributes after doc comments.

  • Do not use return in single-line functions.

  • Move conditionals with >2 branches into a predicate function.

  • Avoid unsafe unless absolutely necessary.

  • Every module must begin with a //! doc comment that explains the module's purpose and utility.

  • Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar

  • Lints must not be silenced except as a last resort.

    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.

  • Use rstest fixtures for shared setup and to avoid repetition between tests.

  • Replace duplicated tests with #[rstest(...)] parameterised cases.

  • Prefer mockall for mocks/stubs.

  • Prefer .expect() over .unwrap()

  • Ensure that any API or behavioural changes are reflected in the documentation in docs/

  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/

  • Files must not exceed 400 lines in length

    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such

    • For testing of functionality depending upon environment variables, dependency injection and the mockable crate are the preferred option.
    • If mockable cannot be used, env mutations in tests ...

Files:

  • crates/comenqd/src/listener.rs
  • crates/comenqd/src/daemon.rs
🧬 Code graph analysis (1)
crates/comenqd/src/daemon.rs (1)
crates/comenqd/src/listener.rs (3)
  • handle_client (141-160)
  • prepare_listener (37-68)
  • run_listener (81-127)
🔍 Remote MCP Ref

Summary of additional relevant facts for reviewing PR #102 (atomic Unix-socket bind)

  • Implementation summary (from PR context):
    • prepare_listener now creates a temp socket in the same parent directory (temp name uses UUID v4), binds to it, sets temp perms to 0o660, atomically renames the temp into the final path, re-applies 0o660 on the final path, and returns the listener. It removes the temp file on rename failure. Tests added that race concurrent creators against prepare_listener. (source: PR context)
  • Tests & CI:
    • New multithreaded test may be timing-sensitive; verify it runs reliably on CI matrix (Linux/macOS) and isn’t flaky. Ensure Cargo.lock/workspace changes (uuid v1 with v4 feature) are included so CI builds succeed. (source: PR context)
  • Export/import CI break:
    • Tests failed because daemon::listener currently did not re-export CLIENT_READ_TIMEOUT_SECS and MAX_REQUEST_BYTES. Recommended fix: add those constants to the pub use list in crates/comenqd/src/daemon.rs so tests importing comenqd::daemon::listener::{...} compile. (source: PR context)
  • Security/behavioral checks to perform in review:
    • Confirm temp socket is created in the target parent directory and that parent resolution handles symlinks, relative paths, and permission errors robustly.
    • Confirm cleanup paths always attempt to unlink the temp socket on failures (including rename and chmod failures).
    • Validate that setting 0o660 after rename is appropriate across intended deployment environments (consider umask/ACLs).
    • Confirm UUID dependency is acceptable for workspace and that Cargo.toml/Cargo.lock updates are included in PR.
    • Ensure error contexts (with_context) provide actionable messages for bind/rename/chmod failures.
  • Actionable edits to unblock CI:
    • Add missing re-exports in crates/comenqd/src/daemon.rs:
      pub use crate::listener::{ handle_client, prepare_listener, run_listener, CLIENT_READ_TIMEOUT_SECS, MAX_REQUEST_BYTES };
    • Or update tests to import constants directly from comenqd::listener instead of comenqd::daemon::listener.

Tools queried (search attempts encountered errors; no external docs retrieved):

  • Ref.ref_search_documentation — search attempts returned errors (402).
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (5)
Cargo.toml (1)

61-61: Add workspace uuid — OK

Adopt the workspace‑scoped uuid with v4; version constraint complies with caret semantics. No further changes needed here.

crates/comenqd/src/listener.rs (3)

23-26: Document atomicity and permission timing — OK

The doc now correctly states same‑dir rename atomicity and the pre/post‑rename permission steps. Ship it.


38-67: Close the unlink‑bind race with atomic rename — LGTM

  • Bind to a temp socket under the same parent.
  • Set 0o660 before visibility; enforce again after rename.
  • Clean up the orphan on rename failure with contextual errors.

This removes the hijack window without introducing new hazards.


162-196: Stabilise the race test — cannot verify in sandbox; run the check locally

Sandbox lacks /proc (error: "no /proc/self/exe available"); run the loop below locally or on CI to confirm the test is stable and report failures.

#!/bin/bash
set -e
for i in $(seq 1 50); do
  echo "Run $i"
  cargo test -p comenqd prepare_listener_prevents_pre_bind_race -- --quiet \
    || { echo "Test failed on run $i"; exit 1; }
done
echo "All good"

Report failures.

crates/comenqd/src/daemon.rs (1)

31-36: Re‑export constants to fix test imports — OK

Keep the re-exports of CLIENT_READ_TIMEOUT_SECS and MAX_REQUEST_BYTES under daemon::listener — tests import them at crates/comenqd/tests/listener.rs:12. Cargo check failed in the sandbox (no /proc); run the commands below locally to confirm the build:

rg -nP --type=rust -C2 '\buse\s+comenqd::daemon::listener::\{[^}]*CLIENT_READ_TIMEOUT_SECS[^}]*MAX_REQUEST_BYTES' crates/comenqd/tests || true
cargo check -p comenqd

Report build failures.

Comment thread crates/comenqd/Cargo.toml
figment = { version = "0.10", default-features = false, features = ["env", "toml"] }
test-support = { workspace = true, optional = true }
backon = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Avoid per‑crate feature duplication; rely on workspace features

Let the workspace drive features to prevent drift. Drop the local features = ["v4"] here.

Apply this diff:

-uuid = { workspace = true, features = ["v4"] }
+uuid = { workspace = true }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uuid = { workspace = true, features = ["v4"] }
uuid = { workspace = true }
🤖 Prompt for AI Agents
In crates/comenqd/Cargo.toml around line 24, the crate is overriding
workspace-managed features by specifying `features = ["v4"]` for the uuid
dependency; remove the `features = ["v4"]` fragment so the dependency is
declared as just `uuid = { workspace = true }` and let the workspace Cargo.toml
control which uuid features are enabled.

Comment on lines +38 to 67
let parent = path.parent().context("socket path missing parent")?;
let file_name = path
.file_name()
.ok_or_else(|| anyhow::anyhow!("socket path missing file name"))?;
let tmp = parent.join(format!(
".{}.{}",
file_name.to_string_lossy(),
Uuid::new_v4()
));
let listener = UnixListener::bind(&tmp)
.with_context(|| format!("binding to temp socket {}", tmp.display()))?;
// Ensure correct permissions before the temp socket becomes visible at the final path.
stdfs::set_permissions(&tmp, stdfs::Permissions::from_mode(0o660))
.with_context(|| format!("setting permissions on {}", tmp.display()))?;

stdfs::rename(&tmp, path)
.inspect_err(|_| {
if let Err(e) = stdfs::remove_file(&tmp) {
tracing::error!(
"failed to remove orphaned socket file {}: {}",
tmp.display(),
e
);
}
})
.with_context(|| format!("renaming socket {} -> {}", tmp.display(), path.display()))?;
// Belt-and-braces: enforce final permissions in case of ACL quirkiness.
stdfs::set_permissions(path, stdfs::Permissions::from_mode(0o660))
.with_context(|| format!("setting permissions on {}", path.display()))?;
Ok(listener)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Unexport opaque errors from a library API

Stop exposing anyhow::Result from public functions in a library. Define a typed error (derive thiserror::Error) and return Result<_, ListenerError>. Map IO/context errors accordingly. Keep eyre/opaque errors at the app boundary only.

Example sketch (apply outside this hunk):

#[derive(thiserror::Error, Debug)]
pub enum ListenerError {
    #[error("binding temp socket {0}: {1}")]
    Bind(std::path::PathBuf, #[source] std::io::Error),
    #[error("setting permissions on {0}: {1}")]
    Chmod(std::path::PathBuf, #[source] std::io::Error),
    #[error("renaming socket {0} -> {1}: {2}")]
    Rename(std::path::PathBuf, std::path::PathBuf, #[source] std::io::Error),
    #[error("missing parent for socket path")]
    MissingParent,
    #[error("socket path missing file name")]
    MissingFileName,
}

Then change signatures to return Result<UnixListener, ListenerError> and replace with_context/bail/anyhow! with typed variants.

🤖 Prompt for AI Agents
In crates/comenqd/src/listener.rs around lines 38 to 67, the function currently
returns anyhow::Result and uses anyhow/with_context which exposes opaque errors;
replace this with a typed ListenerError (derive thiserror::Error) and change the
function signature to Result<UnixListener, ListenerError>. Add variants for
MissingParent and MissingFileName (no source) and Bind/Chmod/Rename carrying
PathBuf(s) and #[source] std::io::Error. Replace context/bail/anyhow! calls by
mapping errors into those variants (e.g.
parent().ok_or(ListenerError::MissingParent), file_name ok_or
ListenerError::MissingFileName, and map Errs from bind/set_permissions/rename
into Bind/Chmod/Rename using map_err or .map_err(|e|
ListenerError::Bind(tmp.clone(), e))). Ensure all path values are cloned/moved
as needed so the error variants hold the relevant PathBufs.

🧹 Nitpick

Remove stale socket on shutdown to reduce operational noise

Unlink the final socket path during orderly shutdown after dropping the listener. This avoids littering /run//tmp with stale entries and keeps service restarts clean.

Add at the end of run_listener (outside this hunk):

// after breaking the accept loop
drop(listener);
let _ = std::fs::remove_file(&config.socket_path);
🤖 Prompt for AI Agents
In crates/comenqd/src/listener.rs around lines 38 to 67, the temp socket is
created and renamed but the final socket file is not unlinked on orderly
shutdown; modify run_listener (outside this hunk, after breaking the accept
loop) to explicitly drop the UnixListener and then attempt to remove the final
socket path (use std::fs::remove_file(&config.socket_path)), ignoring or logging
any error; ensure the drop(listener) happens before remove_file so the socket is
closed, and tolerate failures from remove_file (e.g. with let _ =
std::fs::remove_file(...)) so shutdown is best-effort.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Potential race condition when removing and binding Unix socket file

1 participant