Skip to content

Commit

Permalink
audio-playback: Add support for snapped pipewire
Browse files Browse the repository at this point in the history
A snapped pipewire requires being able to create the pulse
folder inside the global XDG_RUNTIME_DIR folder (not the snap
own folder). Also, it requires to be able to create both the
pipewire-[0-9]-manager and its lock file.

This patch adds that capability to the audio-playback slot;
thus, a snap offering that slot will be able to create the
corresponding global sockets.
  • Loading branch information
sergio-costas committed Sep 6, 2024
1 parent 0dfd94b commit a23df8e
Show file tree
Hide file tree
Showing 4 changed files with 353 additions and 12 deletions.
9 changes: 1 addition & 8 deletions interfaces/builtin/audio_playback.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,7 @@ owner /{,var/}run/pulse/** rwk,
owner /run/user/[0-9]*/ r,
owner /run/user/[0-9]*/pulse/ rw,
# This allows to share screen in Core Desktop
owner /run/user/[0-9]*/pipewire-[0-9] rwk,
# This allows wireplumber to read the pulseaudio
# configuration if pipewire runs inside a container
/etc/pulse/ r,
/etc/pulse/** r,
owner /run/user/[0-9]*/pulse/** rw,
`

const audioPlaybackPermanentSlotSecComp = `
Expand Down
4 changes: 0 additions & 4 deletions interfaces/builtin/audio_playback_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ func (s *AudioPlaybackInterfaceSuite) TestSecCompOnClassic(c *C) {
c.Assert(spec.AddPermanentSlot(s.iface, s.classicSlotInfo), IsNil)
c.Assert(spec.SecurityTags(), HasLen, 0)

c.Assert(spec.SnippetForTag("snap.audio-playback.app1"), Not(testutil.Contains), "owner /run/user/[0-9]*/pipewire-[0-9] rwk,\n")
c.Check(spec.SnippetForTag("snap.audio-playback.app1"), Not(testutil.Contains), "/etc/pulse/ r,\n")
c.Check(spec.SnippetForTag("snap.audio-playback.app1"), Not(testutil.Contains), "/etc/pulse/** r,\n")
}
Expand Down Expand Up @@ -168,9 +167,6 @@ func (s *AudioPlaybackInterfaceSuite) TestAppArmor(c *C) {
c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil)
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.audio-playback.app1"})
c.Check(spec.SnippetForTag("snap.audio-playback.app1"), testutil.Contains, "capability setuid,\n")
c.Assert(spec.SnippetForTag("snap.audio-playback.app1"), testutil.Contains, "owner /run/user/[0-9]*/pipewire-[0-9] rwk,\n")
c.Check(spec.SnippetForTag("snap.audio-playback.app1"), testutil.Contains, "/etc/pulse/ r,\n")
c.Check(spec.SnippetForTag("snap.audio-playback.app1"), testutil.Contains, "/etc/pulse/** r,\n")
}

func (s *AudioPlaybackInterfaceSuite) TestAppArmorOnClassic(c *C) {
Expand Down
151 changes: 151 additions & 0 deletions interfaces/builtin/pipewire.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2018 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package builtin

import (
"strings"

"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/interfaces/apparmor"
"github.com/snapcore/snapd/interfaces/seccomp"
"github.com/snapcore/snapd/snap"
)

// The audio-playback interface is the companion interface to the audio-record
// interface. The design of this interface is based on the idea that the slot
// implementation (eg pulseaudio) is expected to query snapd on if the
// audio-record slot is connected or not and the audio service will mediate
// recording (ie, the rules below allow connecting to the audio service, but do
// not implement enforcement rules; it is up to the audio service to provide
// enforcement). If other audio recording servers require different security
// policy for record and playback (eg, a different socket path), then those
// accesses will be added to this interface.

const pipewireSummary = `allows access to the pipewire sockets, and offer them`

const pipewireBaseDeclarationSlots = `
pipewire:
deny-auto-connection: true
`

const pipewireConnectedPlugAppArmor = `
# Allow communicating with pipewire service
/{run,dev}/shm/pulse-shm-* mrwk,
owner /run/user/[0-9]*/ r,
owner /run/user/[0-9]*/pipewire-[0-9] rw,
owner /run/user/[0-9]*/pipewire-[0-9]-manager rw,
`

const pipewireConnectedPlugAppArmorCore = `
owner /run/user/[0-9]*/###SLOT_SECURITY_TAGS###/pipewire-[0-9] rw,
owner /run/user/[0-9]*/###SLOT_SECURITY_TAGS###/pipewire-[0-9]-manager rw,
`

const pipewireConnectedPlugSecComp = `
shmctl
`

const pipewirePermanentSlotAppArmor = `
capability sys_nice,
capability sys_resource,
owner @{PROC}/@{pid}/exe r,
/etc/machine-id r,
# For udev
network netlink raw,
/sys/devices/virtual/dmi/id/sys_vendor r,
/sys/devices/virtual/dmi/id/bios_vendor r,
/sys/**/sound/** r,
# Shared memory based communication with clients
/{run,dev}/shm/pulse-shm-* mrwk,
owner /run/user/[0-9]*/pipewire-[0-9] rwk,
owner /run/user/[0-9]*/pipewire-[0-9]-manager rwk,
# This allows wireplumber to read the pulseaudio
# configuration if pipewire runs inside a container
/etc/pulse/ r,
/etc/pulse/** r,
`

const pipewirePermanentSlotSecComp = `
# The following are needed for UNIX sockets
personality
setpriority
bind
listen
accept
accept4
shmctl
# libudev
socket AF_NETLINK - NETLINK_KOBJECT_UEVENT
`

type pipewireInterface struct{}

func (iface *pipewireInterface) Name() string {
return "pipewire"
}

func (iface *pipewireInterface) StaticInfo() interfaces.StaticInfo {
return interfaces.StaticInfo{
Summary: pipewireSummary,
ImplicitOnClassic: false,
ImplicitOnCore: false,
BaseDeclarationSlots: pipewireBaseDeclarationSlots,
}
}

func (iface *pipewireInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
spec.AddSnippet(pipewireConnectedPlugAppArmor)
if !implicitSystemConnectedSlot(slot) {
old := "###SLOT_SECURITY_TAGS###"
new := "snap." + slot.Snap().InstanceName() // forms the snap-instance-specific subdirectory name of /run/user/*/ used for XDG_RUNTIME_DIR
snippet := strings.Replace(pipewireConnectedPlugAppArmorCore, old, new, -1)
spec.AddSnippet(snippet)
}
return nil
}

func (iface *pipewireInterface) AppArmorPermanentSlot(spec *apparmor.Specification, slot *snap.SlotInfo) error {
spec.AddSnippet(pipewirePermanentSlotAppArmor)
return nil
}

func (iface *pipewireInterface) SecCompConnectedPlug(spec *seccomp.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
spec.AddSnippet(pipewireConnectedPlugSecComp)
return nil
}

func (iface *pipewireInterface) SecCompPermanentSlot(spec *seccomp.Specification, slot *snap.SlotInfo) error {
spec.AddSnippet(pipewirePermanentSlotSecComp)
return nil
}

func (iface *pipewireInterface) AutoConnect(*snap.PlugInfo, *snap.SlotInfo) bool {
return true
}

func init() {
registerIface(&pipewireInterface{})
}
201 changes: 201 additions & 0 deletions interfaces/builtin/pipewire_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2018 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package builtin_test

import (
. "gopkg.in/check.v1"

"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/interfaces/apparmor"
"github.com/snapcore/snapd/interfaces/builtin"
"github.com/snapcore/snapd/interfaces/seccomp"
"github.com/snapcore/snapd/interfaces/udev"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/testutil"
)

type pipewireInterfaceSuite struct {
iface interfaces.Interface
coreSlotInfo *snap.SlotInfo
coreSlot *interfaces.ConnectedSlot
classicSlotInfo *snap.SlotInfo
classicSlot *interfaces.ConnectedSlot
plugInfo *snap.PlugInfo
plug *interfaces.ConnectedPlug
}

var _ = Suite(&pipewireInterfaceSuite{
iface: builtin.MustInterface("pipewire"),
})

const pipewireMockPlugSnapInfoYaml = `name: consumer
version: 1.0
apps:
app:
command: foo
plugs: [pipewire]
`

// a pipewire slot on a pipewire snap (as installed on a core/all-snap system)
const pipewireMockCoreSlotSnapInfoYaml = `name: pipewire
version: 1.0
apps:
app1:
command: foo
slots: [pipewire]
`

// a pipewire slot on the core snap (as automatically added on classic)
const pipewireMockClassicSlotSnapInfoYaml = `name: core
version: 0
type: os
slots:
pipewire:
interface: pipewire
`

func (s *pipewireInterfaceSuite) SetUpTest(c *C) {
s.coreSlot, s.coreSlotInfo = MockConnectedSlot(c, pipewireMockCoreSlotSnapInfoYaml, nil, "pipewire")
s.classicSlot, s.classicSlotInfo = MockConnectedSlot(c, pipewireMockClassicSlotSnapInfoYaml, nil, "pipewire")
s.plug, s.plugInfo = MockConnectedPlug(c, pipewireMockPlugSnapInfoYaml, nil, "pipewire")
}

func (s *pipewireInterfaceSuite) TestName(c *C) {
c.Assert(s.iface.Name(), Equals, "pipewire")
}

func (s *pipewireInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.coreSlotInfo), IsNil)
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.classicSlotInfo), IsNil)
}

func (s *pipewireInterfaceSuite) TestSanitizePlug(c *C) {
c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil)
}

func (s *pipewireInterfaceSuite) TestSecComp(c *C) {
restore := release.MockOnClassic(false)
defer restore()

// connected plug to core slot
spec := seccomp.NewSpecification(s.plug.AppSet())
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil)
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "shmctl\n")

// connected core slot to plug
spec = seccomp.NewSpecification(s.coreSlot.AppSet())
c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.coreSlot), IsNil)
c.Assert(spec.SecurityTags(), HasLen, 0)

// permanent core slot
spec = seccomp.NewSpecification(s.coreSlot.AppSet())
c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil)
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.pipewire.app1"})
c.Assert(spec.SnippetForTag("snap.pipewire.app1"), testutil.Contains, "listen\n")
}

func (s *pipewireInterfaceSuite) TestSecCompOnClassic(c *C) {
restore := release.MockOnClassic(true)
defer restore()

// connected plug to classic slot
spec := seccomp.NewSpecification(s.plug.AppSet())
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.classicSlot), IsNil)
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "shmctl\n")

c.Assert(spec.SnippetForTag("snap.consumer.app"), Not(testutil.Contains), "owner /run/user/[0-9]*/snap.pipewire/pulse/ r,\n")
c.Assert(spec.SnippetForTag("snap.consumer.app"), Not(testutil.Contains), "owner /run/user/[0-9]*/snap.pipewire/pulse/native rwk,\n")
c.Assert(spec.SnippetForTag("snap.consumer.app"), Not(testutil.Contains), "owner /run/user/[0-9]*/snap.pipewire/pulse/pid r,\n")

// connected classic slot to plug
spec = seccomp.NewSpecification(s.classicSlot.AppSet())
c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.classicSlot), IsNil)
c.Assert(spec.SecurityTags(), HasLen, 0)

// permanent classic slot
spec = seccomp.NewSpecification(s.classicSlot.AppSet())
c.Assert(spec.AddPermanentSlot(s.iface, s.classicSlotInfo), IsNil)
c.Assert(spec.SecurityTags(), HasLen, 0)

c.Assert(spec.SnippetForTag("snap.pipewire.app1"), Not(testutil.Contains), "owner /run/user/[0-9]*/pipewire-[0-9] rwk,\n")
c.Check(spec.SnippetForTag("snap.pipewire.app1"), Not(testutil.Contains), "/etc/pulse/ r,\n")
c.Check(spec.SnippetForTag("snap.pipewire.app1"), Not(testutil.Contains), "/etc/pulse/** r,\n")
}

func (s *pipewireInterfaceSuite) TestAppArmor(c *C) {
restore := release.MockOnClassic(false)
defer restore()

// connected plug to core slot
spec := apparmor.NewSpecification(s.plug.AppSet())
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil)
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/{run,dev}/shm/pulse-shm-* mrwk,\n")
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "owner /run/user/[0-9]*/snap.pipewire/pipewire-[0-9] rw,\n")
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "owner /run/user/[0-9]*/snap.pipewire/pipewire-[0-9]-manager rw,\n")

// connected core slot to plug
spec = apparmor.NewSpecification(s.coreSlot.AppSet())
c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.coreSlot), IsNil)
c.Assert(spec.SecurityTags(), HasLen, 0)

// permanent core slot
spec = apparmor.NewSpecification(s.coreSlot.AppSet())
c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil)
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.pipewire.app1"})
c.Assert(spec.SnippetForTag("snap.pipewire.app1"), testutil.Contains, "owner /run/user/[0-9]*/pipewire-[0-9] rwk,\n")
c.Assert(spec.SnippetForTag("snap.pipewire.app1"), testutil.Contains, "owner /run/user/[0-9]*/pipewire-[0-9]-manager rwk,\n")
}

func (s *pipewireInterfaceSuite) TestAppArmorOnClassic(c *C) {
restore := release.MockOnClassic(true)
defer restore()

// connected plug to classic slot
spec := apparmor.NewSpecification(s.plug.AppSet())
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.classicSlot), IsNil)
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/{run,dev}/shm/pulse-shm-* mrwk,\n")
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "owner /run/user/[0-9]*/pipewire-[0-9] rw,\n")
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "owner /run/user/[0-9]*/pipewire-[0-9]-manager rw,\n")

// connected classic slot to plug
spec = apparmor.NewSpecification(s.classicSlot.AppSet())
c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.classicSlot), IsNil)
c.Assert(spec.SecurityTags(), HasLen, 0)

// permanent classic slot
spec = apparmor.NewSpecification(s.classicSlot.AppSet())
c.Assert(spec.AddPermanentSlot(s.iface, s.classicSlotInfo), IsNil)
c.Assert(spec.SecurityTags(), HasLen, 0)
}

func (s *pipewireInterfaceSuite) TestUDev(c *C) {
spec := udev.NewSpecification(s.coreSlot.AppSet())
c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil)
c.Assert(spec.Snippets(), HasLen, 0)
}

func (s *pipewireInterfaceSuite) TestInterfaces(c *C) {
c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface)
}

0 comments on commit a23df8e

Please sign in to comment.