Blowing Off Steam: Let's plan steam-support interface

@ikey Apologies for the delay. The last two weeks included some of us on holidays and then a sprint last week, so I’m personally behind on the forum and in PRs. But I’m back now and should get over these soon.

As for the interface, I think the easiest in this specific case is indeed to push the PR. In general those are almost entirely composed of poking holes, and then @jdstrand can provide details on the relevant entries right in context, which is easier.

As for auto-connections, we have a very rich language for those that can do anything desired and for very specific cases. We just need to know what to do and the rationale behind it.

3 Likes

OK I’ll poke lxd and figure out the hows (there is very little documentation on this subject) and then get a PR together.

It might have to go in tandem with another PR, because Solus just switched to glvnd. So that basically means we broke our own snap from our perspective. :slight_smile: It does mean we can nuke the old glx-provider statements though in the existing AppArmor bits

1 Like

I get that :slight_smile: I’m just trying to accelerate this as its tied into the direct goals of Solus itself, and unfortunately looks like I can’t hold back our release anymore to wait on these last bits in snapd.

PR basically copies the LXD scaffolding and I guess we go from there

I reviewed this forum topic and the PR then spoke with @ikey on IRC at length. I will be investigating a few things and report back here.

Per our IRC conversation, the simplified overall architecture for steam is that there is a steam-client that is used as the entry point to everything Steam. From there you can purchase games and launch games. Part of its lifecycle is that the client must ptrace the games it launches.

The PR as implemented creates a steam-support interface that allows the steam client to operate and launch games under the same security profile. In our IRC discussion I felt that it would be nice if the steam client and the games were under different security profiles such that the client could ptrace the games, but not the other way around. This would provide a nice security barrier for bad actors to ptrace the client and grab payment information. Yama actually prevents this when kernel.yama.ptrace_scope=1 and while yama defaults to ‘1’ in Ubuntu, yama may not be available everywhere, not to mention some people might have set it to ‘0’, so having the apparmor mediation is worthwhile.

I’ve proven to myself that the separation is possible by using an apparmor child profile. The concept is simple: the client runs like normal in its profile, but when it executes the game binaries, we have a Cx/cx rule that does a profile transition where the game runs in the child profile (the client does not need to be modified for this to work). This approach is completely workable within snapd, but it is a bit awkward in terms of maintenance since all steam game rules are tucked inside the steam-support child profile.

I then thought about yaml that easier to use for the steam-client snap and came up with:

name: steam-client
apps:
  steam-client:
    plugs:
    - steam-support
    - network
    - removable-media
  game:
    plugs:
    - steam-game
    - network
    - removable-media
    - joystick
    - ...

The new ‘steam-support’ interface has only what it needs to operate as the steam-client, ie, install, uninstall and launch games and has the ability to perform lifecycle duties on games. Instead of Cx/cx rules, we use a Px/px rule to perform an exec transition to snap.steam-client.game. The ‘steam-game’ interface has whatever is needed to run as a steam game that isn’t exposed via existing interfaces. In this manner, we can declare in the normal way what the client needs and what the games need, adding additional plugs to either as needed. This is also interesting because it leaves the possibility down the road to introduce different ‘game commands’ in the yaml that use different plugs to represent classes of games (eg, ‘mono-game’, ‘cef-game’, etc. The launcher would need to know which profile to launch them under of course).

What is slightly awkward about this approach is that we have the steam-client.game command that shows up in /snap/bin and isn’t really used for anything other than apparmor profile generation. We can turn this into a positive though and have steam-client.game be a shell script that simply says “You can purchase and launch games via ‘steam-client’”.

@ikey, let’s get some feedback from others before jumping on this in the PR. Once we decide on the path forward, I can advise on the next steps in the implementation.

Just to clarify, the Steam Client itself doesn’t need to do ptracing, only certain game launchers do, so they’d be satisfied by the child profile requirements. Also there is no real way for us to know ahead of time if a game is going to be using cef or mono, or any other technology, so defining those specific child profiles isn’t something that would make sense unless the Steam Client specifically executed them under a given apparmor profile.

For now the best path appears to be what we discussed on IRC, with a root level profile applying to anything not a game, and then a game profile allowing ptrace, etc, matching on the child paths.

OK so I’ve played with this some this morning, and two glaring issues come to mind so far.

Cx drops the environment of required variables like LD_PRELOAD, which LSI needs, so we’d want cx… Additionally any time we define a glob for changing the profile it conflicts with half of the built-in profiles and apparmor fails to parse:

profile has merged rule with conflicting x modifiers
ERROR processing regexs for profile snap.linux-steam-integration.exec, failed to load

Basically all the glob ones get merged and we’re unable to overwrite it unless we use exact paths (which we can’t because we don’t know them). Note this isn’t just on removable-media but even on @{HOME} paths, meaning I’m just not going to be able to change the child profile on the fly using globs. So it looks like we’re back to square 1 with a “super profile”.

We’re actually not back to square one; the conflicting x modifiers is part of the reason why I suggested not jumping on this PR just yet. :wink: We’ll need to carve out the paths in at least SNAP_USER_COMMON. removable-media should be ok though since we don’t have ix rules in there. Does the steam client actually need home? My understanding was it actually only needed SNAP_USER_COMMON, but if it does, we need to do the same for home.

I’m not sure what you mean by “carve out the paths” - can you elaborate please?

And we’ll need to allow home for now for people having setups like ~/SteamLibrary that are migrating to the snap

Or we allow ~/SteamLibrary in steam-support and steam-game (eg, is all of home actually needed?).

And by carve out, I mean adjust the default policy/affected interfaces that cause the conflicting x modifiers to specify ix for those paths so that steam-support can specify Px for them.

This is what I was going to provide to you and was trying to shield you from. :wink:

Well ~/SteamLibrary was a totally arbitrary path, basically we care about:

@{HOME}/**/steamapps/common/*/** ixm
{,run/}media/**/steamapps/common/*/** ixm

Exact bits might be different but you get the idea. :]

I’ll provide the carving out to you (still have some investigating to do; also why I wanted to wait for comment), but I’ll need the exact bits. If I can count on something like ‘/steamapps/common/’ then it should be ok, but if it can be literally anything then no. If it can be literally anything, we can also make it so it works for where the snap expects it to be, and allow steam-support to migrate to the expected locations (this is actually closer to what we do for other snaps).

The games being launched will always be under /steamapps/common/ paths. Steam Client itself will follow normal rules for living inside SNAP_USER_COMMON

1 Like

Hrmm, I forgot about https://launchpad.net/bugs/1696552 which prevents us from using the px rule approach (the cx rule approach is still valid). I really like the separation of the plugs though so I’m going to think through this and see if I can make this work with cx rules.

2 Likes

Ok, I think this is workable if we have yaml like this:

name: steam
plugs:
  steam-game:
    steam-client: client 
apps:
  client:
    plugs:
    - steam-support
    - network
    - removable-media
  child:
    plugs:
    - steam-game
    - network
    - removable-media
    - joystick
    - ...

The apparmor profile for ‘client’ works like always. The apparmor profile for ‘game’ is slightly different in that the ###PROFILEATTACH### looks like this:

profile snap.steam.client//game { ... }

where client comes from the steam-game interface attribute steam-client and game is hard-coded in the interface. Put another way, we need to tell snapd that the child command is actually a child profile for client, so we use an interface attribute for that.

@ikey, please don’t implement this yet. When we got feedback on the design, I will create a branch off of your PR and do the heavy lifting so you can run with it.

This is all super crazy stuff! I will certainly hold back until the smoke has cleared, thank you! :slight_smile:

1 Like

Getting back to this now.

<TLDR;>

Due to limitations with the AppArmor parser and the design of snapd, we (IMO) have essentially two options:

  1. The Px/px idea is the cleanest implementation, but requires patching apparmor parser to fix bug #16965522. The problem with this approach is propagating that fix everywhere. It is possible for Solus to pick it up and for Ubuntu to SRU, but far less likely Ubuntu derivatives, Debian, SUSE, etc will pick it up. snapd could perform a runtime detection and do something smart-- eg distros with full AppArmor support only expose the interface if the fix is in place (partial AppArmor support doesn’t end up with the rule at all so no problem)
  2. We utilize a child profile with a Cx/cx rule and modify snapd to accommodate this. The least intrusive option is to put the child profile (ie what plugs: [steam-game]) in a separate file '#include’d by the parent (ie, what plugs: [steam-support]) and adjust snapd to load/unload the parent profile with interface connect/disconnect for the child. We also adjust ‘command’ in snap.yaml to either require ‘null’ assignment or to be omitted when used with ‘steam-game’

I prefer ‘1’ since it is the cleanest implementation, but need an architect to decide if it is ok from a cross-distro perspective. ‘2’ is not as clean, but I think it can be done in a way that isn’t terrible from an implementation perspective (with a little design for the finer points (which can be done in PR review)).

Full context

jjohansen and I discussed options with apparmor wrt Px and Cx rules.

Because of the apparmor bug with Px/px, we can’t use the Px/px idea today. We could fix the parser for the bug, but that won’t propagate to everywhere snapd is supported (we could SRU for Ubuntu, but Solus, SUSE, Arch, Debian, Ubuntu derivatives, etc may not have the fix), but then the snap would operate differently on different distros. To remedy this, we could surface the interfaces on if the parser had the fix (it could be a runtime check).

The alternative is to use a child profile in some fashion. In practice, this results in variations on two choices:

  1. embed the child profile in the parent (eg profile snap.snap-client.snap-client { game {} })
  2. put the child profile in a separate file

‘1’ might work with snap.yaml like the following:

name: steam-client
apps:
  steam-client:
    command: $SNAP/bin/client
    plugs:
    - steam-support
    - network
    - removable-media
    - home
    - desktop
    - browser-support
    - ...

With this, we have steam-support put one set of rules in the parent profile and another set in the child profile and we adjust snapd so that all the other interfaces are added to both. This is a somewhat complicated proposition within snapd in terms of adjusting interface connections, but not impossible. It also suffers from the fact that the client and the game have all the same interfaces (differing only in what stream-support conditionally puts in each).

‘2’ might work with snap.yaml like the following:

name: steam-client
apps:
  steam-client:
    command: $SNAP/bin/client
    plugs:
    - steam-support
    - network
    - desktop
    - home
    - removable-media
  game:
    command: ???
    plugs:
    - steam-game
    - network
    - desktop
    - home
    - ...

With this, the client and the game can have different plugs. Because the child profile for ‘game’ is in another profile, we either need to:

  • be cognizant of load order (parent then child) and unload order (child then parent) and lexically name the profile such that the cache files load in the right order. This would require fairly involced changes to snapd profile load/unload
  • have the parent ‘#include’ the child and have snapd ignore loading/unloading the child. This requires simpler changes to snapd

With ‘2’ , in order to avoid more complicated changes to snap run, snap-confine, etc for the profile name being snap.steam-client.steam-client//game, we also have to decide how to deal with the fact that ‘steam-game’ is not actually a command and that it is a child profile of ‘steam-client’. One way to handle this is by ignoring command when used with steam-game. Eg:

name: steam-client
plugs:
  steam-support:
    child-command: game
apps:
  steam-client:
    command: $SNAP/bin/client
    plugs:
    - steam-support
    - ...
  game:
    command: null
    plugs:
    - steam-game
    ...

@niemeyer - if you could decide on the general approach, I can run with it and discuss implementation details in a PR.