Discussion:
[systemd-devel] Session-specific user services
Arseny Maslennikov
2021-04-02 15:41:45 UTC
Permalink
Hi everyone!

Recently there's a trend for session-specific processes and services
(and even GUI apps, via `systemd-run --scope') to run as their own user
units on eligible systems/distros, to have a clean and controlled cgroup
hierarchy.

I've been looking at https://systemd.io/DESKTOP_ENVIRONMENTS/ and see no
answer to the following question: how can a process, e. g. gvfs-daemon,
know which logind-session is it run for (for example, to find out the
seat by session ID), if it's actually in a user service unit?
The libsystemd source for sd_pid_get_session(), which is used by gvfs as
of today, gives a hint that function won't work in that case, since a
user service process's /proc/self/cgroup does not contain
"session-XXX.scope".
Mantas Mikulėnas
2021-04-02 16:29:23 UTC
Permalink
Post by Arseny Maslennikov
Hi everyone!
Recently there's a trend for session-specific processes and services
(and even GUI apps, via `systemd-run --scope') to run as their own user
units on eligible systems/distros, to have a clean and controlled cgroup
hierarchy.
I've been looking at https://systemd.io/DESKTOP_ENVIRONMENTS/ and see no
answer to the following question: how can a process, e. g. gvfs-daemon,
know which logind-session is it run for (for example, to find out the
seat by session ID), if it's actually in a user service unit?
The libsystemd source for sd_pid_get_session(), which is used by gvfs as
of today, gives a hint that function won't work in that case, since a
user service process's /proc/self/cgroup does not contain
"session-XXX.scope".
I don't think most daemons need to know this? Polkit has already changed
its policy from allowing actions for specific active session, to allowing
them for the whole uid if it owns *an* active session.

Also, part of the trend includes not running more than one graphical
session per uid. So the daemon doesn't really need to ask, if there's only
one answer. (Note that stuff like $DISPLAY gets imported into systemd
--user's environment, to make it available to the services'
environment, and you can't make $DISPLAY have two values at once.)

And if the daemon did know "its" session, that sounds like it would make it
*less* useful with two sessions, because you would have no way to run a
second instance for the other session anyway.
--
Mantas Mikulėnas
Arseny Maslennikov
2021-04-02 17:54:05 UTC
Permalink
Post by Mantas Mikulėnas
Post by Arseny Maslennikov
Hi everyone!
Recently there's a trend for session-specific processes and services
(and even GUI apps, via `systemd-run --scope') to run as their own user
units on eligible systems/distros, to have a clean and controlled cgroup
hierarchy.
I've been looking at https://systemd.io/DESKTOP_ENVIRONMENTS/ and see no
answer to the following question: how can a process, e. g. gvfs-daemon,
know which logind-session is it run for (for example, to find out the
seat by session ID), if it's actually in a user service unit?
The libsystemd source for sd_pid_get_session(), which is used by gvfs as
of today, gives a hint that function won't work in that case, since a
user service process's /proc/self/cgroup does not contain
"session-XXX.scope".
I don't think most daemons need to know this? Polkit has already changed
its policy from allowing actions for specific active session, to allowing
Do you mean default policies as bundled by maintainers of software
upstreams? IIRC it's still possible to use session ID in rules.
Post by Mantas Mikulėnas
them for the whole uid if it owns *an* active session.
There's at least a use case to know if an active session owned by the
UID is present on seat X:
https://gitlab.gnome.org/GNOME/gvfs/-/issues/557
In short, if a USB storage drive is connected to a particular seat,
we'd like to only try automounting it on that seat, and not on a random
seat that wins a race condition.
Post by Mantas Mikulėnas
Also, part of the trend includes not running more than one graphical
session per uid. So the daemon doesn't really need to ask, if there's only
Yes, that's mentioned at the systemd.io link. That would be OK if non-graphical
simultaneous sessions are allowed as well, locally or remotely.
Post by Mantas Mikulėnas
one answer. (Note that stuff like $DISPLAY gets imported into systemd
--user's environment, to make it available to the services'
environment, and you can't make $DISPLAY have two values at once.)
And if the daemon did know "its" session, that sounds like it would make it
*less* useful with two sessions, because you would have no way to run a
second instance for the other session anyway.
Simon McVittie
2021-04-06 11:51:56 UTC
Permalink
Post by Arseny Maslennikov
There's at least a use case to know if an active session owned by the
https://gitlab.gnome.org/GNOME/gvfs/-/issues/557
In short, if a USB storage drive is connected to a particular seat,
we'd like to only try automounting it on that seat, and not on a random
seat that wins a race condition.
Consider this system:

uid 1000
\- login session 1 on seat 1
\- GNOME
\- login session 2 on seat 2
\- getty/bash
\- systemd --user
\- gvfsd process 11111
uid 2000
\- login session 3 on seat 3
\- GNOME
\- login session 4 on seat 4
\- getty/bash
\- systemd --user
\- gvfsd process 22222

(Note that this is already a bit silly because each of uid 1000 and uid
2000 presumably represents a person, and at any given time, that person
is physically sitting in at most one seat...)

If gvfsd is a `systemd --user` service, then there are two instances of it:
one for uid 1000, and one for uid 2000. It is *not* the case that there are
four instances of it, one per seat.

Now we plug a USB drive into seat 3, and another USB drive into seat 4.
Suppose both users have configured gvfsd to automount USB drives. What
we want is that uid 1000 does not mount either of the USB drives (even
if they have the necessary permissions), and uid 2000 mounts both of them.

You seem to be suggesting that the right logic is for each gvfsd instance
to run this pseudocode:

# Here get_login_session() is assumed to return non-null even if the
# process is a systemd --user service. Will it return 3 or 4 for
# uid 2000's gvfsd? It can only return one of those!
if this_process.get_login_session().seat == usb_drive.seat:
usb_drive.do_automount()

I don't think that's right, though: assuming get_login_session() is
deterministic, we will only automount *either* the USB drive on seat 3
*or* the one on seat 4, not both.

I think the logic we want in gvfsd is probably more like this:

for login_session in this_uid.get_login_sessions():
if login_session.seat == usb_drive.seat:
usb_drive.do_automount()
break

Or, perhaps even better:

# This time we want this to return null if this process is a
# systemd --user service
login_session = this_process.get_login_session()

if login_session:
# traditional D-Bus activation, or XDG autostart, or run in the
# foreground for development/debugging; responsible for only its
# own login session
if login_session.seat == usb_drive.seat:
usb_drive.do_automount()
else:
# systemd --user service, shared by all our login sessions
for login_session in this_uid.get_login_sessions():
if login_session.seat == usb_drive.seat:
usb_drive.do_automount()
break

either of which will do the right thing: uid 1000 will not automount either
of the USB sticks (even if they have permissions to do so), and uid 2000
will automount both of them.

smcv
Arseny Maslennikov
2021-04-06 13:51:10 UTC
Permalink
Post by Simon McVittie
Post by Arseny Maslennikov
There's at least a use case to know if an active session owned by the
https://gitlab.gnome.org/GNOME/gvfs/-/issues/557
In short, if a USB storage drive is connected to a particular seat,
we'd like to only try automounting it on that seat, and not on a random
seat that wins a race condition.
uid 1000
\- login session 1 on seat 1
\- GNOME
\- login session 2 on seat 2
\- getty/bash
\- systemd --user
\- gvfsd process 11111
uid 2000
\- login session 3 on seat 3
\- GNOME
\- login session 4 on seat 4
\- getty/bash
\- systemd --user
\- gvfsd process 22222
(Note that this is already a bit silly because each of uid 1000 and uid
2000 presumably represents a person, and at any given time, that person
is physically sitting in at most one seat...)
At any given time one or both sessions might be locked (and thus
inactive). Anyway, I wouldn't expect additional complexity that
explicitly forbids this scenario.
Post by Simon McVittie
one for uid 1000, and one for uid 2000. It is *not* the case that there are
four instances of it, one per seat.
Now we plug a USB drive into seat 3, and another USB drive into seat 4.
Suppose both users have configured gvfsd to automount USB drives. What
we want is that uid 1000 does not mount either of the USB drives (even
if they have the necessary permissions), and uid 2000 mounts both of them.
IOW, if we're a user service, query all the sessions and their seats and
look for the right seat in there.
I agree with you here, this is a solid approach.
Post by Simon McVittie
You seem to be suggesting that the right logic is for each gvfsd instance
# Here get_login_session() is assumed to return non-null even if the
# process is a systemd --user service. Will it return 3 or 4 for
# uid 2000's gvfsd? It can only return one of those!
usb_drive.do_automount()
I don't think that's right, though: assuming get_login_session() is
deterministic, we will only automount *either* the USB drive on seat 3
*or* the one on seat 4, not both.
usb_drive.do_automount()
break
# This time we want this to return null if this process is a
# systemd --user service
login_session = this_process.get_login_session()
# traditional D-Bus activation, or XDG autostart, or run in the
# foreground for development/debugging; responsible for only its
# own login session
usb_drive.do_automount()
# systemd --user service, shared by all our login sessions
usb_drive.do_automount()
break
This behaviour would make sense to me.
Post by Simon McVittie
either of which will do the right thing: uid 1000 will not automount either
of the USB sticks (even if they have permissions to do so), and uid 2000
will automount both of them.
smcv
Arseny Maslennikov
2021-04-06 16:02:37 UTC
Permalink
Post by Mantas Mikulėnas
one answer. (Note that stuff like $DISPLAY gets imported into systemd
--user's environment, to make it available to the services'
environment, and you can't make $DISPLAY have two values at once.)
Oh. There's a design problem here as well: if session-specific software
sets environment variables for user services, they are not removed from
the user manager env store when that session terminates.

Let's consider the following scenario:
1. A machine is booted up and is ready to establish user sessions.
2. User U logs in (maybe remotely, or locally on a VT/kmscon) and
establishes an XDG_SESSION_TYPE=tty session Id=1. A systemd --user
instance is launched.
3. Later on, user U logs into a graphical session Id=2 at a
seat on the machine. `systemctl import-environment DISPLAY
XAUTHORITY', etc. Some time afterwards user U logs out; session 1 is
intact, as well as the systemd --user instance; the now-defunct
DISPLAY is not expunged from the systemd --user environment
dictionary.
4. User U logs in into a graphical session Id=3 again, but with a
Wayland-based setup, e. g. to work around a bug. `systemctl
import-environment WAYLAND_DISPLAY', etc. The defunct DISPLAY is
still present in the environment!


The above was a brainstormed reproducer; I did actually encounter this
kind of bug IRL a couple of times this way.
I'd used GDM to log in to GNOME on Xorg, then logged out, then logged in
to Sway (a Wayland compositor/window manager), and some apps e. g.
telegram-desktop malfunction because they infer they're running in GNOME
from the GNOME-specific environment vars that were not unset on logout
from GNOME:

% systemctl --user show-environment | grep -i GNOME
DESKTOP_SESSION=gnome
GDMSESSION=gnome
GNOME_DESKTOP_SESSION_ID=this-is-deprecated
XDG_CURRENT_DESKTOP=GNOME
XDG_MENU_PREFIX=gnome-
XDG_SESSION_DESKTOP=gnome
_=/usr/bin/gnome-session

The _, most likely set by a unix shell, is especially funny. :)

The leaked values of XDG_CURRENT_DESKTOP and XDG_SESSION_DESKTOP also
break xdg-desktop-portal in the new session as well, etc. In short,
this is not robust enough and can break in a multitude of ways, most of
them unknown a priori.

To sum it up, if we use the user manager environment store this way,
there should be a way to automatically remove session-specific
environment vars from systemd --user when the respective session is
terminated.

Possible ways to fix:
* Per-session unit managers: this approach was discussed previously and
deemed hard to implement; unclear relationships between session units
and user units.
* Tie the systemd--user environment store entries to session IDs.
When logind emits org.freedesktop.login1.Manager.SessionRemoved on
/org/freedesktop/login1, remove environment variables tied to that
session.
* user-specific environment stores for graphical sessions in logind and
an opt-in mechanism (e. g. UseSessionEnvironment=yes) in unit files to
use them, API to modify/flush them (on the bus and via loginctl),
autoflush when the graphical session dies.
Not too hard to implement, basically 1 hash table per user.
A session starter would do
`loginctl import-environment XDG_CURRENT_DESKTOP'
instead of
`systemctl import-environment XDG_CURRENT_DESKTOP'
If I recall correctly, user managers already requires logind, so
that's not a problem.

I'm not opposed to writing a patch to fix this or just filing an issue
report, but wanted to discuss the general direction we'd like to take
here first.
Benjamin Berg
2021-04-15 14:38:00 UTC
Permalink
Hi,
[SNIP]
The above was a brainstormed reproducer; I did actually encounter this
kind of bug IRL a couple of times this way.
I'd used GDM to log in to GNOME on Xorg, then logged out, then logged in
to Sway (a Wayland compositor/window manager), and some apps e. g.
telegram-desktop malfunction because they infer they're running in GNOME
from the GNOME-specific environment vars that were not unset on logout
  % systemctl --user show-environment | grep -i GNOME
  DESKTOP_SESSION=gnome
  GDMSESSION=gnome
  GNOME_DESKTOP_SESSION_ID=this-is-deprecated
  XDG_CURRENT_DESKTOP=GNOME
  XDG_MENU_PREFIX=gnome-
  XDG_SESSION_DESKTOP=gnome
  _=/usr/bin/gnome-session
The _, most likely set by a unix shell, is especially funny. :)
Yes, this is a problem. The _ is really not great, we could add it to
the ignore list.
The leaked values of XDG_CURRENT_DESKTOP and XDG_SESSION_DESKTOP also
break xdg-desktop-portal in the new session as well, etc. In short,
this is not robust enough and can break in a multitude of ways, most
of them unknown a priori.
To sum it up, if we use the user manager environment store this way,
there should be a way to automatically remove session-specific
environment vars from systemd --user when the respective session is
terminated.
* Per-session unit managers: this approach was discussed previously and
  deemed hard to implement; unclear relationships between session
units
  and user units.
* Tie the systemd--user environment store entries to session IDs.
  When logind emits org.freedesktop.login1.Manager.SessionRemoved on
  /org/freedesktop/login1, remove environment variables tied to that
  session.
* user-specific environment stores for graphical sessions in logind and
  an opt-in mechanism (e. g. UseSessionEnvironment=yes) in unit files
to
  use them, API to modify/flush them (on the bus and via loginctl),
  autoflush when the graphical session dies.
  Not too hard to implement, basically 1 hash table per user.
  A session starter would do
    `loginctl import-environment XDG_CURRENT_DESKTOP'
  instead of
    `systemctl import-environment XDG_CURRENT_DESKTOP'
  If I recall correctly, user managers already requires logind, so
  that's not a problem.
So, what I had been thinking to do in GNOME at one point (but never
did), is to write a file in $XDG_RUNTIME_DIR of all variables that were
set by GNOME (possibly containing the old values).

Then, at logout, unset these variables (or reset to old values). In
GNOME, everything is going through the gnome-session binary, and it
would be quite simple to implement this in a safe manner.
I'm not opposed to writing a patch to fix this or just filing an
issue report, but wanted to discuss the general direction we'd like
to take here first.
Hmm, what I think would be helpful is a mechanism to restart services
after the session has adjusted the environment. Otherwise these
services will not pick up changes in the environment; usually not a big
deal, but it can for example cause issues when the user changes their
language and e.g. pulseaudio/pipewire are not restarted even though the
user has logged out for a short time.

Maybe we could have a feature in systemd that causes services to
restart when certain variables in their environment block have changed?

Benjamin

Loading...