Skip to content

Commit eaad8a2

Browse files
authored
interfaces: add a steam-support interface (#11708)
This interface is intended to provide some additional permissions needed by the steam snap. At present, this is primarily AppArmor and seccomp rules to allow Steam to launch pressure-vessel containers, which it uses to provide a consistent runtime environment to some games (at the moment mainly Windows games it runs under Proton/Wine). PV is based on Bubblewrap, as used by Flatpak and various other process sandboxes on GNOME systems. Related to getting Steam games to run, I've added the futex_waitv syscall to the base template. Although the Ubuntu kernels don't yet support this syscall, we want to let Proton try to call it so it will fall back to the old futex API. As this has essentially the same security concerns as the existing futex syscalls, it seemed sensible to add it to the base template rather than the steam-support interface. snap-seccomp knows about this syscall as of 15th April, when PR #11674 was merged. * interfaces: add a steam-support interface with permissions needed to set up pressure-vessel containers * interfaces/seccomp: add futex_waitv to the base template This is a new syscall used to wait on multiple futexes at once, and Wine/Proton will attempt to use it if the kernel supports it. Blocking access prevents it from falling back to the other futex related syscalls. * tests: add steam-support to policy snap * interfaces: limit proc access to same owner in steam interface * interfaces: lock down the remount AppArmor rules for steam-support * interfaces: allow pressure-vessel to mount tmpfs to mask certain directories * interfaces/policy: add base declaration tests for steam-support
1 parent 71053c6 commit eaad8a2

File tree

5 files changed

+331
-0
lines changed

5 files changed

+331
-0
lines changed

interfaces/builtin/steam_support.go

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// -*- Mode: Go; indent-tabs-mode: t -*-
2+
3+
/*
4+
* Copyright (C) 2022 Canonical Ltd
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License version 3 as
8+
* published by the Free Software Foundation.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*
18+
*/
19+
20+
package builtin
21+
22+
const steamSupportSummary = `allow Steam to configure pressure-vessel containers`
23+
24+
const steamSupportBaseDeclarationPlugs = `
25+
steam-support:
26+
allow-installation: false
27+
deny-auto-connection: true
28+
`
29+
30+
const steamSupportBaseDeclarationSlots = `
31+
steam-support:
32+
allow-installation:
33+
slot-snap-type:
34+
- core
35+
deny-auto-connection: true
36+
`
37+
38+
const steamSupportConnectedPlugAppArmor = `
39+
# Allow pressure-vessel to set up its Bubblewrap sandbox.
40+
/sys/kernel/ r,
41+
@{PROC}/sys/kernel/overflowuid r,
42+
@{PROC}/sys/kernel/overflowgid r,
43+
@{PROC}/sys/kernel/sched_autogroup_enabled r,
44+
@{PROC}/pressure/io r,
45+
owner @{PROC}/@{pid}/uid_map rw,
46+
owner @{PROC}/@{pid}/gid_map rw,
47+
owner @{PROC}/@{pid}/setgroups rw,
48+
owner @{PROC}/@{pid}/mounts r,
49+
owner @{PROC}/@{pid}/mountinfo r,
50+
51+
# Create and pivot to the intermediate root
52+
mount options=(rw, rslave) -> /,
53+
mount options=(rw, silent, rslave) -> /,
54+
mount fstype=tmpfs options=(rw, nosuid, nodev) tmpfs -> /tmp/,
55+
mount options=(rw, rbind) /tmp/newroot/ -> /tmp/newroot/,
56+
pivot_root oldroot=/tmp/oldroot/ /tmp/,
57+
58+
# Set up sandbox in /newroot
59+
mount options=(rw, rbind) /oldroot/ -> /newroot/,
60+
mount options=(rw, rbind) /oldroot/dev/ -> /newroot/dev/,
61+
mount options=(rw, rbind) /oldroot/etc/ -> /newroot/etc/,
62+
mount options=(rw, rbind) /oldroot/proc/ -> /newroot/proc/,
63+
mount options=(rw, rbind) /oldroot/sys/ -> /newroot/sys/,
64+
mount options=(rw, rbind) /oldroot/tmp/ -> /newroot/tmp/,
65+
mount options=(rw, rbind) /oldroot/var/ -> /newroot/var/,
66+
mount options=(rw, rbind) /oldroot/var/tmp/ -> /newroot/var/tmp/,
67+
mount options=(rw, rbind) /oldroot/usr/ -> /newroot/run/host/usr/,
68+
mount options=(rw, rbind) /oldroot/etc/ -> /newroot/run/host/etc/,
69+
mount options=(rw, rbind) /oldroot/usr/lib/os-release -> /newroot/run/host/os-release,
70+
71+
# Bubblewrap performs remounts on directories it binds under /newroot
72+
# to fix up the options (since options other than MS_REC are ignored
73+
# when performing a bind mount). Ideally we could do something like:
74+
# remount options=(bind, silent, nosuid, *) /newroot/{,**},
75+
#
76+
# But that is not supported by AppArmor. So we enumerate the possible
77+
# combinations of options Bubblewrap might use.
78+
remount options=(bind, silent, nosuid, rw) /newroot/{,**},
79+
remount options=(bind, silent, nosuid, rw, nodev) /newroot/{,**},
80+
remount options=(bind, silent, nosuid, rw, noexec) /newroot/{,**},
81+
remount options=(bind, silent, nosuid, rw, nodev, noexec) /newroot/{,**},
82+
remount options=(bind, silent, nosuid, rw, noatime) /newroot/{,**},
83+
remount options=(bind, silent, nosuid, rw, nodev, noatime) /newroot/{,**},
84+
remount options=(bind, silent, nosuid, rw, noexec, noatime) /newroot/{,**},
85+
remount options=(bind, silent, nosuid, rw, nodev, noexec, noatime) /newroot/{,**},
86+
remount options=(bind, silent, nosuid, rw, relatime) /newroot/{,**},
87+
remount options=(bind, silent, nosuid, rw, nodev, relatime) /newroot/{,**},
88+
remount options=(bind, silent, nosuid, rw, noexec, relatime) /newroot/{,**},
89+
remount options=(bind, silent, nosuid, rw, nodev, noexec, relatime) /newroot/{,**},
90+
remount options=(bind, silent, nosuid, rw, nodiratime) /newroot/{,**},
91+
remount options=(bind, silent, nosuid, rw, nodev, nodiratime) /newroot/{,**},
92+
remount options=(bind, silent, nosuid, rw, noexec, nodiratime) /newroot/{,**},
93+
remount options=(bind, silent, nosuid, rw, nodev, noexec, nodiratime) /newroot/{,**},
94+
remount options=(bind, silent, nosuid, rw, noatime, nodiratime) /newroot/{,**},
95+
remount options=(bind, silent, nosuid, rw, nodev, noatime, nodiratime) /newroot/{,**},
96+
remount options=(bind, silent, nosuid, rw, noexec, noatime, nodiratime) /newroot/{,**},
97+
remount options=(bind, silent, nosuid, rw, nodev, noexec, noatime, nodiratime) /newroot/{,**},
98+
remount options=(bind, silent, nosuid, rw, relatime, nodiratime) /newroot/{,**},
99+
remount options=(bind, silent, nosuid, rw, nodev, relatime, nodiratime) /newroot/{,**},
100+
remount options=(bind, silent, nosuid, rw, noexec, relatime, nodiratime) /newroot/{,**},
101+
remount options=(bind, silent, nosuid, rw, nodev, noexec, relatime, nodiratime) /newroot/{,**},
102+
remount options=(bind, silent, nosuid, ro) /newroot/{,**},
103+
remount options=(bind, silent, nosuid, ro, nodev) /newroot/{,**},
104+
remount options=(bind, silent, nosuid, ro, noexec) /newroot/{,**},
105+
remount options=(bind, silent, nosuid, ro, nodev, noexec) /newroot/{,**},
106+
remount options=(bind, silent, nosuid, ro, noatime) /newroot/{,**},
107+
remount options=(bind, silent, nosuid, ro, nodev, noatime) /newroot/{,**},
108+
remount options=(bind, silent, nosuid, ro, noexec, noatime) /newroot/{,**},
109+
remount options=(bind, silent, nosuid, ro, nodev, noexec, noatime) /newroot/{,**},
110+
remount options=(bind, silent, nosuid, ro, relatime) /newroot/{,**},
111+
remount options=(bind, silent, nosuid, ro, nodev, relatime) /newroot/{,**},
112+
remount options=(bind, silent, nosuid, ro, noexec, relatime) /newroot/{,**},
113+
remount options=(bind, silent, nosuid, ro, nodev, noexec, relatime) /newroot/{,**},
114+
remount options=(bind, silent, nosuid, ro, nodiratime) /newroot/{,**},
115+
remount options=(bind, silent, nosuid, ro, nodev, nodiratime) /newroot/{,**},
116+
remount options=(bind, silent, nosuid, ro, noexec, nodiratime) /newroot/{,**},
117+
remount options=(bind, silent, nosuid, ro, nodev, noexec, nodiratime) /newroot/{,**},
118+
remount options=(bind, silent, nosuid, ro, noatime, nodiratime) /newroot/{,**},
119+
remount options=(bind, silent, nosuid, ro, nodev, noatime, nodiratime) /newroot/{,**},
120+
remount options=(bind, silent, nosuid, ro, noexec, noatime, nodiratime) /newroot/{,**},
121+
remount options=(bind, silent, nosuid, ro, nodev, noexec, noatime, nodiratime) /newroot/{,**},
122+
remount options=(bind, silent, nosuid, ro, relatime, nodiratime) /newroot/{,**},
123+
remount options=(bind, silent, nosuid, ro, nodev, relatime, nodiratime) /newroot/{,**},
124+
remount options=(bind, silent, nosuid, ro, noexec, relatime, nodiratime) /newroot/{,**},
125+
remount options=(bind, silent, nosuid, ro, nodev, noexec, relatime, nodiratime) /newroot/{,**},
126+
127+
/newroot/** rwkl,
128+
/bindfile* rw,
129+
mount options=(rw, rbind) /oldroot/home/** -> /newroot/home/**,
130+
mount options=(rw, rbind) /oldroot/snap/** -> /newroot/snap/**,
131+
mount options=(rw, rbind) /oldroot/home/**/usr/ -> /newroot/usr/,
132+
mount options=(rw, rbind) /oldroot/home/**/usr/etc/** -> /newroot/etc/**,
133+
mount options=(rw, rbind) /oldroot/home/**/usr/etc/ld.so.cache -> /newroot/run/pressure-vessel/ldso/runtime-ld.so.cache,
134+
mount options=(rw, rbind) /oldroot/home/**/usr/etc/ld.so.conf -> /newroot/run/pressure-vessel/ldso/runtime-ld.so.conf,
135+
136+
mount options=(rw, rbind) /oldroot/etc/machine-id -> /newroot/etc/machine-id,
137+
mount options=(rw, rbind) /oldroot/etc/group -> /newroot/etc/group,
138+
mount options=(rw, rbind) /oldroot/etc/passwd -> /newroot/etc/passwd,
139+
mount options=(rw, rbind) /oldroot/etc/host.conf -> /newroot/etc/host.conf,
140+
mount options=(rw, rbind) /oldroot/etc/hosts -> /newroot/etc/hosts,
141+
mount options=(rw, rbind) /oldroot/**/*resolv.conf -> /newroot/etc/resolv.conf,
142+
mount options=(rw, rbind) /bindfile* -> /newroot/etc/timezone,
143+
144+
mount options=(rw, rbind) /oldroot/run/systemd/journal/socket -> /newroot/run/systemd/journal/socket,
145+
mount options=(rw, rbind) /oldroot/run/systemd/journal/stdout -> /newroot/run/systemd/journal/stdout,
146+
147+
mount options=(rw, rbind) /oldroot/usr/share/fonts/ -> /newroot/run/host/fonts/,
148+
mount options=(rw, rbind) /oldroot/usr/local/share/fonts/ -> /newroot/run/host/local-fonts/,
149+
mount options=(rw, rbind) /oldroot/{var/cache/fontconfig,usr/lib/fontconfig/cache}/ -> /newroot/run/host/fonts-cache/,
150+
mount options=(rw, rbind) /oldroot/home/**/.cache/fontconfig/ -> /newroot/run/host/user-fonts-cache/,
151+
mount options=(rw, rbind) /bindfile* -> /newroot/run/host/font-dirs.xml,
152+
153+
mount options=(rw, rbind) /oldroot/usr/share/icons/ -> /newroot/run/host/share/icons/,
154+
mount options=(rw, rbind) /oldroot/home/**/.local/share/icons/ -> /newroot/run/host/user-share/icons/,
155+
156+
mount options=(rw, rbind) /oldroot/run/user/[0-9]*/wayland-* -> /newroot/run/pressure-vessel/wayland-*,
157+
mount options=(rw, rbind) /oldroot/tmp/.X11-unix/X* -> /newroot/tmp/.X11-unix/X99,
158+
mount options=(rw, rbind) /bindfile* -> /newroot/run/pressure-vessel/Xauthority,
159+
160+
mount options=(rw, rbind) /bindfile* -> /newroot/run/pressure-vessel/pulse/config,
161+
mount options=(rw, rbind) /oldroot/run/user/[0-9]*/pulse/native -> /newroot/run/pressure-vessel/pulse/native,
162+
mount options=(rw, rbind) /oldroot/dev/snd/ -> /newroot/dev/snd/,
163+
mount options=(rw, rbind) /bindfile* -> /newroot/etc/asound.conf,
164+
mount options=(rw, rbind) /oldroot/run/user/[0-9]*/bus -> /newroot/run/pressure-vessel/bus,
165+
166+
mount options=(rw, rbind) /oldroot/run/dbus/system_bus_socket -> /newroot/run/dbus/system_bus_socket,
167+
mount options=(rw, rbind) /oldroot/run/systemd/resolve/io.systemd.Resolve -> /newroot/run/systemd/resolve/io.systemd.Resolve,
168+
mount options=(rw, rbind) /bindfile* -> /newroot/run/host/container-manager,
169+
170+
# Allow masking of certain directories in the sandbox
171+
mount fstype=tmpfs options=(rw, nosuid, nodev) tmpfs -> /newroot/home/*/snap/steam/common/.local/share/vulkan/implicit_layer.d/,
172+
mount fstype=tmpfs options=(rw, nosuid, nodev) tmpfs -> /newroot/run/pressure-vessel/ldso/,
173+
mount fstype=tmpfs options=(rw, nosuid, nodev) tmpfs -> /newroot/tmp/.X11-unix/,
174+
175+
# Pivot from the intermediate root to sandbox root
176+
mount options in (rw, silent, rprivate) -> /oldroot/,
177+
umount /oldroot/,
178+
pivot_root oldroot=/newroot/ /newroot/,
179+
umount /,
180+
181+
# Permissions needed within sandbox root
182+
/usr/lib/pressure-vessel/** ixr,
183+
/run/host/** mr,
184+
/run/pressure-vessel/** mrw,
185+
/run/host/usr/sbin/ldconfig* ixr,
186+
/run/host/usr/bin/localedef ixr,
187+
/var/cache/ldconfig/** rw,
188+
189+
capability sys_admin,
190+
capability sys_ptrace,
191+
capability setpcap,
192+
`
193+
194+
const steamSupportConnectedPlugSecComp = `
195+
# Description: additional permissions needed by Steam
196+
197+
# Allow Steam to set up "pressure-vessel" containers to run games in.
198+
mount
199+
umount2
200+
pivot_root
201+
`
202+
203+
func init() {
204+
registerIface(&commonInterface{
205+
name: "steam-support",
206+
summary: steamSupportSummary,
207+
implicitOnCore: false,
208+
implicitOnClassic: true,
209+
baseDeclarationSlots: steamSupportBaseDeclarationSlots,
210+
baseDeclarationPlugs: steamSupportBaseDeclarationPlugs,
211+
connectedPlugAppArmor: steamSupportConnectedPlugAppArmor,
212+
connectedPlugSecComp: steamSupportConnectedPlugSecComp,
213+
})
214+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// -*- Mode: Go; indent-tabs-mode: t -*-
2+
3+
/*
4+
* Copyright (C) 2022 Canonical Ltd
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License version 3 as
8+
* published by the Free Software Foundation.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*
18+
*/
19+
20+
package builtin_test
21+
22+
import (
23+
. "gopkg.in/check.v1"
24+
25+
"github.com/snapcore/snapd/interfaces"
26+
"github.com/snapcore/snapd/interfaces/apparmor"
27+
"github.com/snapcore/snapd/interfaces/builtin"
28+
"github.com/snapcore/snapd/interfaces/seccomp"
29+
"github.com/snapcore/snapd/snap"
30+
"github.com/snapcore/snapd/testutil"
31+
)
32+
33+
type SteamSupportInterfaceSuite struct {
34+
iface interfaces.Interface
35+
slotInfo *snap.SlotInfo
36+
slot *interfaces.ConnectedSlot
37+
plugInfo *snap.PlugInfo
38+
plug *interfaces.ConnectedPlug
39+
}
40+
41+
const steamSupportCoreYaml = `name: core
42+
version: 0
43+
type: os
44+
slots:
45+
steam-support:
46+
`
47+
48+
const steamSupportConsumerYaml = `name: consumer
49+
version: 0
50+
apps:
51+
app:
52+
plugs: [steam-support]
53+
`
54+
55+
var _ = Suite(&SteamSupportInterfaceSuite{
56+
iface: builtin.MustInterface("steam-support"),
57+
})
58+
59+
func (s *SteamSupportInterfaceSuite) SetUpTest(c *C) {
60+
s.plug, s.plugInfo = MockConnectedPlug(c, steamSupportConsumerYaml, nil, "steam-support")
61+
s.slot, s.slotInfo = MockConnectedSlot(c, steamSupportCoreYaml, nil, "steam-support")
62+
}
63+
64+
func (s *SteamSupportInterfaceSuite) TestName(c *C) {
65+
c.Assert(s.iface.Name(), Equals, "steam-support")
66+
}
67+
68+
func (s *SteamSupportInterfaceSuite) TestSanitizeSlot(c *C) {
69+
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
70+
}
71+
72+
func (s *SteamSupportInterfaceSuite) TestSanitizePlug(c *C) {
73+
c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil)
74+
}
75+
76+
func (s *SteamSupportInterfaceSuite) TestAppArmorSpec(c *C) {
77+
78+
spec := &apparmor.Specification{}
79+
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
80+
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
81+
c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "mount options=(rw, rbind) /tmp/newroot/ -> /tmp/newroot/,\n")
82+
}
83+
84+
func (s *SteamSupportInterfaceSuite) TestSecCompSpec(c *C) {
85+
spec := &seccomp.Specification{}
86+
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
87+
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")
88+
}
89+
90+
func (s *SteamSupportInterfaceSuite) TestInterfaces(c *C) {
91+
c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface)
92+
}

interfaces/policy/basedeclaration_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,24 @@ plugs:
678678
c.Check(err, IsNil)
679679
}
680680

681+
func (s *baseDeclSuite) TestAutoConnectionSteamSupportOverride(c *C) {
682+
cand := s.connectCand(c, "steam-support", "", "")
683+
_, err := cand.CheckAutoConnect()
684+
c.Check(err, NotNil)
685+
c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"steam-support\"")
686+
687+
plugsSlots := `
688+
plugs:
689+
steam-support:
690+
allow-auto-connection: true
691+
`
692+
693+
snapDecl := s.mockSnapDecl(c, "some-snap", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
694+
cand.PlugSnapDeclaration = snapDecl
695+
_, err = cand.CheckAutoConnect()
696+
c.Check(err, IsNil)
697+
}
698+
681699
func (s *baseDeclSuite) TestAutoConnectionOverrideMultiple(c *C) {
682700
plugsSlots := `
683701
plugs:
@@ -781,6 +799,7 @@ var (
781799
"sd-control": {"core"},
782800
"serial-port": {"core", "gadget"},
783801
"spi": {"core", "gadget"},
802+
"steam-support": {"core"},
784803
"storage-framework-service": {"app"},
785804
"thumbnailer-service": {"app"},
786805
"ubuntu-download-manager": {"app"},
@@ -904,6 +923,7 @@ func (s *baseDeclSuite) TestPlugInstallation(c *C) {
904923
"snap-refresh-control": true,
905924
"snap-themes-control": true,
906925
"snapd-control": true,
926+
"steam-support": true,
907927
"system-files": true,
908928
"tee": true,
909929
"uinput": true,
@@ -1153,6 +1173,7 @@ func (s *baseDeclSuite) TestValidity(c *C) {
11531173
"snap-refresh-control": true,
11541174
"snap-themes-control": true,
11551175
"snapd-control": true,
1176+
"steam-support": true,
11561177
"system-files": true,
11571178
"tee": true,
11581179
"udisks2": true,

interfaces/seccomp/template.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ fork
147147
ftime
148148
futex
149149
futex_time64
150+
futex_waitv
150151
get_mempolicy
151152
get_robust_list
152153
get_thread_area

tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,9 @@ apps:
425425
ssh-public-keys:
426426
command: bin/run
427427
plugs: [ ssh-public-keys ]
428+
steam-support:
429+
command: bin/run
430+
plugs: [ steam-support ]
428431
storage-framework-service:
429432
command: bin/run
430433
plugs: [ storage-framework-service ]

0 commit comments

Comments
 (0)