Fixing GNOME Snapshot's PipeWire segfault on NixOS and OpenCode with Deepseek V4 Flash
TL;DR: GNOME Snapshot segfaults (SIGSEGV 139) when the camera portal
returns a PipeWire file descriptor. The portal steals the connection's fd,
disconnects the client, and hands the dup to Snapshot — which then tries to
re-register on a dead connection. The fix is a 50-line LD_PRELOAD shim that
intercepts pw_context_connect_fd, closes the poisoned fd, and opens a fresh
connection with pw_context_connect.
The symptom
Running snapshot --debug on NixOS 26.11 (labwc, no GNOME):
2026-07-17T12:01:11.078450Z INFO ashpd::proxy: Calling method org.freedesktop.portal.Camera:OpenPipeWireRemote
2026-07-17T12:01:11.095543Z DEBUG aperture::device_provider: Starting device provider with file descriptor: 28
[1] 25748 segmentation fault snapshot --debug
Exit code 139 (128 + SIGSEGV). The portal successfully returned a file
descriptor, but the moment Snapshot's aperture library tried to use it —
boom.
Investigation
The crash site from the abort message:
process_remote in libpipewire-module-protocol-native.so
A null-pointer dereference at address 0x3 in process_remote. This
function processes the initial protocol handshake between a PipeWire client
and the daemon. The fact that it's crashing there means the connection was
in a bad state before any data was exchanged.
Following the fd
The flow is:
- Snapshot calls
OpenPipeWireRemoteon the portal's D-Bus Camera
interface - The portal frontend (
xdg-desktop-portal) calls into the portal-gnome
backend, which opens a real PipeWire connection viapw_context_connect() - Portal-gnome obtains a fd for that connection, calls
pw_core_steal_fd()to steal it from thepw_core, then passes it back
through the portal frontend - The portal frontend dups the fd and sends it over D-Bus to Snapshot
- Snapshot calls
pw_context_connect_fd()on the received fd - Segfault at
process_remote
The disconnect
The key insight is in step 3. After pw_core_steal_fd(), the portal
destroys its pw_core remote via pipewire_remote_destroy(). This sends a
DISCONNECT message to the PipeWire daemon over the same protocol
connection. Even though the fd was stolen from the pw_core struct (the
field is set to -1), the protocol connection's socket is still the same
file description — the dup() in step 4 creates a new file descriptor, but
it points to the same open socket.
So by the time Snapshot gets its dup'd fd, the daemon has already processed
the DISCONNECT and marked that connection as defunct. When Snapshot'spw_context_connect_fd() runs its first process_remote, the protocol
state machine is in an unexpected state, reads a null pointer, and
segfaults.
Why pw-cli works
pw-cli creates its own pw_context and calls pw_context_connect()
directly — it never goes through the portal. The portal's fd is the whole
problem.
The fix: LD_PRELOAD shim
The simplest fix: intercept pw_context_connect_fd() via LD_PRELOAD,
close the poisoned portal fd, and call pw_context_connect() to open a
fresh connection from scratch.
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
struct pw_core;
struct pw_context;
struct pw_properties;
static struct pw_core* (*real_connect_fd)(struct pw_context *, int,
struct pw_properties *, size_t) = NULL;
static struct pw_core* (*real_connect)(struct pw_context *,
struct pw_properties *, size_t) = NULL;
struct pw_core *pw_context_connect_fd(struct pw_context *context, int fd,
struct pw_properties *properties,
size_t user_data_size)
{
struct pw_core *core = NULL;
if (!real_connect) {
void *handle = dlopen("libpipewire-0.3.so.0", RTLD_LAZY | RTLD_NOLOAD);
if (handle) {
real_connect = dlsym(handle, "pw_context_connect");
dlclose(handle);
}
if (!real_connect) {
real_connect = dlsym(RTLD_DEFAULT, "pw_context_connect");
}
}
if (real_connect) {
close(fd);
core = real_connect(context, properties, user_data_size);
return core;
}
if (!real_connect_fd) {
void *handle = dlopen("libpipewire-0.3.so.0", RTLD_LAZY | RTLD_NOLOAD);
if (handle) {
real_connect_fd = dlsym(handle, "pw_context_connect_fd");
dlclose(handle);
}
}
if (real_connect_fd) {
core = real_connect_fd(context, fd, properties, user_data_size);
return core;
}
return NULL;
}
The dlsym gotcha
Note the use of dlopen("libpipewire-0.3.so.0", RTLD_NOLOAD) +dlsym(handle, ...). This is important: dlsym(RTLD_NEXT, ...) forpw_context_connect fails from an LD_PRELOAD shim on this system. The
exact reason is unclear — the symbol is a global function exported in.dynsym — but dlopen + dlsym works reliably.
NixOS packaging
On NixOS, the fix is integrated as a Nix derivation that compiles the C
source and a wrapper that injects LD_PRELOAD:
snapshotPwFixSrc = ./snapshot-pw-fix.c;
snapshotPwFix = pkgs.stdenv.mkDerivation {
name = "snapshot-pw-fix";
src = snapshotPwFixSrc;
dontUnpack = true;
buildPhase = ''
${pkgs.gcc}/bin/gcc -shared -fPIC -o snapshot-pw-fix.so $src -ldl
'';
installPhase = ''
mkdir -p $out/lib
cp snapshot-pw-fix.so $out/lib/
'';
};
snapshotPwWrapped = pkgs.writeShellScriptBin "snapshot" ''
export LD_PRELOAD=${snapshotPwFix}/lib/snapshot-pw-fix.so''${LD_PRELOAD:+:$LD_PRELOAD}
exec ${pkgs.snapshot}/bin/snapshot "$@"
'';
Then snapshotPwWrapped replaces snapshot in environment.systemPackages.
Two other requirements
Two things needed to get this far in the first place:
xdg-desktop-portal-gnomeas a Camera portal backend. The GTK
portal does not implementorg.freedesktop.impl.portal.Camera. Only the
GNOME portal does. Without it,OpenPipeWireRemotereturns an error
before the crash even happens.GST_PLUGIN_SYSTEM_PATH_1_0must includegst-plugins-bad.
Snapshot'saperturelibrary needscamerabinfromgst-plugins-bad.
Without it, Snapshot panics on startup. The NixOS binary wrapper for
snapshot only setsGDK_PIXBUF_MODULE_FILE, so the GStreamer paths must
be set separately or compiled into the system environment.
Result
2026-07-17T12:35:18.770387Z INFO ashpd::proxy: Calling method org.freedesktop.portal.Camera:OpenPipeWireRemote
2026-07-17T12:35:18.784920Z DEBUG aperture::device_provider: Starting device provider with file descriptor: 31
2026-07-17T12:35:18.792816Z DEBUG aperture::device_provider: Camera found: hm1091_techfront (V4L2)
2026-07-17T12:35:18.796324Z DEBUG snapshot::widgets::camera: Device provider started
2026-07-17T12:35:19.068207Z DEBUG aperture::viewfinder: Trying to set camerabin state to Playing
The camera (hm1091_techfront, 1280×720 JPEG) enumerates, the device
provider starts, and Snapshot begins streaming. No crash.