Skip to content

Fix CLI exit paths to preserve Zeroize semantics#29

Merged
darkmaster0345 merged 1 commit into
mainfrom
gt/birch/be29d428
Jun 19, 2026
Merged

Fix CLI exit paths to preserve Zeroize semantics#29
darkmaster0345 merged 1 commit into
mainfrom
gt/birch/be29d428

Conversation

@darkmaster0345

@darkmaster0345 darkmaster0345 commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Change read_password() to return Result<Zeroizing, (ExitCode, String)> instead of calling process::exit() internally. This allows the Zeroize Drop implementation to run on password buffers when errors occur, preventing sensitive data from remaining in memory on early exit paths.

Summary by Sourcery

Adjust CLI password-reading flow to propagate errors instead of exiting directly so that Zeroize cleanup can occur on all exit paths.

Bug Fixes:

  • Ensure password buffers are zeroized on early error exits by returning errors from password-reading helpers instead of calling process::exit().

Enhancements:

  • Unify password and password-confirmation error handling in the CLI, including JSON output for non-interactive error cases.

Change read_password() to return Result<Zeroizing<String>, (ExitCode, String)>
instead of calling process::exit() internally. This allows the Zeroize Drop
implementation to run on password buffers when errors occur, preventing
sensitive data from remaining in memory on early exit paths.
@sourcery-ai

sourcery-ai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Refactors CLI password reading to return structured error results instead of exiting the process directly, so Zeroizing drop semantics are preserved on all error paths and errors are surfaced consistently to run().

Sequence diagram for CLI password reading and error handling refactor

sequenceDiagram
    participant Run as run
    participant RPC as read_password_confirmed
    participant RP as read_password
    participant FS as fs_read
    participant ENV as env_var
    participant RPP as rpassword_prompt_password
    participant JSON as emit_json

    Run->>RPC: read_password_confirmed(password_file)
    RPC->>RP: read_password(password_file)
    alt password_file provided
        RP->>FS: fs::read(path)
        alt fs::read error
            FS-->>RP: Err
            RP-->>RPC: Err(ExitCode::BadInput, msg)
            RPC-->>Run: Err(ExitCode::BadInput, msg)
        else fs::read ok
            FS-->>RP: Ok(bytes)
            RP-->>RPC: Ok(Zeroizing<String>)
            RPC-->>Run: Ok(Zeroizing<String>)
        end
    else NEURON_PASSWORD set
        RP->>ENV: std::env::var(NEURON_PASSWORD)
        ENV-->>RP: Ok(pw)
        RP-->>RPC: Ok(Zeroizing<String>)
        RPC-->>Run: Ok(Zeroizing<String>)
    else interactive
        RP->>RPP: rpassword::prompt_password
        alt prompt_password error
            RPP-->>RP: Err(e)
            RP-->>Run: Err(ExitCode::BadInput, msg)
        else prompt_password ok
            RPP-->>RP: Ok(pw)
            RP-->>Run: Ok(Zeroizing<String>)
        end
    end

    %% Error handling path in run for direct read_password use
    Run->>RP: read_password(password_file)
    alt read_password returns Err
        RP-->>Run: Err(code, msg)
        alt [cli.json]
            Run->>JSON: emit_json(JsonResult{status:"error", error:Some(msg)})
        end
        Run->>Run: return Err(code)
    else read_password returns Ok
        RP-->>Run: Ok(Zeroizing<String>)
    end
Loading

File-Level Changes

Change Details Files
Make password reading functions return Result with ExitCode and message instead of exiting directly, and propagate these errors through run() with appropriate user/json output.
  • Change read_password to return Result<Zeroizing, (ExitCode, String)> and replace process::exit calls with structured errors using map_err and explicit Err returns.
  • Update read_password_confirmed to use the new read_password signature, return Result<Zeroizing, (ExitCode, String)>, and attach ExitCode::BadInput to its error cases including mismatch and prompt failures.
  • Adjust run() to handle the new Result-based APIs: for confirm operations, propagate (ExitCode, String) errors as printed messages and ExitCode; for non-confirm operations, handle read_password errors by optionally emitting a JSON error payload when --json is set, printing the error, and returning the associated ExitCode.
neuron-encrypt/src/bin/cli.rs

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

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@darkmaster0345, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 59 minutes and 34 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f81cbdfb-9d81-4b2a-93bc-6b150fba2f61

📥 Commits

Reviewing files that changed from the base of the PR and between f4975af and 1b8ed60.

📒 Files selected for processing (1)
  • neuron-encrypt/src/bin/cli.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gt/birch/be29d428

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@darkmaster0345 darkmaster0345 merged commit 7cd2972 into main Jun 19, 2026
4 of 6 checks passed

@sourcery-ai sourcery-ai Bot left a comment

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.

Hey - I've found 1 issue, and left some high level feedback:

  • Using (ExitCode, String) as the error type makes the interfaces harder to read and evolve; consider introducing a small error enum or type alias (e.g., type CliError = (ExitCode, String) or enum CliError { ... }) to clarify intent and avoid tuple unpacking everywhere.
  • The JSON vs non-JSON error reporting logic added in the read_password error path duplicates patterns used elsewhere; consider factoring this into a small helper (e.g., fn report_error(code, msg, json, start)), so all exit paths handle formatting and emission consistently.
  • Now that read_password and read_password_confirmed share the same error shape, it might be worth extracting shared error messages/constants (e.g., for Error reading passphrase) to reduce string duplication and keep future changes to those messages in one place.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Using `(ExitCode, String)` as the error type makes the interfaces harder to read and evolve; consider introducing a small error enum or type alias (e.g., `type CliError = (ExitCode, String)` or `enum CliError { ... }`) to clarify intent and avoid tuple unpacking everywhere.
- The JSON vs non-JSON error reporting logic added in the `read_password` error path duplicates patterns used elsewhere; consider factoring this into a small helper (e.g., `fn report_error(code, msg, json, start)`), so all exit paths handle formatting and emission consistently.
- Now that `read_password` and `read_password_confirmed` share the same error shape, it might be worth extracting shared error messages/constants (e.g., for `Error reading passphrase`) to reduce string duplication and keep future changes to those messages in one place.

## Individual Comments

### Comment 1
<location path="neuron-encrypt/src/bin/cli.rs" line_range="432-435" />
<code_context>
         {
             match read_password_confirmed(&cli.password_file) {
                 Ok(password) => password,
-                Err(msg) => {
+                Err((code, msg)) => {
                     eprintln!("Error: {msg}");
-                    return Err(ExitCode::BadInput);
</code_context>
<issue_to_address>
**issue (bug_risk):** JSON mode does not emit structured error output for `read_password_confirmed`, unlike the non-confirm path.

In this branch, `read_password_confirmed` errors only print to stderr and return an exit code, even when `cli.json` is set, whereas the non-confirm branch now emits a JSON error before returning. This inconsistency means JSON-mode callers get structured errors only for some password failures (e.g., mismatched passphrases). Please mirror the JSON emission here so all password-related errors produce a JSON payload when `--json` is enabled.
</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 on lines 432 to +435
{
match read_password_confirmed(&cli.password_file) {
Ok(password) => password,
Err(msg) => {
Err((code, msg)) => {

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.

issue (bug_risk): JSON mode does not emit structured error output for read_password_confirmed, unlike the non-confirm path.

In this branch, read_password_confirmed errors only print to stderr and return an exit code, even when cli.json is set, whereas the non-confirm branch now emits a JSON error before returning. This inconsistency means JSON-mode callers get structured errors only for some password failures (e.g., mismatched passphrases). Please mirror the JSON emission here so all password-related errors produce a JSON payload when --json is enabled.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity · 2 duplication

Metric Results
Complexity 0
Duplication 2

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1b8ed6053a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +113 to +115
let mut pw = Zeroizing::new(String::from_utf8(bytes).map_err(|e| {
(ExitCode::BadInput, format!("Password file contains invalid UTF-8: {e}"))
})?);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Zeroize invalid UTF-8 password bytes

When --password-file points at a file whose secret contains any invalid UTF-8 byte, String::from_utf8 returns the original file contents inside FromUtf8Error; this handler formats the error and drops that buffer before it is ever wrapped in Zeroizing. That leaves the password file bytes in the allocator on this new non-exiting error path, so the hardening does not actually wipe the sensitive buffer for invalid password files.

Useful? React with 👍 / 👎.

@kilo-code-bot

kilo-code-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown

Code Review Roast 🔥

Verdict: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
🚨 critical 0
⚠️ warning 0
💡 suggestion 1
🤏 nitpick 0

details:

Issue Details (click to expand)
File Line Roast
neuron-encrypt/src/bin/cli.rs 435 The confirm-path forgot to put on its JSON suit — it's still doing the bare-minimum error handling while the non-confirm path is all dressed up with structured output.

🏆 Best part: The core security fix is solid — removing those process::exit() calls means Zeroize can actually do its job and scrub passwords from memory on error paths. Took me a second to realize this wasn't a typo-fest.

💀 Worst part: Line 435 — the read_password_confirmed error handler is the only kid on the playground not wearing the same uniform. It prints to stderr and bails, completely ignoring --json mode while the other path at least puts on the structured error costume.

📊 Overall: Like teaching a kid to tie their shoes but forgetting to show them the left foot — the intent is good, but half the implementation missed the memo.

Files Reviewed (1 file)
  • neuron-encrypt/src/bin/cli.rs - 1 issue

@kilo-code-bot

kilo-code-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown

Code Review Roast 🔥

Verdict: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
🚨 critical 0
⚠️ warning 0
💡 suggestion 1
🤏 nitpick 0
Issue Details (click to expand)
File Line Roast
neuron-encrypt/src/bin/cli.rs 435 The confirm-path forgot to put on its JSON suit — it's still doing the bare-minimum error handling while the non-confirm path is all dressed up with structured output.

🏆 Best part: The core security fix is solid — removing those process::exit() calls means Zeroize can actually do its job and scrub passwords from memory on error paths. Took me a second to realize this wasn't a typo-fest.

💀 Worst part: Line 435 — the read_password_confirmed error handler is the only kid on the playground not wearing the same uniform. It prints to stderr and bails, completely ignoring --json mode while the other path at least puts on the structured error costume.

📊 Overall: Like teaching a kid to tie their shoes but forgetting to show them the left foot — the intent is good, but half the implementation missed the memo.

Files Reviewed (1 file)
  • neuron-encrypt/src/bin/cli.rs - 1 issue

Reviewed by laguna-m.1-20260312:free · Input: 284.6K · Output: 10.3K · Cached: 66K

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant