Files
owlry/docs/superpowers/specs/2026-03-26-codebase-hardening-design.md
vikingowl bd69f8eafe perf(ui): use ListBox::remove_all() instead of per-child loop
Replaces five while-loop child removal patterns with the batched
remove_all() method available since GTK 4.12. Avoids per-removal
layout invalidation.
2026-03-29 20:43:41 +02:00

7.2 KiB

Codebase Hardening: owlry + owlry-plugins

Date: 2026-03-26 Scope: 15 fixes across 2 repositories, organized in 5 severity tiers Approach: Severity-ordered tiers, one commit per tier, core repo first


Tier 1: Critical / Soundness (owlry core)

1a. Replace static mut HOST_API with OnceLock

File: crates/owlry-plugin-api/src/lib.rs Problem: static mut is unsound — concurrent reads during initialization are UB. Fix: Replace with std::sync::OnceLock<&'static HostAPI>. init_host_api() calls HOST_API.set(api), host_api() calls HOST_API.get().copied(). No public API changes — convenience wrappers (notify(), log_info(), etc.) keep working. No ABI impact since HOST_API is internal.

1b. Add IPC message size limit

File: crates/owlry-core/src/server.rs Problem: BufReader::lines() reads unbounded lines. A malicious/buggy client can OOM the daemon. Fix: Replace the lines() iterator with a manual read_line() loop enforcing a 1 MB max. Lines exceeding the limit get an error response and the connection is dropped. Constant: const MAX_REQUEST_SIZE: usize = 1_048_576.

1c. Handle mutex poisoning gracefully

File: crates/owlry-core/src/server.rs Problem: All lock().unwrap() calls panic on poisoned mutex, crashing handler threads. Fix: Replace with lock().unwrap_or_else(|e| e.into_inner()). The ProviderManager and FrecencyStore don't have invariants that require abort-on-poison.


Tier 2: Security (owlry core)

2a. Set socket permissions after bind

File: crates/owlry-core/src/server.rs Problem: Socket inherits process umask, may be readable by other local users. Fix: After UnixListener::bind(), call std::fs::set_permissions(socket_path, Permissions::from_mode(0o600)). Uses std::os::unix::fs::PermissionsExt.

2b. Log signal handler failure

File: crates/owlry-core/src/main.rs Problem: ctrlc::set_handler(...).ok() silently swallows errors. Failed handler means no socket cleanup on SIGINT. Fix: Replace .ok() with if let Err(e) = ... { warn!("...") }.

2c. Add client read timeout

File: crates/owlry-core/src/server.rs Problem: A client that connects but never sends data blocks a thread forever. Fix: Set stream.set_read_timeout(Some(Duration::from_secs(30))) on accepted connections before entering the read loop.


Tier 3: Robustness / Quality (owlry core)

3a. Log malformed JSON requests

File: crates/owlry-core/src/server.rs Problem: JSON parse errors only sent as response to client, not visible in daemon logs. Fix: Add warn!("Malformed request from client: {}", e) before sending the error response.

3b. Replace Mutex with RwLock for concurrent reads

File: crates/owlry-core/src/server.rs Problem: Mutex<ProviderManager> blocks all concurrent queries even though they're read-only. Fix: Replace both Arc<Mutex<ProviderManager>> and Arc<Mutex<FrecencyStore>> with Arc<RwLock<...>>.

Lock usage per request type:

Request ProviderManager FrecencyStore
Query read() read()
Launch write()
Providers read()
Refresh write()
Toggle
Submenu read()
PluginAction read()

Poisoning recovery: .unwrap_or_else(|e| e.into_inner()) applies to RwLock the same way.


Tier 4: Critical fixes (owlry-plugins)

4a. Fix Box::leak memory leak in converter

File: owlry-plugins/crates/owlry-plugin-converter/src/units.rs Problem: Box::leak(code.into_boxed_str()) leaks memory on every keystroke for currency queries. Fix: Currency codes are already &'static str in CURRENCY_ALIASES. Change resolve_currency_code() return type from Option<String> to Option<&'static str> so it returns the static str directly. This eliminates the Box::leak. Callers in units.rs (find_unit, convert_currency, convert_currency_common) and currency.rs (is_currency_alias) must be updated to work with &'static str — mostly removing .to_string() calls or adding them at the boundary where String is needed (e.g., HashMap lookups that need owned keys).

4b. Fix bookmarks temp file race condition

File: owlry-plugins/crates/owlry-plugin-bookmarks/src/lib.rs Problem: Predictable /tmp/owlry_places_temp.sqlite path — concurrent instances clobber, symlink attacks possible. Fix: Append PID and monotonic counter to filename: owlry_places_{pid}.sqlite. Uses std::process::id(). Each profile copy gets its own name via index. Cleanup on exit remains the same.

4c. Fix bookmarks background refresh never updating state

File: owlry-plugins/crates/owlry-plugin-bookmarks/src/lib.rs Problem: Background thread loads items and saves cache but never writes back to self.items. Current session keeps stale data. Fix: Replace items: Vec<PluginItem> with items: Arc<Mutex<Vec<PluginItem>>>. Background thread writes to the shared vec after completing. provider_refresh reads from it. The loading AtomicBool already prevents concurrent loads.


Tier 5: Quality fixes (owlry-plugins)

5a. SSH plugin: read terminal from config

File: owlry-plugins/crates/owlry-plugin-ssh/src/lib.rs Problem: Hardcoded kitty as terminal fallback. Core already detects terminals. Fix: Read terminal from [plugins.ssh] in ~/.config/owlry/config.toml. Fall back to $TERMINAL env var, then xdg-terminal-exec. Same config pattern as weather/pomodoro plugins.

5b. WebSearch plugin: read engine from config

File: owlry-plugins/crates/owlry-plugin-websearch/src/lib.rs Problem: TODO comment for config reading, never implemented. Engine is always duckduckgo. Fix: Read engine from [plugins.websearch] in config.toml. Supports named engines (google, duckduckgo, etc.) or custom URL templates with {query}. Falls back to duckduckgo.

5c. Emoji plugin: build items once at init

File: owlry-plugins/crates/owlry-plugin-emoji/src/lib.rs Problem: load_emojis() clears and rebuilds ~370 items on every refresh() call. Fix: Call load_emojis() in EmojiState::new(). provider_refresh returns self.items.clone() without rebuilding.

5d. Calculator/Converter: safer shell commands

Files: owlry-plugin-calculator/src/lib.rs, owlry-plugin-converter/src/lib.rs Problem: sh -c 'echo -n "..."' pattern with double-quote interpolation. Theoretically breakable by unexpected result formatting. Fix: Use printf '%s' '...' | wl-copy with single-quote escaping (replace('\'', "'\\''")) — the same safe pattern already used by bookmarks and clipboard plugins.


Out of scope

These were identified but deferred:

  • Hardcoded emoji list — replacing with a crate/data file is a feature, not a fix
  • Plugin vtable-level tests — valuable but a separate testing initiative
  • IPC protocol versioning — protocol change, not a bug fix
  • Plugin sandbox enforcement — large feature, not a point fix
  • Desktop Exec field sanitization — deep rabbit hole, needs separate design
  • Config validation — separate concern, deserves its own pass