Skip to content

Commit 1d2a0c0

Browse files
committed
Sanitize systemd user services
In some cases, snapd insists on enabling user services not only globally, as expected (thus, adding a symlink at `/etc/systemd/user/XXXXX.target.wants`), but also locally (thus, at $HOME/.config/systemd/user/XXXX.target.wants`). This means that if an user service is migrated from `default.target` to `graphical-session.target` (or vice-versa), the soft link at the user's HOME folder will be in the wrong subfolder. Unfortunately, the main snapd service seems to not have the capability of accessing and managing the local user services, having to rely on the user's local daemon (`snap userd`), which runs as an user's service. So a change in that daemon (which has its source code at `usersession/userd` folder) is required to check the locally enabled user services and ensure that their soft link is in the right place. This commit does this by checking the links in the user folder every time user session daemon is launched, and runs as the user a `systemctl --user reenable SNAP-SERVICE-NAME` to rebuild the wrong softlink.
1 parent 6e4eb95 commit 1d2a0c0

File tree

3 files changed

+110
-0
lines changed

3 files changed

+110
-0
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,6 @@ require (
5252
github.com/rogpeppe/go-internal v1.6.1 // indirect
5353
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
5454
golang.org/x/term v0.20.0 // indirect
55+
gopkg.in/ini.v1 v1.67.1 // indirect
5556
maze.io/x/crypto v0.0.0-20190131090603-9b94c9afe066 // indirect
5657
)

go.sum

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ github.com/canonical/tcglog-parser v0.0.0-20240924110432-d15eaf652981/go.mod h1:
1717
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
1818
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
1919
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
20+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2021
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
22+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2123
github.com/frankban/quicktest v1.2.2 h1:xfmOhhoH5fGPgbEAlhLpJH9p0z/0Qizio9osmvn9IUY=
2224
github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20=
2325
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
@@ -49,6 +51,7 @@ github.com/mvo5/libseccomp-golang v0.9.1-0.20180308152521-f4de83b52afb/go.mod h1
4951
github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q=
5052
github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI=
5153
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
54+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5255
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
5356
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
5457
github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a h1:3QH7VyOaaiUHNrA9Se4YQIRkDTCw1EJls9xTUCaCeRM=
@@ -63,7 +66,15 @@ github.com/snapcore/maze.io-x-crypto v0.0.0-20190131090603-9b94c9afe066 h1:InG0E
6366
github.com/snapcore/maze.io-x-crypto v0.0.0-20190131090603-9b94c9afe066/go.mod h1:VuAdaITF1MrGzxPU+8GxagM1HW2vg7QhEFEeGHbmEMU=
6467
github.com/snapcore/secboot v0.0.0-20260129175210-e638825ef829 h1:9qeADnUPs/YhO0tty+j2zxi9dUI2Bn96y9Nc9XOKTOk=
6568
github.com/snapcore/secboot v0.0.0-20260129175210-e638825ef829/go.mod h1:BeEYaTJC4cqXVgpjjxajO31p2kVDvXwXJJx3YD7nCaE=
69+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
70+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
71+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
72+
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
73+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
74+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
6675
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
76+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
77+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
6778
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
6879
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
6980
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -97,6 +108,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
97108
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
98109
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
99110
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
111+
gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
112+
gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
100113
gopkg.in/macaroon.v1 v1.0.0 h1:BmexIS8QyY02i0uoeXwrtlH8vCS/Rmxq9uzOy4qQvk8=
101114
gopkg.in/macaroon.v1 v1.0.0/go.mod h1:KeG3in9Jb7Z3RNA/PFngm+mISBo0Q0O9KQeF958zuoQ=
102115
gopkg.in/retry.v1 v1.0.3 h1:a9CArYczAVv6Qs6VGoLMio99GEs7kY9UzSF9+LD+iGs=
@@ -105,5 +118,6 @@ gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV
105118
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk=
106119
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
107120
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
121+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
108122
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
109123
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

usersession/userd/userd.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,20 @@ package userd
2121

2222
import (
2323
"fmt"
24+
"io/fs"
25+
"os"
26+
"path"
27+
"path/filepath"
28+
"strings"
2429

2530
"github.com/godbus/dbus/v5"
2631
"github.com/godbus/dbus/v5/introspect"
32+
"gopkg.in/ini.v1"
2733
"gopkg.in/tomb.v2"
2834

2935
"github.com/snapcore/snapd/dbusutil"
3036
"github.com/snapcore/snapd/logger"
37+
"github.com/snapcore/snapd/systemd"
3138
)
3239

3340
type dbusInterface interface {
@@ -50,6 +57,92 @@ var userdBusNames = []string{
5057
"io.snapcraft.Settings",
5158
}
5259

60+
func clearBrokenLink(targetPath string) error {
61+
_, err := filepath.EvalSymlinks(targetPath)
62+
if err != nil {
63+
switch err.(type) {
64+
case *fs.PathError:
65+
logger.Noticef("deleting broken link in snap service %s", targetPath)
66+
// it's a broken link; delete it
67+
os.Remove(targetPath)
68+
default:
69+
return err
70+
}
71+
}
72+
return nil
73+
}
74+
75+
func reenableUserService(serviceName string) error {
76+
logger.Noticef("re-enabling user service %s", serviceName)
77+
sysd := systemd.New(systemd.UserMode, nil)
78+
return sysd.DaemonReEnable([]string{serviceName})
79+
}
80+
81+
func checkServicePlacement(targetName, snapTarget string) error {
82+
snapTargetData, err := os.ReadFile(snapTarget)
83+
if err != nil {
84+
return err
85+
}
86+
targetIni, err := ini.Load(snapTargetData)
87+
if err != nil {
88+
return err
89+
}
90+
wantedBy := ""
91+
if section := targetIni.Section("Install"); section != nil {
92+
wantedBy = section.Key("WantedBy").String()
93+
}
94+
if wantedBy != targetName {
95+
// the symlink is in the wrong folder. Re-enable the service
96+
// to change it.
97+
err := reenableUserService(path.Base(snapTarget))
98+
if err != nil {
99+
return err
100+
}
101+
}
102+
return nil
103+
}
104+
105+
func sanitizeUserServices() error {
106+
// ensure that the user services enabled in the $HOME folder are
107+
// correctly placed in the right .target.wants folder. This placement
108+
// will be wrong if a service is moved from default.target to
109+
// graphical-session.target or vice-versa, so this clean up is required.
110+
//
111+
// The change can happen in two cases:
112+
//
113+
// * when migrating from an old version of snapd without "graphical-session.target"
114+
// support, to a new version that supports it: all the user daemons' WantedBy entry
115+
// will be updated, and so these entries will have to be updated too.
116+
// * when a snap adds or removes the `desktop` plug in a daemon
117+
118+
userSystemdQuery := path.Join(os.Getenv("HOME"), ".config", "systemd", "user", "*.target.wants")
119+
entries, err := filepath.Glob(userSystemdQuery)
120+
if err != nil {
121+
return err
122+
}
123+
for _, targetWantPath := range entries {
124+
snapTargetPaths, err := filepath.Glob(path.Join(targetWantPath, "snap.*"))
125+
if err != nil {
126+
logger.Noticef("cannot get the user service files at %s: %s", targetWantPath, err.Error())
127+
continue
128+
}
129+
for _, snapTarget := range snapTargetPaths {
130+
// Remove any broken link (that's a service that was removed)
131+
if err = clearBrokenLink(snapTarget); err != nil {
132+
logger.Noticef("cannot remove the broken link %s: %s", snapTarget, err.Error())
133+
continue
134+
}
135+
// Check that any snap service is in the right .target.wants
136+
targetName := strings.TrimSuffix(targetWantPath, ".wants")
137+
if err := checkServicePlacement(path.Base(targetName), snapTarget); err != nil {
138+
logger.Noticef("cannot process user service at %s for %s: %s", snapTarget, targetName, err.Error())
139+
continue
140+
}
141+
}
142+
}
143+
return nil
144+
}
145+
53146
func (ud *Userd) Init() error {
54147
var err error
55148

@@ -86,6 +179,8 @@ func (ud *Userd) Init() error {
86179
return fmt.Errorf("cannot obtain bus name '%s'", name)
87180
}
88181
}
182+
183+
sanitizeUserServices()
89184
return nil
90185
}
91186

0 commit comments

Comments
 (0)