Skip to content

0.7.0 performance hardening and latest crate upgrades - #31

Merged
programmersd21 merged 1 commit into
mainfrom
release/0.7.0-hardening
Oct 19, 2026
Merged

programmersd21 merged 1 commit into
mainfrom
release/0.7.0-hardening

Conversation

@programmersd21

@programmersd21 programmersd21 commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Bumps MSRV to 1.88, upgrades dependencies to latest compatible releases, adds mimalloc and native CPU release tuning, and continues performance/hardening work across the daemon, renderer, and media pipelines.

Summary by Sourcery

Harden daemon, IPC, configuration, and media pipelines while upgrading the toolchain and dependencies for safer and faster 0.7.0 releases.

New Features:

  • Add safer, bounded IPC with private socket ownership, startup locking, concurrency limits, and request/response timeouts.
  • Support validated seek timestamps in multiple formats and bounded, private stdin image staging.
  • Add defensive GIF/video validation and preserve correct animation timing and scheduler behavior.

Bug Fixes:

  • Prevent daemon stalls, subprocess hangs, malformed media panics, cache collisions, IPC error suppression, and unsafe socket cleanup.
  • Correct configuration defaults, reload behavior, theme cache invalidation, and Wayland surface lifetime management.

Enhancements:

  • Move blocking theme, hook, reload, and rendering-related work off the Tokio runtime.
  • Improve release performance with mimalloc, native CPU tuning, fat LTO, panic aborts, and continued allocation reductions.

Build:

  • Upgrade the workspace to Rust 1.88, resolver 3, and current compatible graphics, media, Wayland, and utility dependencies.

CI:

  • Verify workspace crate versions, the 0.7.0 release version, and changelog coverage in CI.

Documentation:

  • Document the 0.7.0 release, updated MSRV, dependency upgrades, performance tuning, hardening, and breaking changes.

Tests:

  • Expand regression coverage for IPC ownership, configuration parsing, media bounds, GIF timing, duration handling, cache identity, scheduler seeking, and command timeouts.

Chores:

  • Bump the workspace release to 0.7.0 and update release workflow examples.

@sourcery-ai

sourcery-ai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

This release combines a Rust/dependency and release-profile upgrade with broad hardening: IPC ownership and timeouts, blocking isolation for external work, strict configuration and CLI input handling, defensive GIF/video validation, safer Wayland/wgpu resource lifetimes, and regression tests covering the new failure modes.

Sequence diagram for hardened IPC command handling

sequenceDiagram
    participant Client
    participant Socket as IpcSocket
    participant Server as IPC server
    participant Handler as Command handler

    Client->>Socket: acquire(path)
    Socket->>Socket: lock and validate socket ownership
    Socket-->>Client: private listener
    Client->>Server: send_ipc_command(command)
    Server->>Server: limit connections and apply request timeout
    Server->>Handler: validate_command(command)
    Handler-->>Server: IpcResponse
    Server-->>Client: bounded response or timeout
Loading

Sequence diagram for blocking theme and reload work

sequenceDiagram
    participant Client
    participant Daemon
    participant Runtime as Tokio runtime
    participant Worker as Blocking worker
    participant Process as Theme or hook process

    Client->>Daemon: Reload or wallpaper command
    Daemon->>Runtime: spawn_blocking(...)
    Runtime->>Worker: execute reload_blocking or set_wallpaper_blocking
    Worker->>Process: run provider, hook, or reload command
    Process-->>Worker: exit or timeout
    Worker-->>Runtime: result
    Runtime-->>Daemon: response
    Daemon-->>Client: IpcResponse
Loading

Flow diagram for defensive media input processing

flowchart TD
    Input[Untrusted GIF or video input] --> Bounds{Input and metadata bounds valid?}
    Bounds -- No --> Reject[Return decode or format error]
    Bounds -- Yes --> Decode[Decode and validate dimensions, rectangles, strides]
    Decode --> Safe{Frame data complete and in range?}
    Safe -- No --> Reject
    Safe -- Yes --> Pipeline[Composite, copy, or render frame]
Loading

Flow diagram for safe release configuration and runtime tuning

flowchart LR
    Rust88[Rust 1.88 MSRV] --> Build[Native CPU and lld flags]
    Build --> Release[Fat LTO, abort panic, overflow checks off]
    Release --> Binary[wallr binary]
    Binary --> Allocator[mimalloc global allocator]
Loading

File-Level Changes

Change Details Files
Raises the workspace baseline and tunes release builds for newer dependencies and runtime performance.
  • Bumps MSRV and workspace version, switches to resolver 3, and upgrades wgpu, Wayland, media, crypto, and filesystem crates.
  • Enables fat LTO, aborting panics, disabled release overflow checks, native CPU/linker tuning, and mimalloc.
  • Adds CI enforcement for synchronized crate versions and the 0.7.0 changelog entry.
Cargo.toml
Cargo.lock
.cargo/config.toml
wallr/Cargo.toml
wallr/src/main.rs
.github/workflows/ci.yml
.github/workflows/release.yml
README.md
CHANGELOG.md
wallr-core/Cargo.toml
Hardens IPC startup, protocol handling, and daemon lifecycle against races, malformed clients, hangs, and unsafe cleanup.
  • Adds private socket permissions, advisory startup locking, stale-socket validation, inode-aware cleanup, connection limits, size bounds, and operation/handler timeouts.
  • Reserves IPC ownership before Wayland/GPU initialization and propagates daemon failures through CLI commands.
  • Removes the obsolete preview CLI variant and centralizes overflow-safe seek timestamp parsing.
wallr-core/src/ipc/mod.rs
wallr-core/src/daemon/mod.rs
wallr-common/src/ipc.rs
wallr-common/src/cli.rs
wallr/src/main.rs
Makes configuration, theme execution, and CLI file handling explicit, blocking-safe, and resilient to hostile or inconsistent input.
  • Returns errors for unset or malformed environment references, preserves explicit config paths across reloads, and aligns serde defaults with programmatic defaults.
  • Moves theme providers, hooks, reloads, and post-render work to blocking workers with bounded subprocess runtimes and zombie reaping.
  • Improves cache identity using nanosecond mtime, size, and raw path bytes; uses unique atomic temporary files; bounds private stdin staging.
wallr-common/src/config.rs
wallr-common/src/effect.rs
wallr-core/src/theme/mod.rs
wallr-core/src/wallpaper/mod.rs
wallr-core/src/daemon/mod.rs
wallr/src/main.rs
Adds defensive validation and fixes state handling across GIF animation and video decode/playback pipelines.
  • Bounds GIF input, frame counts, dimensions, rectangles, allocations, and frame cache behavior while correctly associating graphic-control delays with image descriptors.
  • Validates video timing, dimensions, decoder availability, strides, plane sizes, and overflow conditions before conversion or allocation.
  • Uses offset-based scheduler seeking and reports decoder failure instead of allowing invalid media data to panic or stall playback.
wallr-core/src/animated/mod.rs
wallr-core/src/video/decoder.rs
wallr-core/src/video/error.rs
wallr-core/src/video/playback.rs
wallr-core/src/video/scheduler.rs
Modernizes the wgpu rendering path and reduces resource leaks and unnecessary conversion overhead.
  • Migrates renderer and surface configuration to wgpu 30 APIs, including async adapter enumeration, new surface acquisition/presentation, pipeline descriptors, and color-space fields.
  • Stores surfaces in Arc-backed owned state instead of leaked allocations and updates blocking GPU polling and buffer-copy paths.
  • Retains Wayland handles with documented safety invariants and optimizes packed pixel copying.
wallr-core/src/daemon/mod.rs
wallr-core/src/renderer/mod.rs
wallr-core/src/video/gpu.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

@programmersd21
programmersd21 merged commit 1218cde into main Oct 19, 2026
2 checks passed
@programmersd21
programmersd21 deleted the release/0.7.0-hardening branch October 19, 2026 15:58

@sourcery-ai sourcery-ai 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.

Hey - I've found 16 issues

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

## Individual Comments

### Comment 1
<location path="wallr-core/src/ipc/mod.rs" line_range="116-118" />
<code_context>
+            Err(err) => return Err(IpcError::Io(err)),
+        }
+
+        let listener = UnixListener::bind(&path)?;
+        use std::os::unix::fs::{MetadataExt, PermissionsExt};
+        if let Err(err) = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)) {
+            let _ = std::fs::remove_file(&path);
+            return Err(IpcError::Io(err));
</code_context>
<issue_to_address>
**Unauthorized socket access**

When the daemon binds a socket in a directory accessible to other users and the socket is initially writable by them, `UnixListener::bind` creates the socket before `set_permissions` applies mode `0600`, leaving a window for another local user to connect and queue privileged IPC commands that the daemon later processes.

Create the socket with restrictive permissions from the outset so no other user can connect before permissions are applied.
</issue_to_address>

### Comment 2
<location path="wallr-core/src/theme/mod.rs" line_range="444" />
<code_context>
+        match child.try_wait() {
+            Ok(Some(status)) => return Ok(status),
+            Ok(None) if started.elapsed() >= timeout => {
+                let _ = child.kill();
+                let _ = child.wait();
+                return Err(ThemeError::Timeout(timeout, label.to_string()));
</code_context>
<issue_to_address>
**Timed-out commands keep running**

When a timed-out shell hook or reload command has spawned a child process that outlives the shell, `wait_with_timeout` kills and reaps only the direct child, so descendants continue consuming resources or performing side effects after the daemon reports a timeout.

Terminate and reap the command’s process group when it times out.

Also at `wallr-core/src/theme/mod.rs:399-402`.
</issue_to_address>

### Comment 3
<location path="wallr/src/main.rs" line_range="273" />
<code_context>
+        let listener = UnixListener::bind(&path)?;
+        use std::os::unix::fs::{MetadataExt, PermissionsExt};
+        if let Err(err) = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)) {
+            let _ = std::fs::remove_file(&path);
+            return Err(IpcError::Io(err));
+        }
</code_context>
<issue_to_address>
**Stdin wallpaper disappears**

When a `wallr set -` request succeeds, the daemon persists the staged path for wallpaper restoration and starts detached theme work that uses it after sending the IPC response, but the CLI then unlinks the file. Theme processing fails, and the daemon cannot restore that wallpaper after a restart.

Keep the staged image available for daemon theme work and restart restoration instead of unlinking it when the IPC response arrives.

Also at `wallr-core/src/daemon/mod.rs:2891-2906`, `wallr/src/main.rs:274`.
</issue_to_address>

### Comment 4
<location path="wallr-core/src/daemon/mod.rs" line_range="2891-2905" />
<code_context>
-        .await?;
+        });

         tokio::task::spawn_blocking(move || {
             loop {
diff --git a/wallr-core/src/ipc/mod.rs b/wallr-core/src/ipc/mod.rs
index 398b521..a999dc9 100644
--- a/wallr-core/src/ipc/mod.rs
+++ b/wallr-core/src/ipc/mod.rs
@@ -3,98 +3,452 @@

 pub use wallr_common::ipc::*;

-use std::path::Path;
+use std::path::{Path, PathBuf};
+use std::sync::Arc;
+use std::time::Duration;
 use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
 use tokio::net::{UnixListener, UnixStream};
+use tokio::sync::Semaphore;
+
</code_context>
<issue_to_address>
**Older theme overwrites newer one**

When an earlier wallpaper’s provider or hook is still running when a newer wallpaper request starts its theme work, the generation check runs only before `set_wallpaper_blocking`; the earlier task can finish afterward and overwrite the newer wallpaper’s theme, leaving colors inconsistent with the current wallpaper.

Prevent an older post-render task from writing its theme after a newer wallpaper request has started.
</issue_to_address>

### Comment 5
<location path="wallr-core/src/daemon/mod.rs" line_range="1878" />
<code_context>
+            let mut tmp = vec![0u8; size];
+            let ok = animated.decompress_into(index, &mut tmp);
+            if ok {
+                let mut mapped = slice.get_mapped_range_mut().unwrap();
+                mapped.copy_from_slice(&tmp);
+            }
</code_context>
<issue_to_address>
**Map failure aborts playback**

When wgpu fails to map the staging buffer, the map callback result and device-poll error are ignored, so `get_mapped_range_mut().unwrap()` panics; in release builds the detached playback task aborts the daemon instead of handling the GPU error.

Check and handle the map callback and device-poll results before accessing the mapped range.

Also at `wallr-core/src/daemon/mod.rs:1879`.
</issue_to_address>

### Comment 6
<location path=".cargo/config.toml" line_range="2" />
<code_context>
+[build]
+rustflags = ["-C", "target-cpu=native"]
+
+[target.x86_64-unknown-linux-gnu]
</code_context>
<issue_to_address>
**Release binary needs newer CPU**

When a published binary runs on an x86-64 CPU older than the build host, `target-cpu=native` lets code generation use the build host’s CPU features, so the release binary can execute unsupported instructions and terminate with an illegal-instruction fault.

Build distributable binaries for a supported baseline CPU instead of using `target-cpu=native`.

Also at `.cargo/config.toml:5`.
</issue_to_address>

### Comment 7
<location path="wallr-core/src/ipc/mod.rs" line_range="279-282" />
<code_context>
+            }
+            Ok(Ok(_)) => match serde_json::from_slice::<IpcCommand>(&buf) {
+                Ok(cmd) => match validate_command(&cmd) {
+                    Ok(()) => match tokio::time::timeout(HANDLER_TIMEOUT, handler(cmd)).await {
+                        Ok(response) => response,
+                        Err(_) => IpcResponse::err("IPC command timed out"),
+                    },
+                    Err(reason) => IpcResponse::err(reason),
+                },
</code_context>
<issue_to_address>
**Timed-out render continues in background**

When rendering takes longer than the 120-second handler timeout, the handler timeout drops the future awaiting `set_wallpaper_with_retry`, but its `spawn_blocking` task continues; it can still change the wallpaper after the client receives a timeout, and a retry can enqueue another change. The caller sees failure while the wallpaper changes later.

Propagate cancellation to the blocking render operation, or avoid reporting a command timeout while its non-cancellable work can still mutate wallpaper state.
</issue_to_address>

### Comment 8
<location path="wallr-core/src/daemon/mod.rs" line_range="2746" />
<code_context>
                         {
                             let mut eng = engine.lock().await;
                             eng.config = fresh.clone();
-                            if let Err(e) = eng.reload() {
+                        }
</code_context>
<issue_to_address>
**Failed reload leaves new config active**

When a reload command fails or times out after the new config passes validation, the reload handler replaces `engine.config` before running reload commands; if `reload_blocking` then fails, the handler returns an error but later wallpaper/theme operations still read the new config, leaving the daemon partially reloaded.

Run reload commands before committing `fresh` to `engine.config`, or restore the previous config if reload fails.
</issue_to_address>

### Comment 9
<location path="wallr-core/src/ipc/mod.rs" line_range="246" />
<code_context>
+        .create(true)
+        .truncate(false)
+        .mode(0o600)
+        .open(path)
+}
+
</code_context>
<issue_to_address>
**Lock file can block daemon startup**

When a different local user can write the configured socket’s parent directory and pre-creates a writable `<socket>.lock` file while holding an exclusive flock, `open_lock_file` opens the derived lock path and `lock_exclusive` maps a held lock to `AlreadyRunning`, so the daemon refuses to start even though no daemon owns the socket.

Store the lock in a user-private runtime directory, or securely create and validate the lock file’s ownership and permissions before relying on its lock.
</issue_to_address>

### Comment 10
<location path="wallr-common/src/config.rs" line_range="176" />
<code_context>
     Some([x, y])
 }

+/// Parse an IPC seek timestamp into milliseconds.
</code_context>
<issue_to_address>
**Default socket fails without runtime directory**

When `XDG_RUNTIME_DIR` is unset and a command uses the default socket, `expand_path` returns `UnsetEnv` for the default socket path, so CLI commands and daemon startup fail before using a socket, even when another writable runtime location is available.

Provide a fallback socket path when `XDG_RUNTIME_DIR` is unset.

Also at `wallr-common/src/config.rs:175`, `wallr-common/src/config.rs:287`.
</issue_to_address>

### Comment 11
<location path="wallr-common/src/cli.rs" line_range="171-174" />
<code_context>
+            return Err("timestamp must be a finite non-negative number".to_string());
+        }
+        let millis = (seconds * 1000.0).round();
+        if millis > u64::MAX as f64 {
+            return Err("timestamp overflows".to_string());
+        }
+        Ok(millis as u64)
+    }
+}
</code_context>
<issue_to_address>
**Overflowing timestamp is accepted**

When a finite bare-seconds timestamp rounds to 2^64 milliseconds, `u64::MAX as f64` rounds to 2^64, so the `>` check accepts that value and the cast saturates to `u64::MAX` instead of returning the documented overflow error.

Reject rounded millisecond values equal to 2^64 as well as larger values.
</issue_to_address>

### Comment 12
<location path="wallr-core/src/daemon/mod.rs" line_range="1875-1878" />
<code_context>
+                .poll(wgpu::PollType::wait_indefinitely())
+                .ok();
+            let size = slice.size() as usize;
+            let mut tmp = vec![0u8; size];
+            let ok = animated.decompress_into(index, &mut tmp);
+            if ok {
+                let mut mapped = slice.get_mapped_range_mut().unwrap();
+                mapped.copy_from_slice(&tmp);
+            }
</code_context>
<issue_to_address>
**GIF uploads churn memory**

When a GIF uses the direct-upload playback path, the path allocates a full-frame temporary `Vec` for each GIF frame, decompresses into it, then copies it into the mapped staging buffer. Large animations incur recurring allocation and memory-bandwidth overhead that can undermine smooth playback and exhaust memory.

Decompress directly into the mapped staging buffer without allocating and copying a full-frame temporary buffer.
</issue_to_address>

### Comment 13
<location path="wallr-core/src/theme/mod.rs" line_range="428" />
<code_context>
+        };
+        command.stdout(Stdio::null()).stderr(Stdio::null());
+        if let Ok(mut child) = command.spawn() {
+            let _ = wait_with_timeout(&mut child, RELOAD_TIMEOUT, cmd);
         }
     }
</code_context>
<issue_to_address>
**Timed-out reloads report success**

When a configured reload command exceeds `RELOAD_TIMEOUT`, `run_reload_list` discards the result of `wait_with_timeout` and returns `Ok(())`, so a reload command killed after its timeout is reported as successful and leaves no diagnostic for the operator.

Log timed-out reload commands and return an error or otherwise expose the timeout to the caller while continuing to ignore expected `pkill` nonzero exits.
</issue_to_address>

### Comment 14
<location path="wallr-common/src/config.rs" line_range="175" />
<code_context>
+
 fn default_socket() -> String {
-    "/tmp/wallr.sock".to_string()
+    "$XDG_RUNTIME_DIR/wallr.sock".to_string()
 }

</code_context>
<issue_to_address>
**Upgrade starts a second daemon**

When a config omits `daemon.socket` and a 0.6.4 daemon is still running during an upgrade, a config without an explicit `daemon.socket` now expands to `$XDG_RUNTIME_DIR/wallr.sock`, while a still-running 0.6.4 daemon listens on `/tmp/wallr.sock`. The new CLI checks only the new path, so `wallr set` starts another daemon and both can control the same outputs.

Preserve the legacy default or detect and migrate an active daemon on the old socket before auto-starting a new one.
</issue_to_address>

### Comment 15
<location path="Cargo.toml" line_range="14" />
<code_context>
 categories = ["command-line-utilities", "rendering"]
 keywords = ["wallpaper", "wayland", "wgpu", "daemon", "matugen"]
-rust-version = "1.85"
+rust-version = "1.88"

 # Optimized profile settings for faster compilation and better runtime performance
</code_context>
<issue_to_address>
**Contributor setup advertises older Rust**

When a contributor follows the documented 1.85 minimum, contributors following `CONTRIBUTING.md` can install Rust 1.85, but Cargo rejects the workspace because the declared minimum is now 1.88, so the documented setup does not build the project.

Update the contributor setup requirement to Rust 1.88 or newer.
</issue_to_address>

### Comment 16
<location path="wallr-core/src/animated/mod.rs" line_range="311-314" />
<code_context>
-                self.next_index += 1;
-                continue;
-            }
+            // Do not skip cached frames without consuming the decoder: the
+            // decoded stream position and `next_index` must stay in lockstep.
+            // Otherwise a later uncached tail frame is stored under the wrong
+            // index after a loop wrap. Re-decoding a cached prefix still uses
+            // the same cache entry (decode_next only inserts when absent).
             let before = self.next_index;
</code_context>
<issue_to_address>
**Cached GIF loops decode frames again**

When playback wraps an animation whose frames are already fully cached, after a full loop, `ensure_upto` restarts the GIF decoder and decodes each cached frame again to keep decoder position synchronized, even though playback can return those frames from cache. Fully cached animations therefore spend every loop decoding the animation a second time, increasing CPU use and risking playback stutter.

Use cached frames without decoding them when no uncached tail needs the decoder, and restart/advance the decoder only when an uncached frame must be produced.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 15 findings to address first, and the daemon now relies on socket permissions, ownership checks, and inode-based cleanup to control access to its IPC commands; if that boundary is wrong, another local user could control or disrupt the daemon. Reverting restores the previous behavior, but any unauthorized commands or external hooks already triggered would not be undone.

Blocking findings: wallr-core/src/ipc/mod.rs:118, wallr-core/src/theme/mod.rs:444, wallr/src/main.rs:273, wallr-core/src/daemon/mod.rs:2905, wallr-core/src/daemon/mod.rs:1878, and 10 more


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread wallr-core/src/ipc/mod.rs
Comment on lines +116 to +118
let listener = UnixListener::bind(&path)?;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
if let Err(err) = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Unauthorized socket access

When the daemon binds a socket in a directory accessible to other users and the socket is initially writable by them, UnixListener::bind creates the socket before set_permissions applies mode 0600, leaving a window for another local user to connect and queue privileged IPC commands that the daemon later processes.

Create the socket with restrictive permissions from the outset so no other user can connect before permissions are applied.

Prompt for AI agents
In `wallr-core/src/ipc/mod.rs` at lines 116-118:

**Unauthorized socket access**

When the daemon binds a socket in a directory accessible to other users and the socket is initially writable by them, `UnixListener::bind` creates the socket before `set_permissions` applies mode `0600`, leaving a window for another local user to connect and queue privileged IPC commands that the daemon later processes.

Create the socket with restrictive permissions from the outset so no other user can connect before permissions are applied.

match child.try_wait() {
Ok(Some(status)) => return Ok(status),
Ok(None) if started.elapsed() >= timeout => {
let _ = child.kill();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Timed-out commands keep running

When a timed-out shell hook or reload command has spawned a child process that outlives the shell, wait_with_timeout kills and reaps only the direct child, so descendants continue consuming resources or performing side effects after the daemon reports a timeout.

Terminate and reap the command’s process group when it times out.

Also at wallr-core/src/theme/mod.rs:399-402.

Prompt for AI agents
In `wallr-core/src/theme/mod.rs` at line 444:

**Timed-out commands keep running**

When a timed-out shell hook or reload command has spawned a child process that outlives the shell, `wait_with_timeout` kills and reaps only the direct child, so descendants continue consuming resources or performing side effects after the daemon reports a timeout.

Terminate and reap the command’s process group when it times out.

Also at `wallr-core/src/theme/mod.rs:399-402`.

Comment thread wallr/src/main.rs
anyhow::bail!("{}", msg);
.await;
if staged {
let _ = std::fs::remove_file(&path);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High · Stdin wallpaper disappears

When a wallr set - request succeeds, the daemon persists the staged path for wallpaper restoration and starts detached theme work that uses it after sending the IPC response, but the CLI then unlinks the file. Theme processing fails, and the daemon cannot restore that wallpaper after a restart.

Keep the staged image available for daemon theme work and restart restoration instead of unlinking it when the IPC response arrives.

Also at wallr-core/src/daemon/mod.rs:2891-2906, wallr/src/main.rs:274.

Prompt for AI agents
In `wallr/src/main.rs` at line 273:

**Stdin wallpaper disappears**

When a `wallr set -` request succeeds, the daemon persists the staged path for wallpaper restoration and starts detached theme work that uses it after sending the IPC response, but the CLI then unlinks the file. Theme processing fails, and the daemon cannot restore that wallpaper after a restart.

Keep the staged image available for daemon theme work and restart restoration instead of unlinking it when the IPC response arrives.

Also at `wallr-core/src/daemon/mod.rs:2891-2906`, `wallr/src/main.rs:274`.

Comment on lines +2891 to +2905
tokio::task::spawn_blocking(move || {
if theme_gen_check.load(Ordering::SeqCst) != generation {
return;
}
let mut eng = match WallpaperEngine::new(config) {
Ok(engine) => engine,
Err(err) => {
tracing::warn!(
"Post-render theme setup failed for {p_clone:?}: {err}"
);
return;
}
};
if let Err(e) =
eng.set_wallpaper(&p_clone, &opts).await
eng.set_wallpaper_blocking(&p_clone, &opts)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High · Older theme overwrites newer one

When an earlier wallpaper’s provider or hook is still running when a newer wallpaper request starts its theme work, the generation check runs only before set_wallpaper_blocking; the earlier task can finish afterward and overwrite the newer wallpaper’s theme, leaving colors inconsistent with the current wallpaper.

Prevent an older post-render task from writing its theme after a newer wallpaper request has started.

Prompt for AI agents
In `wallr-core/src/daemon/mod.rs` at lines 2891-2905:

**Older theme overwrites newer one**

When an earlier wallpaper’s provider or hook is still running when a newer wallpaper request starts its theme work, the generation check runs only before `set_wallpaper_blocking`; the earlier task can finish afterward and overwrite the newer wallpaper’s theme, leaving colors inconsistent with the current wallpaper.

Prevent an older post-render task from writing its theme after a newer wallpaper request has started.

let mut tmp = vec![0u8; size];
let ok = animated.decompress_into(index, &mut tmp);
if ok {
let mut mapped = slice.get_mapped_range_mut().unwrap();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High · Map failure aborts playback

When wgpu fails to map the staging buffer, the map callback result and device-poll error are ignored, so get_mapped_range_mut().unwrap() panics; in release builds the detached playback task aborts the daemon instead of handling the GPU error.

Check and handle the map callback and device-poll results before accessing the mapped range.

Also at wallr-core/src/daemon/mod.rs:1879.

Prompt for AI agents
In `wallr-core/src/daemon/mod.rs` at line 1878:

**Map failure aborts playback**

When wgpu fails to map the staging buffer, the map callback result and device-poll error are ignored, so `get_mapped_range_mut().unwrap()` panics; in release builds the detached playback task aborts the daemon instead of handling the GPU error.

Check and handle the map callback and device-poll results before accessing the mapped range.

Also at `wallr-core/src/daemon/mod.rs:1879`.

Comment on lines +1875 to +1878
let mut tmp = vec![0u8; size];
let ok = animated.decompress_into(index, &mut tmp);
if ok {
let mut mapped = slice.get_mapped_range_mut().unwrap();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · GIF uploads churn memory

When a GIF uses the direct-upload playback path, the path allocates a full-frame temporary Vec for each GIF frame, decompresses into it, then copies it into the mapped staging buffer. Large animations incur recurring allocation and memory-bandwidth overhead that can undermine smooth playback and exhaust memory.

Decompress directly into the mapped staging buffer without allocating and copying a full-frame temporary buffer.

Prompt for AI agents
In `wallr-core/src/daemon/mod.rs` at lines 1875-1878:

**GIF uploads churn memory**

When a GIF uses the direct-upload playback path, the path allocates a full-frame temporary `Vec` for each GIF frame, decompresses into it, then copies it into the mapped staging buffer. Large animations incur recurring allocation and memory-bandwidth overhead that can undermine smooth playback and exhaust memory.

Decompress directly into the mapped staging buffer without allocating and copying a full-frame temporary buffer.

};
command.stdout(Stdio::null()).stderr(Stdio::null());
if let Ok(mut child) = command.spawn() {
let _ = wait_with_timeout(&mut child, RELOAD_TIMEOUT, cmd);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Timed-out reloads report success

When a configured reload command exceeds RELOAD_TIMEOUT, run_reload_list discards the result of wait_with_timeout and returns Ok(()), so a reload command killed after its timeout is reported as successful and leaves no diagnostic for the operator.

Log timed-out reload commands and return an error or otherwise expose the timeout to the caller while continuing to ignore expected pkill nonzero exits.

Prompt for AI agents
In `wallr-core/src/theme/mod.rs` at line 428:

**Timed-out reloads report success**

When a configured reload command exceeds `RELOAD_TIMEOUT`, `run_reload_list` discards the result of `wait_with_timeout` and returns `Ok(())`, so a reload command killed after its timeout is reported as successful and leaves no diagnostic for the operator.

Log timed-out reload commands and return an error or otherwise expose the timeout to the caller while continuing to ignore expected `pkill` nonzero exits.


fn default_socket() -> String {
"/tmp/wallr.sock".to_string()
"$XDG_RUNTIME_DIR/wallr.sock".to_string()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Upgrade starts a second daemon

When a config omits daemon.socket and a 0.6.4 daemon is still running during an upgrade, a config without an explicit daemon.socket now expands to $XDG_RUNTIME_DIR/wallr.sock, while a still-running 0.6.4 daemon listens on /tmp/wallr.sock. The new CLI checks only the new path, so wallr set starts another daemon and both can control the same outputs.

Preserve the legacy default or detect and migrate an active daemon on the old socket before auto-starting a new one.

Prompt for AI agents
In `wallr-common/src/config.rs` at line 175:

**Upgrade starts a second daemon**

When a config omits `daemon.socket` and a 0.6.4 daemon is still running during an upgrade, a config without an explicit `daemon.socket` now expands to `$XDG_RUNTIME_DIR/wallr.sock`, while a still-running 0.6.4 daemon listens on `/tmp/wallr.sock`. The new CLI checks only the new path, so `wallr set` starts another daemon and both can control the same outputs.

Preserve the legacy default or detect and migrate an active daemon on the old socket before auto-starting a new one.

Comment thread Cargo.toml
categories = ["command-line-utilities", "rendering"]
keywords = ["wallpaper", "wayland", "wgpu", "daemon", "matugen"]
rust-version = "1.85"
rust-version = "1.88"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ Low · Contributor setup advertises older Rust

When a contributor follows the documented 1.85 minimum, contributors following CONTRIBUTING.md can install Rust 1.85, but Cargo rejects the workspace because the declared minimum is now 1.88, so the documented setup does not build the project.

Update the contributor setup requirement to Rust 1.88 or newer.

Prompt for AI agents
In `Cargo.toml` at line 14:

**Contributor setup advertises older Rust**

When a contributor follows the documented 1.85 minimum, contributors following `CONTRIBUTING.md` can install Rust 1.85, but Cargo rejects the workspace because the declared minimum is now 1.88, so the documented setup does not build the project.

Update the contributor setup requirement to Rust 1.88 or newer.

Comment on lines +311 to +314
// Do not skip cached frames without consuming the decoder: the
// decoded stream position and `next_index` must stay in lockstep.
// Otherwise a later uncached tail frame is stored under the wrong
// index after a loop wrap. Re-decoding a cached prefix still uses

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Cached GIF loops decode frames again

When playback wraps an animation whose frames are already fully cached, after a full loop, ensure_upto restarts the GIF decoder and decodes each cached frame again to keep decoder position synchronized, even though playback can return those frames from cache. Fully cached animations therefore spend every loop decoding the animation a second time, increasing CPU use and risking playback stutter.

Use cached frames without decoding them when no uncached tail needs the decoder, and restart/advance the decoder only when an uncached frame must be produced.

Prompt for AI agents
In `wallr-core/src/animated/mod.rs` at lines 311-314:

**Cached GIF loops decode frames again**

When playback wraps an animation whose frames are already fully cached, after a full loop, `ensure_upto` restarts the GIF decoder and decodes each cached frame again to keep decoder position synchronized, even though playback can return those frames from cache. Fully cached animations therefore spend every loop decoding the animation a second time, increasing CPU use and risking playback stutter.

Use cached frames without decoding them when no uncached tail needs the decoder, and restart/advance the decoder only when an uncached frame must be produced.

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