From ffcb2ba657d469889d5dd25612a51788e62d33c5 Mon Sep 17 00:00:00 2001 From: OneNoted Date: Wed, 15 Apr 2026 10:40:15 +0200 Subject: [PATCH 1/4] fix: reject root-only uinput setup readiness Prevent `whispers setup` from short-circuiting on root-only `/dev/uinput` readiness, which can otherwise report success without configuring the actual desktop user. Constraint: Automatic uinput setup should be evaluated for the real desktop user, not sudo-root permissions Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep setup privilege checks explicit so root-only readiness can never masquerade as user readiness Tested: cargo test setup::tests:: -- --nocapture Not-tested: Live sudo invocation --- src/setup/side_effects.rs | 17 ++++++++++++----- src/setup/tests.rs | 11 +++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/setup/side_effects.rs b/src/setup/side_effects.rs index f81d2be..efbdd8e 100644 --- a/src/setup/side_effects.rs +++ b/src/setup/side_effects.rs @@ -143,6 +143,7 @@ pub(super) fn maybe_prewarm_experimental_nemo( } pub(super) fn maybe_setup_injection_access(ui: &SetupUi) -> Result { + validate_setup_user(unsafe { libc::geteuid() })?; let readiness = crate::inject::InjectionReadinessReport::collect(); if readiness.is_ready() || !readiness.has_uinput_issue() { return Ok(InjectionSetupOutcome::default()); @@ -339,11 +340,7 @@ fn current_user_in_group(username: &str, group: &str) -> Result { fn current_username() -> Result { let uid = unsafe { libc::geteuid() }; - if uid == 0 { - return Err(crate::error::WhsprError::Config( - "run `whispers setup` as your normal user, not as root".into(), - )); - } + validate_setup_user(uid)?; current_username_for_uid_with(uid, |uid, passwd, buffer, result| unsafe { libc::getpwuid_r( @@ -356,6 +353,16 @@ fn current_username() -> Result { }) } +pub(super) fn validate_setup_user(uid: libc::uid_t) -> Result<()> { + if uid == 0 { + return Err(crate::error::WhsprError::Config( + "run `whispers setup` as your normal user, not as root".into(), + )); + } + + Ok(()) +} + pub(super) fn current_username_for_uid_with(uid: libc::uid_t, mut lookup: F) -> Result where F: FnMut(libc::uid_t, *mut libc::passwd, &mut Vec, *mut *mut libc::passwd) -> libc::c_int, diff --git a/src/setup/tests.rs b/src/setup/tests.rs index dfcb3bc..8c70f6f 100644 --- a/src/setup/tests.rs +++ b/src/setup/tests.rs @@ -70,6 +70,17 @@ fn group_membership_failures_become_warnings_without_marking_success() { assert!(warning.contains("group add failed")); } +#[test] +fn setup_rejects_root_before_uinput_readiness_short_circuit() { + let err = side_effects::validate_setup_user(0).expect_err("root should be rejected"); + match err { + WhsprError::Config(message) => { + assert!(message.contains("run `whispers setup` as your normal user, not as root")); + } + other => panic!("unexpected error variant: {other:?}"), + } +} + #[test] fn group_membership_success_marks_logout_as_needed() { let mut outcome = side_effects::InjectionSetupOutcome::default(); From 242dd2cbeab92cb59e3b1a39a42018d0824c892a Mon Sep 17 00:00:00 2001 From: OneNoted Date: Wed, 15 Apr 2026 10:41:08 +0200 Subject: [PATCH 2/4] docs: ship README-linked docs in release artifacts Keep the bundled and packaged README links valid by shipping the docs pages it references in release bundles and Cargo package contents. Constraint: Packaged README links should not point to files that are omitted from shipped artifacts Confidence: high Scope-risk: narrow Reversibility: clean Directive: If README starts linking to new local docs, add those files to package and release artifact manifests in the same change Tested: cargo package --list --allow-dirty; scripts/build-release-bundle.sh with temp dist dir (verified docs in tarball) Not-tested: GitHub release workflow end-to-end --- Cargo.toml | 3 +++ scripts/build-release-bundle.sh | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index e9b5037..42f8032 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,9 @@ include = [ "Cargo.toml", "Cargo.lock", "README.md", + "docs/install.md", + "docs/cli.md", + "docs/troubleshooting.md", "LICENSE", "NOTICE", "config.example.toml", diff --git a/scripts/build-release-bundle.sh b/scripts/build-release-bundle.sh index ae72dd3..85b3793 100755 --- a/scripts/build-release-bundle.sh +++ b/scripts/build-release-bundle.sh @@ -46,6 +46,7 @@ mkdir -p \ "$stage_dir/$bundle_name/share/bash-completion/completions" \ "$stage_dir/$bundle_name/share/zsh/site-functions" \ "$stage_dir/$bundle_name/share/fish/vendor_completions.d" \ + "$stage_dir/$bundle_name/share/doc/whispers/docs" \ "$stage_dir/$bundle_name/share/doc/whispers" \ "$stage_dir/$bundle_name/share/licenses/whispers" @@ -65,6 +66,12 @@ install -Dm755 "${target_dir}/whispers-rewrite-worker" \ install -Dm644 README.md \ "$stage_dir/$bundle_name/share/doc/whispers/README.md" +install -Dm644 docs/install.md \ + "$stage_dir/$bundle_name/share/doc/whispers/docs/install.md" +install -Dm644 docs/cli.md \ + "$stage_dir/$bundle_name/share/doc/whispers/docs/cli.md" +install -Dm644 docs/troubleshooting.md \ + "$stage_dir/$bundle_name/share/doc/whispers/docs/troubleshooting.md" install -Dm644 config.example.toml \ "$stage_dir/$bundle_name/share/doc/whispers/config.example.toml" install -Dm644 LICENSE \ From 278ae2b326ca23f49fbd1109905c7dd35bf9b240 Mon Sep 17 00:00:00 2001 From: OneNoted Date: Wed, 15 Apr 2026 11:02:12 +0200 Subject: [PATCH 3/4] fix: fail root setup before side effects Reject `whispers setup` at the start of the setup flow when it is launched as root so it cannot mutate config or starter files before reporting the privilege error. Constraint: Root-only setup runs must fail before any local state is written Rejected: Leave the root guard inside the later injection setup helper | config and starter files can already be mutated before that point Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep the root guard at the top of `run_setup()` so later helpers cannot become the first failing point again Tested: cargo test setup::tests:: -- --nocapture; cargo fmt --all -- --check; cargo check --all-targets; cargo clippy --all-targets -- -D warnings; cargo test Not-tested: Live sudo invocation --- src/setup.rs | 6 ++++++ src/setup/tests.rs | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/setup.rs b/src/setup.rs index 3d7028d..047c7c4 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -54,7 +54,13 @@ impl CloudSetup { } } +fn validate_setup_invoker(uid: libc::uid_t) -> Result<()> { + side_effects::validate_setup_user(uid) +} + pub async fn run_setup(config_path_override: Option<&Path>) -> Result<()> { + validate_setup_invoker(unsafe { libc::geteuid() })?; + let ui = SetupUi::new(); ui.print_header("whispers setup"); ui.blank(); diff --git a/src/setup/tests.rs b/src/setup/tests.rs index 8c70f6f..2e25ed4 100644 --- a/src/setup/tests.rs +++ b/src/setup/tests.rs @@ -81,6 +81,17 @@ fn setup_rejects_root_before_uinput_readiness_short_circuit() { } } +#[test] +fn setup_rejects_root_before_any_setup_side_effects() { + let err = super::validate_setup_invoker(0).expect_err("root should be rejected up front"); + match err { + WhsprError::Config(message) => { + assert!(message.contains("run `whispers setup` as your normal user, not as root")); + } + other => panic!("unexpected error variant: {other:?}"), + } +} + #[test] fn group_membership_success_marks_logout_as_needed() { let mut outcome = side_effects::InjectionSetupOutcome::default(); From 30cbe9be68540e07d346bcd23123d28515452247 Mon Sep 17 00:00:00 2001 From: OneNoted Date: Wed, 15 Apr 2026 11:08:25 +0200 Subject: [PATCH 4/4] test: stabilize wl-copy non-zero exit coverage Make the non-zero clipboard test consume stdin before exiting so CI reliably exercises the intended non-zero exit path instead of racing into a broken-pipe write. Constraint: The test should still cover the non-zero exit path, not the broken-pipe write path Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep subprocess tests deterministic by making the child process consume the expected I/O before exiting Tested: cargo test inject::tests:: -- --nocapture; cargo fmt --all -- --check; cargo check --all-targets; cargo clippy --all-targets -- -D warnings; cargo test Not-tested: GitHub Actions rerun still pending --- src/inject/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inject/tests.rs b/src/inject/tests.rs index 6044bd2..f1bc9d5 100644 --- a/src/inject/tests.rs +++ b/src/inject/tests.rs @@ -21,7 +21,7 @@ fn run_wl_copy_reports_spawn_failure() { fn run_wl_copy_reports_non_zero_exit() { let err = clipboard::run_wl_copy( "/bin/sh", - &[String::from("-c"), String::from("exit 7")], + &[String::from("-c"), String::from("cat >/dev/null; exit 7")], "hello", ) .expect_err("non-zero exit should fail");