Skip to content

interfaces: add a steam-support interface #11708

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 214 additions & 0 deletions interfaces/builtin/steam_support.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2022 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

const steamSupportSummary = `allow Steam to configure pressure-vessel containers`

const steamSupportBaseDeclarationPlugs = `
steam-support:
allow-installation: false
deny-auto-connection: true
`

const steamSupportBaseDeclarationSlots = `
steam-support:
allow-installation:
slot-snap-type:
- core
deny-auto-connection: true
`

const steamSupportConnectedPlugAppArmor = `
# Allow pressure-vessel to set up its Bubblewrap sandbox.
/sys/kernel/ r,
@{PROC}/sys/kernel/overflowuid r,
@{PROC}/sys/kernel/overflowgid r,
@{PROC}/sys/kernel/sched_autogroup_enabled r,
@{PROC}/pressure/io r,
owner @{PROC}/@{pid}/uid_map rw,
owner @{PROC}/@{pid}/gid_map rw,
owner @{PROC}/@{pid}/setgroups rw,
owner @{PROC}/@{pid}/mounts r,
owner @{PROC}/@{pid}/mountinfo r,

# Create and pivot to the intermediate root
mount options=(rw, rslave) -> /,
mount options=(rw, silent, rslave) -> /,
mount fstype=tmpfs options=(rw, nosuid, nodev) tmpfs -> /tmp/,
mount options=(rw, rbind) /tmp/newroot/ -> /tmp/newroot/,
pivot_root oldroot=/tmp/oldroot/ /tmp/,

# Set up sandbox in /newroot
mount options=(rw, rbind) /oldroot/ -> /newroot/,
mount options=(rw, rbind) /oldroot/dev/ -> /newroot/dev/,
mount options=(rw, rbind) /oldroot/etc/ -> /newroot/etc/,
mount options=(rw, rbind) /oldroot/proc/ -> /newroot/proc/,
mount options=(rw, rbind) /oldroot/sys/ -> /newroot/sys/,
mount options=(rw, rbind) /oldroot/tmp/ -> /newroot/tmp/,
mount options=(rw, rbind) /oldroot/var/ -> /newroot/var/,
mount options=(rw, rbind) /oldroot/var/tmp/ -> /newroot/var/tmp/,
mount options=(rw, rbind) /oldroot/usr/ -> /newroot/run/host/usr/,
mount options=(rw, rbind) /oldroot/etc/ -> /newroot/run/host/etc/,
mount options=(rw, rbind) /oldroot/usr/lib/os-release -> /newroot/run/host/os-release,

# Bubblewrap performs remounts on directories it binds under /newroot
# to fix up the options (since options other than MS_REC are ignored
# when performing a bind mount). Ideally we could do something like:
# remount options=(bind, silent, nosuid, *) /newroot/{,**},
#
# But that is not supported by AppArmor. So we enumerate the possible
# combinations of options Bubblewrap might use.
remount options=(bind, silent, nosuid, rw) /newroot/{,**},
Copy link
Contributor

Choose a reason for hiding this comment

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

We might save some bytes if we generated this list programmatically, but it's probably not worth the effort (and if even, it should be a follow-up).

remount options=(bind, silent, nosuid, rw, nodev) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, noexec) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, nodev, noexec) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, noatime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, nodev, noatime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, noexec, noatime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, nodev, noexec, noatime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, relatime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, nodev, relatime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, noexec, relatime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, nodev, noexec, relatime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, nodev, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, noexec, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, nodev, noexec, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, noatime, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, nodev, noatime, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, noexec, noatime, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, nodev, noexec, noatime, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, relatime, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, nodev, relatime, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, noexec, relatime, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, rw, nodev, noexec, relatime, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, nodev) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, noexec) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, nodev, noexec) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, noatime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, nodev, noatime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, noexec, noatime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, nodev, noexec, noatime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, relatime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, nodev, relatime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, noexec, relatime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, nodev, noexec, relatime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, nodev, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, noexec, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, nodev, noexec, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, noatime, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, nodev, noatime, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, noexec, noatime, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, nodev, noexec, noatime, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, relatime, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, nodev, relatime, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, noexec, relatime, nodiratime) /newroot/{,**},
remount options=(bind, silent, nosuid, ro, nodev, noexec, relatime, nodiratime) /newroot/{,**},

/newroot/** rwkl,
/bindfile* rw,
mount options=(rw, rbind) /oldroot/home/** -> /newroot/home/**,
mount options=(rw, rbind) /oldroot/snap/** -> /newroot/snap/**,
mount options=(rw, rbind) /oldroot/home/**/usr/ -> /newroot/usr/,
mount options=(rw, rbind) /oldroot/home/**/usr/etc/** -> /newroot/etc/**,
mount options=(rw, rbind) /oldroot/home/**/usr/etc/ld.so.cache -> /newroot/run/pressure-vessel/ldso/runtime-ld.so.cache,
mount options=(rw, rbind) /oldroot/home/**/usr/etc/ld.so.conf -> /newroot/run/pressure-vessel/ldso/runtime-ld.so.conf,

mount options=(rw, rbind) /oldroot/etc/machine-id -> /newroot/etc/machine-id,
mount options=(rw, rbind) /oldroot/etc/group -> /newroot/etc/group,
mount options=(rw, rbind) /oldroot/etc/passwd -> /newroot/etc/passwd,
mount options=(rw, rbind) /oldroot/etc/host.conf -> /newroot/etc/host.conf,
mount options=(rw, rbind) /oldroot/etc/hosts -> /newroot/etc/hosts,
mount options=(rw, rbind) /oldroot/**/*resolv.conf -> /newroot/etc/resolv.conf,
mount options=(rw, rbind) /bindfile* -> /newroot/etc/timezone,

mount options=(rw, rbind) /oldroot/run/systemd/journal/socket -> /newroot/run/systemd/journal/socket,
mount options=(rw, rbind) /oldroot/run/systemd/journal/stdout -> /newroot/run/systemd/journal/stdout,

mount options=(rw, rbind) /oldroot/usr/share/fonts/ -> /newroot/run/host/fonts/,
mount options=(rw, rbind) /oldroot/usr/local/share/fonts/ -> /newroot/run/host/local-fonts/,
mount options=(rw, rbind) /oldroot/{var/cache/fontconfig,usr/lib/fontconfig/cache}/ -> /newroot/run/host/fonts-cache/,
mount options=(rw, rbind) /oldroot/home/**/.cache/fontconfig/ -> /newroot/run/host/user-fonts-cache/,
mount options=(rw, rbind) /bindfile* -> /newroot/run/host/font-dirs.xml,

mount options=(rw, rbind) /oldroot/usr/share/icons/ -> /newroot/run/host/share/icons/,
mount options=(rw, rbind) /oldroot/home/**/.local/share/icons/ -> /newroot/run/host/user-share/icons/,

mount options=(rw, rbind) /oldroot/run/user/[0-9]*/wayland-* -> /newroot/run/pressure-vessel/wayland-*,
mount options=(rw, rbind) /oldroot/tmp/.X11-unix/X* -> /newroot/tmp/.X11-unix/X99,
mount options=(rw, rbind) /bindfile* -> /newroot/run/pressure-vessel/Xauthority,

mount options=(rw, rbind) /bindfile* -> /newroot/run/pressure-vessel/pulse/config,
mount options=(rw, rbind) /oldroot/run/user/[0-9]*/pulse/native -> /newroot/run/pressure-vessel/pulse/native,
mount options=(rw, rbind) /oldroot/dev/snd/ -> /newroot/dev/snd/,
mount options=(rw, rbind) /bindfile* -> /newroot/etc/asound.conf,
mount options=(rw, rbind) /oldroot/run/user/[0-9]*/bus -> /newroot/run/pressure-vessel/bus,

mount options=(rw, rbind) /oldroot/run/dbus/system_bus_socket -> /newroot/run/dbus/system_bus_socket,
mount options=(rw, rbind) /oldroot/run/systemd/resolve/io.systemd.Resolve -> /newroot/run/systemd/resolve/io.systemd.Resolve,
mount options=(rw, rbind) /bindfile* -> /newroot/run/host/container-manager,

# Allow masking of certain directories in the sandbox
mount fstype=tmpfs options=(rw, nosuid, nodev) tmpfs -> /newroot/home/*/snap/steam/common/.local/share/vulkan/implicit_layer.d/,
mount fstype=tmpfs options=(rw, nosuid, nodev) tmpfs -> /newroot/run/pressure-vessel/ldso/,
mount fstype=tmpfs options=(rw, nosuid, nodev) tmpfs -> /newroot/tmp/.X11-unix/,

# Pivot from the intermediate root to sandbox root
mount options in (rw, silent, rprivate) -> /oldroot/,
umount /oldroot/,
pivot_root oldroot=/newroot/ /newroot/,
Copy link
Contributor

Choose a reason for hiding this comment

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

I learnt something new today! Nice! :-)

umount /,

# Permissions needed within sandbox root
/usr/lib/pressure-vessel/** ixr,
/run/host/** mr,
/run/pressure-vessel/** mrw,
/run/host/usr/sbin/ldconfig* ixr,
/run/host/usr/bin/localedef ixr,
/var/cache/ldconfig/** rw,
Copy link
Collaborator

Choose a reason for hiding this comment

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

does the snap does a layout on this one, as it comes from the base afaict?

Copy link
Contributor

Choose a reason for hiding this comment

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

Could we followup with that outside of this PR? I'd like @jhenstridge to respond, but we won't get that as timely as we'd like.

Copy link
Collaborator

Choose a reason for hiding this comment

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

ah, these are in the pivoted root? but is still mount /var from the snap root, so still not entirely sure how that is writable

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added these rules to handle accesses made by the process within the mount namespace created by pressure-vessel/bubblewrap.

Maybe in future this could be separated out into a sub-profile, but that's complicated by the fact that the executables we'd perform the transitions on are downloaded by Steam and may have varying paths.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm still a bit confused how that dir gets writable because the underlying dir comes from the base that is read-only

Copy link
Collaborator

Choose a reason for hiding this comment

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

we can land this but I would like to understand how that gets writable by Monday. I'm probably missing something but I'm guessing what happens just looking at the rules here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The root of the bubblewrap sandbox is a tmpfs, so any path not mounted over is potentially writeable. It isn't exposing the whole host system /run or /var, so these locations are writeable (at least when AppArmor allows it).


capability sys_admin,
capability sys_ptrace,
capability setpcap,
`

const steamSupportConnectedPlugSecComp = `
# Description: additional permissions needed by Steam

# Allow Steam to set up "pressure-vessel" containers to run games in.
mount
umount2
pivot_root
`

func init() {
registerIface(&commonInterface{
name: "steam-support",
summary: steamSupportSummary,
implicitOnCore: false,
implicitOnClassic: true,
baseDeclarationSlots: steamSupportBaseDeclarationSlots,
baseDeclarationPlugs: steamSupportBaseDeclarationPlugs,
connectedPlugAppArmor: steamSupportConnectedPlugAppArmor,
connectedPlugSecComp: steamSupportConnectedPlugSecComp,
})
}
92 changes: 92 additions & 0 deletions interfaces/builtin/steam_support_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2022 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/snap"
"github.com/snapcore/snapd/testutil"
)

type SteamSupportInterfaceSuite struct {
iface interfaces.Interface
slotInfo *snap.SlotInfo
slot *interfaces.ConnectedSlot
plugInfo *snap.PlugInfo
plug *interfaces.ConnectedPlug
}

const steamSupportCoreYaml = `name: core
version: 0
type: os
slots:
steam-support:
`

const steamSupportConsumerYaml = `name: consumer
version: 0
apps:
app:
plugs: [steam-support]
`

var _ = Suite(&SteamSupportInterfaceSuite{
iface: builtin.MustInterface("steam-support"),
})

func (s *SteamSupportInterfaceSuite) SetUpTest(c *C) {
s.plug, s.plugInfo = MockConnectedPlug(c, steamSupportConsumerYaml, nil, "steam-support")
s.slot, s.slotInfo = MockConnectedSlot(c, steamSupportCoreYaml, nil, "steam-support")
}

func (s *SteamSupportInterfaceSuite) TestName(c *C) {
c.Assert(s.iface.Name(), Equals, "steam-support")
}

func (s *SteamSupportInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
}

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

func (s *SteamSupportInterfaceSuite) TestAppArmorSpec(c *C) {

spec := &apparmor.Specification{}
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "mount options=(rw, rbind) /tmp/newroot/ -> /tmp/newroot/,\n")
}

func (s *SteamSupportInterfaceSuite) TestSecCompSpec(c *C) {
spec := &seccomp.Specification{}
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "# Allow Steam to set up \"pressure-vessel\" containers to run games in.\nmount\numount2\npivot_root\n")
}

func (s *SteamSupportInterfaceSuite) TestInterfaces(c *C) {
c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface)
}
21 changes: 21 additions & 0 deletions interfaces/policy/basedeclaration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,24 @@ plugs:
c.Check(err, IsNil)
}

func (s *baseDeclSuite) TestAutoConnectionSteamSupportOverride(c *C) {
cand := s.connectCand(c, "steam-support", "", "")
_, err := cand.CheckAutoConnect()
c.Check(err, NotNil)
c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"steam-support\"")

plugsSlots := `
plugs:
steam-support:
allow-auto-connection: true
`

snapDecl := s.mockSnapDecl(c, "some-snap", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
cand.PlugSnapDeclaration = snapDecl
_, err = cand.CheckAutoConnect()
c.Check(err, IsNil)
}

func (s *baseDeclSuite) TestAutoConnectionOverrideMultiple(c *C) {
plugsSlots := `
plugs:
Expand Down Expand Up @@ -781,6 +799,7 @@ var (
"sd-control": {"core"},
"serial-port": {"core", "gadget"},
"spi": {"core", "gadget"},
"steam-support": {"core"},
"storage-framework-service": {"app"},
"thumbnailer-service": {"app"},
"ubuntu-download-manager": {"app"},
Expand Down Expand Up @@ -904,6 +923,7 @@ func (s *baseDeclSuite) TestPlugInstallation(c *C) {
"snap-refresh-control": true,
"snap-themes-control": true,
"snapd-control": true,
"steam-support": true,
"system-files": true,
"tee": true,
"uinput": true,
Expand Down Expand Up @@ -1153,6 +1173,7 @@ func (s *baseDeclSuite) TestValidity(c *C) {
"snap-refresh-control": true,
"snap-themes-control": true,
"snapd-control": true,
"steam-support": true,
"system-files": true,
"tee": true,
"udisks2": true,
Expand Down
1 change: 1 addition & 0 deletions interfaces/seccomp/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ fork
ftime
futex
futex_time64
futex_waitv
get_mempolicy
get_robust_list
get_thread_area
Expand Down
3 changes: 3 additions & 0 deletions tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,9 @@ apps:
ssh-public-keys:
command: bin/run
plugs: [ ssh-public-keys ]
steam-support:
command: bin/run
plugs: [ steam-support ]
storage-framework-service:
command: bin/run
plugs: [ storage-framework-service ]
Expand Down