canonical / steam-snap

Steam as a snap
74 stars 10 forks source link

Discovery for Pressure-Vessel-like Subcontainers Re: Improving Steam/Snap compatibility #365

Open ZoopOTheGoop opened 8 months ago

ZoopOTheGoop commented 8 months ago

Is there an existing issue for this?

Is the feature related to a problem or existing issue?

Yes, most of the issues are related to this, it is primarily discussed here, but has come up at other points: https://github.com/snapcore/snapd/pull/12794#issuecomment-1539947236

Describe the solution/feature you'd like.

I would like the snapd environment and Snapcraft specification to be resilient to changes to Steam and Pressure Vessel changes on Valve, Collabora et al's end, to minimize the maintenance burden for both parties as much as is reasonably possible.

To begin with, I would like to explore the Portal/Subcontainerization idea, and what the requirements on our side would be from Steam for Linux's end if we were able and primed to do such a thing, and a rough idea of what compromises are acceptable if an "ideal" version can't be implemented (whether due to security concerns, scope issues, or whatever else). No need to go over every single change you would need, I don't want an entire detailed spec to have to be written for us, but I would like relatively clear details, particularly since I'm at best intermediate-level in containerization.

I understand a Dbus API (or equivalent) similar to Flatpak's is desired. I haven't had a chance to read and understand it in depth, though it's on my to-do list, and lack much familiarity with Flatpak, but I understand that the corresponding implementation exists in This PR by @smcv and the corresponding issue. It would help if I could get a broad description of the scope of what snapd would have to do on Steam's behalf to suitably emulate/replace/act as a Pressure Vessel (if that's the goal as I understand it). If this is a viable solution, I would be willing to help contribute Steam for Linux Runtime code to enable it as well, though I'd likely need help setting the environment up. (Only the open source bits we already have access to, obviously, I'm not asking for access to anything else).

One potential challenge I want to explicitly ask about: from my tinkering with the Runtime Tools, it's my understanding that Pressure-Vessel quite literally is in large part implemented on (a modification of) bwrap, which is also a significant part of the underlying code to Flatpak. I'm curious how this potentially affects the difficulty of implementation in snapd. How much were the changes to Flatpak simplified by them being related like this?

If (and I'm not sure if this is the case, but an educated guess) pvwrap is fundamentally basically just asking Flatpak to do what it was going to do anyway, but via Flatpak bwrap and associated overhead instead of its own/the system bwrap, then can we reasonably expect snapd to be able to provide a similar enough environment even if we undergo the effort (practical and diplomatic) to expose a similar API to Steam? What's the chance that, due to the differences in implementation, we'd just end up in exactly the same place as we are now, but with an extra Portal system on top of it to maintain?

Describe any alternatives you've considered.

https://github.com/canonical/steam-snap/issues/363#issue-2111240893

Additional context

No response

smcv commented 8 months ago

What do you mean by "discovery" here? Is this another word for requirements-capture, or do you mean that your feature request is for snapd to discover ... something? (If the latter, sorry, I don't understand what.)

I would like to explore the Portal/Subcontainerization idea, and what the requirements on our side would be from Steam for Linux's end if we were able and primed to do such a thing, and a rough idea of what compromises are acceptable if an "ideal" version can't be implemented (whether due to security concerns, scope issues, or whatever else).

The high-level goal of the Steam Linux Runtime container runtimes (pressure-vessel) is that we want to construct a hybrid environment where /usr is 90% our own runtime (for example sniper), and 10% the user's graphics drivers (Mesa or Nvidia or whatever) and their library dependencies. This lets the game make naive assumptions about /usr and the location of its dependency libraries, and have those assumptions be right. This is important because some native Linux games do things in their startup scripts that you would probably say are wrong, like ignoring and overwriting any LD_LIBRARY_PATH that they inherited from a parent process. As a result, anything that involves setting a LD_LIBRARY_PATH a mile long with components like /snap/x/lib:/snap/y/lib:..., and relying on it being used in all circumstances, is not going to work reliably.

This is not me imposing arbitrary requirements on Snap: I would much prefer it if all native Linux games were "well-behaved" and respected inherited settings like the LD_LIBRARY_PATH (even though I would also prefer not to be using LD_LIBRARY_PATH as a load-bearing component). Instead, it's something that we know from bitter experience, and it is not something that we can negotiate about, because it's a fact about pre-existing Linux games that are no longer actively maintained by their developer/publisher. We cannot go round changing all the games to stop making assumptions, even if those assumptions are wrong, because most of them are not our games. Instead, we try to meet the games where they are coming from, and make their assumptions be true.

The way this works in Flatpak-world is:

Normally, a Flatpak app runs in a new namespace that has the app's files mounted on /app, a runtime chosen by the app maintainer mounted on /usr (for example Steam currently uses org.freedesktop.Platform/x86_64/23.08), a subset of the user's home directory mounted on $HOME, and some sockets in places like $XDG_RUNTIME_DIR.

Normally, pressure-vessel would want to use bubblewrap to create new user and mount namespaces in which it has control over the mount table, then mount the /usr of our choice in that new mount namespace, voluntarily give up all of the extra privileges that it has over that namespace, and exec the "payload" (typically a game).

However, Flatpak apps are not allowed to create nested user/mount namespaces: Flatpak specifically blocks the syscalls that would allow them to do that. This is a constraint imposed by Flatpak's security model: if apps were allowed to create nested sandboxes, they would be able to trick host processes into thinking they were unconfined. This is because Flatpak intentionally does not require any specific LSM, so it cannot rely on LSM labelling (contrast with Snap, which relies on AppArmor, and if I understand correctly does not provide meaningful sandboxing on non-AppArmor-enabled kernels).

So, instead of running bubblewrap, when pressure-vessel detects that it's running under Flatpak it will send D-Bus API calls to flatpak-portal (the interface you linked) to create what Flatpak calls a "sub-sandbox". This creates new user/mount namespaces alongside the Flatpak app's current user/mount namespaces. As an implementation detail, we currently do the D-Bus calls from our own C code (in a tool called steam-runtime-launch-client), but they're the same D-Bus calls that you could make with a sufficiently new version of flatpak-spawn.

A good way to get some understanding of what is happening here would be to try it: install Steam as a native .deb and as a Flatpak app (perhaps on two different virtual machines), download and run some small free-to-play game that uses the sniper container runtime (Battle for Wesnoth, Endless Sky and Retroarch are good examples), and look at pstree and systemd-cgls to see how the process hierarchies are behaving. If you set the game's launch options to PRESSURE_VESSEL_SHELL=instead %command%, you'll get an xterm instead of the actual game, from which you can explore the container interactively.

In the native .deb version, you'll see that the container environment is a tree of processes "below" Steam.

In the Flatpak version, you'll see that instead, the container environment is a tree of processes below flatpak-portal, which Flatpak puts in a separate cgroup.

In both cases, you'll see that the /usr of the container environment is the Steam Runtime 3 'sniper' environment, which is basically Debian 11 with selected backports (Vulkan, SDL, that sort of thing). The usual compatibility symlinks /bin -> usr/bin, etc. also exist. However, you'll also see that some parts of /usr have been edited or removed, and replaced with symbolic links pointing into what we call the "graphics provider"; and you'll see that some key environment variables point into /usr/lib/pressure-vessel/overrides which, again, contains symlinks into the graphics provider.

The /etc of the container environment is a mixture: it's mostly the /usr/etc of the container environment, but with some edits done by either pressure-vessel or Flatpak (as appropriate) to substitute files like /etc/resolv.conf with the host version. In the Flatpak case, this is mostly done for us by Flatpak. Also, some of the /usr/etc has been edited by pressure-vessel to substitute symlinks to files from the graphics provider, the same as /usr.

The /app of the container environment is intentionally empty - I don't remember precisely why, but there was some subtle reason why mounting the Steam Flatpak app's normal /app there would have caused weird conflicts, so we don't.

In the native .deb version, the graphics provider is (a large subset of) the real root filesystem of the computer, which we mount on /run/host inside the container. I think this is analogous to /var/lib/snapd/hostfs in Snap.

In the Flatpak version, the graphics provider is the normal Flatpak environment that the Steam client uses (including extensions for graphics drivers), which Flatpak has mounted in /run/parent/{app,etc,usr,...}.

For more or less everything outside /app, /usr and /etc, including user data directories like /home/me and system sockets like X11 and Wayland, the rule is that if it's available in the environment where the Steam client runs, we expect Flatpak to make the same content available in the same location in the environment where the game runs.

In the Flatpak case, there are also a few things that we specifically needed to share between the two parallel container environments:

So, what we would need from Snap, to do something similar to what we do in Flatpak, would go something like this:

smcv commented 8 months ago

One potential challenge I want to explicitly ask about: from my tinkering with the Runtime Tools, it's my understanding that Pressure-Vessel quite literally is in large part implemented on (a modification of) bwrap

I think pressure-vessel's pv-bwrap is literally the same code as bwrap 0.8.0. If it isn't, then the differences will be very small - we try to upstream everything.

which is also a significant part of the underlying code to Flatpak. I'm curious how this potentially affects the difficulty of implementation in snapd. How much were the changes to Flatpak simplified by them being related like this?

This was mostly a matter of the design and mental model being compatible, rather than the specifics of bwrap. pressure-vessel and Flatpak do very similar things with bwrap, and in fact a lot of the code in pressure-vessel to build the bwrap command-line is directly copied from Flatpak, but Flatpak doesn't give us anywhere near that level of control over the bwrap command-line when it creates a sub-sandbox.

bwrap is quite a simple/straightforward low-level tool, because it has historically needed to be installed setuid-root on some OSs, in which case any extra convenience code would be a security risk. As a result, many of its command-line options translate relatively directly into syscalls.

One big thing that we do directly benefit from in Flatpak is that Flatpak already knows how to merge a runtime-supplied /etc with selected individual files like /etc/resolv.conf from the real host /etc, so we let it do that (and rely on the fact that it will). If Snap doesn't know how to do similarly, then either Snap or pressure-vessel will have to learn to do that. Similarly, when we're using Flatpak, we rely on Flatpak to set up services like X11, D-Bus and Wayland.

If I'm reading /proc/self/mounts correctly, Snap populates /usr with files from the Snap runtime squashfs (analogous to what Flatpak does), but it doesn't use the /etc from the Snap runtime squashfs, and instead uses the host /etc almost entirely as-is (but with AppArmor rules to lock down access to some of it, and a very small number of overrides). I don't think this is going to work reliably on all host OSs, but that's equally true for non-Steam Snap apps, so that seems out of scope here - if Snap wants to be as portable as pressure-vessel, then it will likely need to catch up with pressure-vessel in how it accounts for the fact that some host OSs are frankly bizarre, but that isn't my job!

If pvwrap is fundamentally basically just asking Flatpak to do what it was going to do anyway, but via Flatpak bwrap and associated overhead instead of its own/the system bwrap

There's less of that going on than you might think, because Flatpak doesn't give us fine-grained control over the bwrap command-line.

The best way to get a feel for this would be to try it: install Steam as a native .deb and as a Flatpak app, download and a small free-to-play game that uses the sniper container runtime (like Battle for Wesnoth), put STEAM_LINUX_RUNTIME_LOG=1 STEAM_LINUX_RUNTIME_VERBOSE=1 %command% in its launch options, run it, look at the log file SteamLinuxRuntime_sniper/var/slr-latest.log, and compare the two systems.

If you run G_MESSAGES_DEBUG=all /usr/libexec/flatpak-portal --verbose --replace first, you can also see what Flatpak is doing at the same time.

In a non-Flatpak environment, look for bwrap options before bundling and you'll see that pressure-vessel-wrap is going to finish by execve()'ing a call to pv-bwrap with a very large number of bind-mounts. We have to do all this setup for ourselves in this case, precisely because Flatpak isn't - but in the Flatpak case, we would (correctly!) not be allowed to have this level of fine-grained control.

In a Flatpak environment, look for Final command to execute and you'll see that instead, pressure-vessel-wrap finishes with a call to steam-runtime-launch-client (which is an enhanced flatpak-spawn) which is mostly environment variable manipulation and fd-passing. The pressure-vessel-specific cleverness is all encapsulated in the --usr-path, or in the pressure-vessel-adverb command that gets run inside the new container. pv-adverb is a normal, unprivileged process: it's partly there to do some final setup that would have been inconvenient to do from outside, like building a new ld.so.cache, but mostly there to hold lock-files open so that the edited /usr can't get garbage-collected while the game is still running. In the flatpak debug output, you'll see that Flatpak converts this into a very large bwrap command line that is quite similar to the one pressure-vessel would have used (because we're using a lot of the same code behind the scenes), but the precise details are not under pressure-vessel's control this time.