Skip to content

image-copy: Fix panics on capture failure paths - #2743

Open
minyek wants to merge 1 commit into
pop-os:masterfrom
minyek:fix-image-copy-capture-panics
Open

image-copy: Fix panics on capture failure paths#2743
minyek wants to merge 1 commit into
pop-os:masterfrom
minyek:fix-image-copy-capture-panics

Conversation

@minyek

@minyek minyek commented Aug 15, 2026

Copy link
Copy Markdown

Summary

Fixes two compositor-crash panics and a stuck-session bug in the
image-copy-capture failure handling.

Workspace captures: session removed from the wrong holder.
render_workspace_to_buffer's constraints-failure path called
output.remove_session(), but workspace-scope sessions live in
Workspace::image_copy. If the output never hosted an output-scope capture,
the unguarded ImageCopySessionsData unwrap panicked — reachable by any
workspace screencast frame arriving while the output has no current mode
(e.g. it is being unplugged or disabled). Otherwise the retain() was a
silent no-op: the workspace's owned Session was never dropped, so the
client never received stopped and kept submitting frames to a dead
session, each failing as unknown. The session is now removed from the
workspace; dropping it stops the session and fails the frame as stopped.

Unguarded holder unwraps. remove_session/remove_cursor_session on
Output and CosmicSurface now tolerate a missing ImageCopySessionsData,
making removal idempotent (matching the existing sessions()/remove_frame
style).

Renderer-creation failure during constraint negotiation.
constraints_for_output/constraints_for_toplevel unwrapped
offscreen_renderer(), which can fail (e.g. after a GPU reset — a case the
render-path callers already handle gracefully since 9514b49). They now fail
the capture instead of panicking.

Testing

The failure paths were verified against smithay's session/frame lifecycle:
dropping the workspace's owned Session fails its active frames as stopped
and sends stopped, and a None from the constraints functions produces a
stopped session at creation and is handled by the existing else-branches in
the render-path callers. Normal capture behaviour is unchanged — the diff
only touches failure paths.

AI assistance

This work was developed with AI assistance (Claude Code); use of AI-generated
code is disclosed in the commit messages per the contribution guidelines. All
changes have been reviewed and are understood by the author.

Checklist

  • I have disclosed use of any AI generated code in my commit messages.
  • I understand these changes in full and will be able to respond to review comments.
  • My change is accurately described in the commit message.
  • My contribution is tested and working as described.
  • I have read the Developer Certificate of Origin and certify my contribution under its conditions.

render_workspace_to_buffer's constraints-failure path removed the
session from the workspace's Output, but workspace-scope sessions live
in Workspace::image_copy. On an output that never hosted an output-scope
capture, the ImageCopySessionsData unwrap panicked — a workspace-scope
screencast frame arriving while the output has no current mode crashed
the compositor. On an output that had, the retain() was a silent no-op:
the workspace's owned Session was never dropped, so the client never
received `stopped` and kept submitting frames to the dead session, each
failing as `unknown`. Remove from the workspace instead: dropping the
owned Session stops the session and fails the frame as `stopped`.

Harden the remaining paths of the same shape:
- SessionHolder::remove_session/remove_cursor_session on Output and
  CosmicSurface tolerate a missing ImageCopySessionsData instead of
  unwrapping, making removal idempotent.
- constraints_for_output/constraints_for_toplevel no longer unwrap
  offscreen_renderer(); renderer-creation failure (e.g. after a GPU
  reset) now fails the capture instead of panicking, matching the
  render-path callers.

Developed with AI assistance (Claude Code); all changes have been reviewed and
are understood by the author.
})
.unwrap();
.inspect_err(|err| tracing::warn!(?err, "Couldn't use node for screencopy"))
.ok()?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We shouldn't run into a situation where we have an invalid node for a given output since the renderer is created on the main thread. This points to a deeper issue we should fix instead. Ping @ids1024

})
.unwrap();
.inspect_err(|err| tracing::warn!(?err, "Couldn't use node for screencopy"))
.ok()?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same here.

{
workspace.remove_session(session);
}
frame.fail(CaptureFailureReason::Stopped);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think we need to explicitly call fail, if we drop the session.

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.

2 participants