Skip to content

Modified pipewire backend to allow selection of pipewire stream or sink - #2279

Closed
gearhead wants to merge 50 commits into
mikebrady:developmentfrom
gearhead:master
Closed

gearhead wants to merge 50 commits into
mikebrady:developmentfrom
gearhead:master

Conversation

@gearhead

@gearhead gearhead commented Sep 4, 2026

Copy link
Copy Markdown

Allows Shairport-sync tocontrol native pipewire volumes
.
in the pipewire { } block this parameter is allowed:
mixer_type = "sink"; // pipewire_mixer_type "stream" or "sink"
This parameter changes the Shairport-sync volume to a native PipeWire output either a stream or sink to control the actual output device directly.

Pull Requests

Thanks for helping to improve Shairport Sync. This template is an effort to make a Pull Request as trouble-free and useful as possible.

⚠️ Important: Target Branch

Please ensure your pull request targets the development branch, not master.

Contributions should be submitted against the development branch for review and testing before they are merged into the main release branch.


Description

Please provide a brief description of your changes:
This adds a pipewire/wireplumber mixer to shairport-sync to allow the shairport-sync volume control to directly control either the pipewire stream or sink.
The impetus for this was to allow our headless music appliance on an RPi to directly control the pipewire sink volume. This is similar to having shairport-sync control the alsa hardware mixer when it is configured to do so, but with the abstraction of pipewire.

the config needs to have the backend set to "pipewire" and then this in the backend section

pipewire =
{
    application_name = "Shairport Sync";
    node_name = "Shairport Sync";
//    sink_target = "<sink target name>"; // if not set, picks up PipeWire's default sink
    mixer_type = "sink"; // pipewire mixer_type either "sink" or "stream"
}

Type of Change

Please delete options that are not relevant:

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring
  • Performance improvement
  • Build/CI configuration change

Related Issue

If this PR addresses an existing issue, please link it here:

Fixes #(issue number)

Testing

Please describe the tests you ran to verify your changes:

  • Tested on Linux
  • Tested on FreeBSD
  • Tested on OpenBSD
  • Tested with AirPlay 2
  • Tested with classic AirPlay

Test Configuration:

  • Hardware/Platform: RpiOS arm64 64 bit and armhf 32 bit, Arch Linux x86_64
  • OS Version: RPiOS Trixie, Arch
  • Build flags used:
    on RPiOS
    --prefix=/usr
    --sysconfdir=/etc
    --with-alsa
    --with-stdout
    --with-pipe
    --with-pipewire
    --with-avahi
    --with-pkg-config
    --with-configfiles
    --with-mqtt-client
    --with-ssl=openssl
    --with-metadata
    --with-convolution
    --with-soxr
    --with-systemd-startup
    --with-dbus-interface
    --with-airplay-2
    --with-ffmpeg

On Arch:
--prefix=/usr
--sysconfdir=/etc
--with-alsa
--with-stdout
--with-pipe
--with-pipewire
--with-avahi
--with-dns_sd
--with-pkg-config
--with-configfiles
--with-mqtt-client
--with-ssl=openssl
--with-metadata
--with-soxr
--with-systemd-startup
--with-dbus-interface
--with-airplay-2

Checklist

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (if applicable)
  • My changes generate no new warnings
  • I have tested my changes and verified they work as expected
  • I have checked that my PR targets the development branch

Additional Notes

Any additional information, context, or screenshots that would help reviewers understand your changes:

mikebrady and others added 30 commits February 14, 2026 12:54
…nt debug messages; change how blocks of audio very far in the future are handled; add pw=true to the Bonjour information for classic AirPlay when built for AirPlay 2.
The two error branches in mqtt_publish() logged at debug(1), which is
invisible at default syslog verbosity. A silently-dropped MQTT publish
is the exact failure mode users report as 'MQTT just stopped working'
with no errors anywhere in the logs. Raising these to inform() makes
such drops legible at default verbosity and includes the target topic
plus mosquitto_strerror() text in the non-NO_CONN branch so the
diagnostic actually names the failing call site.
…h-errors

MQTT: promote publish-error log paths to inform() for debuggability
…orm-publish-errors

Revert "mqtt: promote publish-error log paths to inform() for debuggability"

This needs to be done to the `development` branch.
Added caution regarding installing systemd-dev on backported systems.
Update settings document.
Update of configuration flag documentation.
Added a markdown test section to the Bug Report template.
Removed markdown test section from Bug Report template.
Summary of changes:
* If the output rate is different to the input rate (e.g. 48,000 vs 44,100), look for the deepest output bit depth so that the transcoder has the maximum dynamic range available to minimise transcoding inaccuracies.
* Fix a bug whereby the audio backend latency offset was mishandled in AirPlay 2 operation.
* Fix an output format selection bug.
* Update the README reference to the MPRIS specification to the latest version.
* Remove the explicit `-` for `xxd` to read from `STDIN`. It's not necessary, and removing it makes it easier to package Shairport Sync on other build systems, such as OpenWrt.
mikebrady and others added 20 commits August 25, 2026 09:02
Updated the README to clarify remote control support for AirPlay clients.
…HSA-jgrm-g4c3-wq2r)

In handle_setup_2(), each element of the SETUP plist's
timingPeerInfo.Addresses array is read with plist_get_string_val(). That
function only writes *val when the node is a PLIST_STRING; for any other
node type ip_address stays NULL and is then passed to strncat(), which
calls strlen(NULL) -> NULL-pointer dereference.

A SETUP whose timingPeerInfo.Addresses array contains a non-string element
(e.g. an integer) therefore crashes the RTSP thread. As Shairport Sync
installs no SIGSEGV handler and has no per-connection fault isolation, the
crash terminates the whole daemon, ending audio for every connected client.

Guard the append with a NULL check so a non-string element is skipped and
the rest of the message is processed normally, rather than crashing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-6fg5-25pp)

handle_setup_2() reads the "shk" session key from the SETUP plist with
plist_get_data_val(), which returns the data's length in item_value, but
that length is never checked. conn->session_key is later used directly as a
fixed 32-byte ChaCha20-Poly1305-IETF key in rtp.c, where libsodium always
reads crypto_aead_chacha20poly1305_ietf_KEYBYTES (32) bytes. A client that
supplies an "shk" shorter than 32 bytes therefore causes an out-of-bounds
heap read on every realtime-audio packet decrypt (crash, or decrypted
output influenced by adjacent heap bytes).

Reject a session key whose length is not 32 bytes: free it and leave
conn->session_key NULL, matching the existing handling for a missing key
(rtp.c already guards on session_key != NULL before decrypting).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…thods

Backport of the fix merged to development in PR mikebrady#2263. The unrecognised-method
hex-dump fills obf[4096] with two hex chars per input byte for an input clamped
to 4096 bytes, writing up to 8193 bytes -- a pre-auth stack overflow reachable
on AirPlay 2 (fresh connections default to ap_2, bypassing auth). Size the
buffer to hold two hex chars per byte plus the terminating nul.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ge_process

Backport of the fix merged to development in PR mikebrady#2264. message_process() reads
error->value[0] after only checking that the TLVType_Error item is present; a
zero-length Error TLV (07 00) parses to value == NULL, so this is a pre-auth
NULL dereference. Guard with error->size > 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…HSA-536j-295w-5jxr)

In unencrypted_packet_decode(), the ast_uncompressed branch copies length/2
16-bit samples from the received packet into dest (abuf->data), which the caller
allocates for exactly one packet: conn->frames_per_packet *
conn->input_bytes_per_frame bytes. The copy is bounded only by the received
packet length, which is attacker-controlled and never checked against the
allocation, so an oversized uncompressed-PCM packet overflows the heap buffer.

Clamp length to the destination size before copying.

Note: this decode path is only reachable in a classic-AirPlay build using a
built-in decoder (decoder_hammerton / decoder_apple_alac); FFmpeg builds (and
therefore all AirPlay-2 builds) handle uncompressed audio via a different path
and do not call this function.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hang (GHSA-q4w3-3jf5-q43j)

uncompress_nlabel() follows DNS name-compression pointers without any cycle or
hop limit: the target of a pointer is never checked to be earlier in the packet,
and visited offsets are not tracked. A crafted mDNS packet whose compression
pointers form a cycle makes both traversal loops run forever, pinning the mDNS
responder thread at 100% CPU -- a remote, unauthenticated denial of service from
anyone on the local network segment (single UDP packet to 224.0.0.251:5353).

Bound each traversal to pkt_len steps; no legitimate name can exceed the packet
length, so this stops the loop on a malformed/cyclic name while leaving valid
names unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…q4ff-xh78)

rr->data.AAAA.addr is heap-allocated for every parsed or created AAAA record,
but rr_entry_destroy() only frees RR_PTR/RR_TXT/RR_SRV data; RR_AAAA fell
through to the no-op default, leaking the in6_addr allocation. A stream of mDNS
packets containing AAAA records (no reply required) causes a slow memory-
exhaustion denial of service, notable on memory-constrained devices.

Add a RR_AAAA case that frees the address. (RR_A stores its address inline, so
it does not need freeing.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e (GHSA-j3r8-57f9-jqf7)

When recvfrom() returns -1, the mDNS server loop logged the error but continued
on to call mdns_parse_pkt(pkt_buffer, recvsize). recvsize is an ssize_t of -1,
which converts to SIZE_MAX as the size_t pkt_len parameter, so every bounds
check in the parser (label_len(), uncompress_nlabel(), mdns_parse_rr()) is
computed against a wildly out-of-range length -- an out-of-bounds heap read past
the packet buffer, crashing the daemon. This is reachable when recvfrom() fails
with a packet present (e.g. transient ENOBUFS under a UDP flood).

Skip the datagram (continue) when recvfrom() returns a negative size.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…r-obf-pair-nullderef

Backport pre-auth security fixes to master (obf overflow, pairing NULL deref).
Fix NULL deref on non-string timingPeerInfo address in SETUP (master)
Validate shk session key length in SETUP (master)
Bound uncompressed-PCM decode copy to destination size (master)
Bound mDNS compression-pointer following to stop parser hang (master)
Skip datagram on recvfrom() error to avoid SIZE_MAX parse (master)
Free AAAA record address in rr_entry_destroy (master)
….5 upwards across master (previously 5.2.3) an development (previously 5.4-dev) branches.
sink as the volume that shairport-sync controls.
in the pipewire { } block this parameter is allowed:
    mixer_type = "sink"; // pipewire_mixer_type "stream" or "sink"
This parameter changes the SHairport-sync volume to a native pipewire
output either a stream or sink to control the actual output device
directly.
@mikebrady

Copy link
Copy Markdown
Owner

Thanks again for this.

Would it be possible for it to target the development branch, please? It makes it much easier to integrate into the testing and updating process.

@gearhead

gearhead commented Sep 7, 2026

Copy link
Copy Markdown
Author

Oops, I thought that was the branch I targeted... Let me see if I can fix it.

@gearhead
gearhead changed the base branch from master to development September 9, 2026 00:41
@gearhead

gearhead commented Sep 9, 2026

Copy link
Copy Markdown
Author

I opened a new PR. Could not figure out how to revise this one.

@gearhead gearhead closed this Sep 9, 2026
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.

3 participants