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<br>returns a PipeWire file descriptor. The portal steals the connection's fd,<br>disconnects the client, and hands the dup to Snapshot — which then tries to<br>re-register on a dead connection. The fix is a 50-line LD_PRELOAD shim that<br>intercepts pw_context_connect_fd, closes the poisoned fd, and opens a fresh<br>connection with pw_context_connect.<br>The symptom<br>Running snapshot --debug on NixOS 26.11 (labwc, no GNOME):<br>2026-07-17T12:01:11.078450Z INFO ashpd::proxy: Calling method org.freedesktop.portal.Camera:OpenPipeWireRemote<br>2026-07-17T12:01:11.095543Z DEBUG aperture::device_provider: Starting device provider with file descriptor: 28<br>[1] 25748 segmentation fault snapshot --debug<br>Exit code 139 (128 + SIGSEGV). The portal successfully returned a file<br>descriptor, but the moment Snapshot's aperture library tried to use it —<br>boom.<br>Investigation<br>The crash site from the abort message:<br>process_remote in libpipewire-module-protocol-native.so<br>A null-pointer dereference at address 0x3 in process_remote. This<br>function processes the initial protocol handshake between a PipeWire client<br>and the daemon. The fact that it's crashing there means the connection was<br>in a bad state before any data was exchanged.<br>Following the fd<br>The flow is:<br>Snapshot calls OpenPipeWireRemote on the portal's D-Bus Camera<br>interface<br>The portal frontend (xdg-desktop-portal) calls into the portal-gnome<br>backend, which opens a real PipeWire connection via<br>pw_context_connect()<br>Portal-gnome obtains a fd for that connection, calls<br>pw_core_steal_fd() to steal it from the pw_core, then passes it back<br>through the portal frontend<br>The portal frontend dups the fd and sends it over D-Bus to Snapshot<br>Snapshot calls pw_context_connect_fd() on the received fd<br>Segfault at process_remote<br>The disconnect<br>The key insight is in step 3. After pw_core_steal_fd(), the portal<br>destroys its pw_core remote via pipewire_remote_destroy(). This sends a<br>DISCONNECT message to the PipeWire daemon over the same protocol<br>connection. Even though the fd was stolen from the pw_core struct (the<br>field is set to -1), the protocol connection's socket is still the same<br>file description — the dup() in step 4 creates a new file descriptor, but<br>it points to the same open socket.<br>So by the time Snapshot gets its dup'd fd, the daemon has already processed<br>the DISCONNECT and marked that connection as defunct. When Snapshot's<br>pw_context_connect_fd() runs its first process_remote, the protocol<br>state machine is in an unexpected state, reads a null pointer, and<br>segfaults.<br>Why pw-cli works<br>pw-cli creates its own pw_context and calls pw_context_connect()<br>directly — it never goes through the portal. The portal's fd is the whole<br>problem.<br>The fix: LD_PRELOAD shim<br>The simplest fix: intercept pw_context_connect_fd() via LD_PRELOAD,<br>close the poisoned portal fd, and call pw_context_connect() to open a<br>fresh connection from scratch.<br>#define _GNU_SOURCE<br>#include<br>#include<br>#include<br>#include<br>#include<br>#include<br>#include
struct pw_core;<br>struct pw_context;<br>struct pw_properties;
static struct pw_core* (*real_connect_fd)(struct pw_context *, int,<br>struct pw_properties *, size_t) = NULL;<br>static struct pw_core* (*real_connect)(struct pw_context *,<br>struct pw_properties *, size_t) = NULL;
struct pw_core *pw_context_connect_fd(struct pw_context *context, int fd,<br>struct pw_properties *properties,<br>size_t user_data_size)<br>struct pw_core *core = NULL;
if (!real_connect) {<br>void *handle = dlopen("libpipewire-0.3.so.0", RTLD_LAZY | RTLD_NOLOAD);<br>if (handle) {<br>real_connect = dlsym(handle, "pw_context_connect");<br>dlclose(handle);<br>if (!real_connect) {<br>real_connect = dlsym(RTLD_DEFAULT, "pw_context_connect");
if (real_connect) {<br>close(fd);<br>core = real_connect(context, properties, user_data_size);<br>return core;
if (!real_connect_fd) {<br>void *handle = dlopen("libpipewire-0.3.so.0", RTLD_LAZY | RTLD_NOLOAD);<br>if (handle) {<br>real_connect_fd = dlsym(handle, "pw_context_connect_fd");<br>dlclose(handle);<br>if (real_connect_fd) {<br>core = real_connect_fd(context, fd, properties, user_data_size);<br>return core;
return NULL;<br>The dlsym gotcha<br>Note the use of dlopen("libpipewire-0.3.so.0", RTLD_NOLOAD) +<br>dlsym(handle, ...). This is important: dlsym(RTLD_NEXT, ...) for<br>pw_context_connect fails from an LD_PRELOAD shim on this system. The<br>exact reason is unclear — the symbol is a global function exported in<br>.dynsym — but dlopen + dlsym works reliably.<br>NixOS packaging<br>On NixOS, the fix is integrated as a Nix derivation that compiles the C<br>source and a wrapper that injects LD_PRELOAD:<br>snapshotPwFixSrc = ./snapshot-pw-fix.c;<br>snapshotPwFix = pkgs.stdenv.mkDerivation {<br>name = "snapshot-pw-fix";<br>src = snapshotPwFixSrc;<br>dontUnpack = true;<br>buildPhase = ''<br>${pkgs.gcc}/bin/gcc -shared -fPIC -o snapshot-pw-fix.so $src -ldl<br>'';<br>installPhase = ''<br>mkdir -p $out/lib<br>cp snapshot-pw-fix.so $out/lib/<br>'';<br>};<br>snapshotPwWrapped =...