Skip to content

many: reintroduce fdstore helpers - #16784

Merged
ZeyadYasser merged 10 commits into
canonical:masterfrom
ZeyadYasser:add-fdstore-helpers-second-attempt
Jun 9, 2026
Merged

many: reintroduce fdstore helpers#16784
ZeyadYasser merged 10 commits into
canonical:masterfrom
ZeyadYasser:add-fdstore-helpers-second-attempt

Conversation

@ZeyadYasser

@ZeyadYasser ZeyadYasser commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

This commit reintroduces reverted PR #16119

systemd: add fdstore helpers
The initial use case of using systemd's fdstore is to keep sensitive data that cannot be persisted to disk like recovery keys and passphrases while having them survive snapd restarts to increase the robustness of FDE operation.

netutil: only use activation sockets passed from systemd
In upcoming work, more fds will be passed from systemd on startup, previously the helper from go-system considered all passed fds as activation fds which will not be true in the future.

packaging: remove go-systemd dependency
This was the only usage of go-systemd, so I am removing its dependency as well.

JIRA: SNAPDENG-36595

@ZeyadYasser ZeyadYasser added this to the 2.76 milestone Mar 18, 2026
@ZeyadYasser ZeyadYasser added Needs Samuele review Needs a review from Samuele before it can land Run nested The PR also runs tests inluded in nested suite labels Mar 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR reintroduces systemd file-descriptor-store (fdstore) helpers and updates activation socket handling to avoid treating all passed FDs as activation sockets, enabling future work to keep sensitive data (e.g., recovery keys/passphrases) across snapd restarts without persisting it to disk.

Changes:

  • Add Linux/non-Linux split implementations for systemd.SdNotify, and introduce SdNotifyWithFds on Linux.
  • Introduce systemd/fdstore package to manage named FDs from systemd fdstore and expose activation socket FDs.
  • Replace go-systemd/activation usage in netutil with fdstore.ActivationSocketFds, and drop go-systemd dependency from Go module and packaging manifests.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
systemd/sdnotify.go Removed monolithic sdnotify implementation (replaced by per-OS files).
systemd/sdnotify_linux.go New Linux implementation of SdNotify and SdNotifyWithFds.
systemd/sdnotify_other.go New non-Linux stub returning unsupported error.
systemd/sdnotify_linux_test.go New Linux-only tests, including FD-passing coverage.
systemd/sdnotify_test.go Removed prior cross-platform sdnotify tests (replaced by linux-only tests).
systemd/fdstore/fdstore.go New fdstore helper package for named FDs and activation socket filtering.
systemd/fdstore/fdstore_test.go New tests covering init/prune/add/remove/socket-fd behavior.
systemd/fdstore/export_test.go New test-only hooks/mocks for fdstore internals.
netutil/activation.go Switch activation listener discovery from go-systemd to fdstore.ActivationSocketFds.
packaging/fedora/snapd.spec Remove go-systemd activation dependency from RPM spec.
packaging/debian-sid/control Remove go-systemd dependency from Debian control file.
go.mod / go.sum Drop github.com/coreos/go-systemd module dependency.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread netutil/activation.go Outdated
Comment on lines 74 to 83
for name, fds := range socketFds {
for _, fd := range fds {
f := os.NewFile(uintptr(fd), name)
ln, err := net.FileListener(f)
if err != nil {
return nil, err
}
addr := ln.Addr().String()
lns[addr] = ln
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't necessarily wrong, net.FileListener() does a dup inside. I hadn't realized this before, but we likely have an ownership issue in fdstore.Get() and ActivationSocketFds(). i'll leave a comment there

Comment on lines +103 to +113
ch := make(chan string)
go func() {
var buf [128]byte
n, err := conn.Read(buf[:])
c.Assert(err, IsNil)
ch <- string(buf[:n])
}()

err = systemd.SdNotify("something")
c.Assert(err, IsNil)
c.Check(<-ch, Equals, "something")
Comment thread systemd/fdstore/fdstore.go
_, err = unix.Seek(msgfds[1], 0, 0)
panicOnErr(err)
_, err = unix.Write(msgfds[1], []byte("hello-from-the-other-side-2"))
panicOnErr(err)
Comment thread systemd/fdstore/fdstore.go Outdated
if shouldRemove {
logger.Noticef("removing unexpected fdstore entry %q", name)
if err := removeUnlocked(name); err != nil {
logger.Noticef("internal error: cannot remove fdstore entry %q: %v\n", name, err)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep

@codecov

codecov Bot commented Mar 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.83495% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.20%. Comparing base (8722e8b) to head (5059e41).
⚠️ Report is 169 commits behind head on master.

Files with missing lines Patch % Lines
systemd/fdstore/fdstore.go 93.36% 9 Missing and 4 partials ⚠️
netutil/activation.go 0.00% 10 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #16784      +/-   ##
==========================================
+ Coverage   79.04%   79.20%   +0.15%     
==========================================
  Files        1370     1379       +9     
  Lines      190784   193253    +2469     
  Branches     2465     2466       +1     
==========================================
+ Hits       150810   153067    +2257     
- Misses      30888    30992     +104     
- Partials     9086     9194     +108     
Flag Coverage Δ
unittests 79.20% <88.83%> (+0.15%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Mar 18, 2026

Copy link
Copy Markdown

Mon Jun 8 19:23:57 UTC 2026
The following results are from: https://github.com/canonical/snapd/actions/runs/27120883640

Failures:

Preparing:

  • openstack:opensuse-16.0-64:
  • openstack:opensuse-16.0-64:
  • openstack:opensuse-16.0-64:
  • openstack:opensuse-16.0-64:

Executing:

  • openstack:opensuse-16.0-64:tests/main/security-logging
  • openstack:opensuse-tumbleweed-selinux-64:tests/main/security-logging
  • openstack:opensuse-tumbleweed-64:tests/main/security-logging
  • garden:ubuntu-core-18-64:tests/core/auto-refresh-backoff-after-reboot:kernel

Restoring:

  • openstack:opensuse-16.0-64:
  • openstack:opensuse-16.0-64:

Skipped tests from snapd-testing-skip

If you wish to have any of the below tests run in your PR, in your PR description, add 'unskip:' followed by a copy-and-pasted list (without variants) of the below tests you wish to run (unskip plus test list must be valid yaml)

  • garden:ubuntu-25.10-64:tests/main/apparmor-prompting-support
  • openstack-arm:ubuntu-24.04-arm-64:tests/main/i18n
  • openstack-arm:ubuntu-core-24-arm-64:tests/main/i18n
  • openstack:debian-sid-64:tests/main/interfaces-network-status-classic
  • openstack:ubuntu-24.04-64:tests/main/i18n
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-flag-restart
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:audio_record_single
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:audio_record_timespan_allow
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:audio_record_timespan_deny
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:create_multiple_actioned_by_other_pid_always_allow
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:create_multiple_actioned_by_other_pid_always_deny
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:create_multiple_allow
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:create_multiple_deny
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:create_multiple_not_actioned_by_other_pid_single_allow
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:create_multiple_not_actioned_by_other_pid_single_deny
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:create_write_chmod_same_fd_single_allow
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:create_write_chmod_same_path_single_allow
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:create_write_write_same_path_single_deny
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:download_file_conflict
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:download_file_defaults
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:download_file_safer
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:read_single_allow
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:read_single_deny
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:special_characters
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:timespan_allow
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:timespan_deny
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:write_read_multiple_actioned_by_other_pid_allow_deny
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:write_read_multiple_actioned_by_other_pid_deny_allow
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:write_single_allow
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:write_single_deny
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-prompt-restoration
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:audiorecord_allow_forever
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:audiorecord_allow_session
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:audiorecord_allow_single
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:audiorecord_allow_timespan
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:audiorecord_deny_forever
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:audiorecord_deny_session
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:audiorecord_deny_single
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:audiorecord_deny_timespan
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:camera_allow_forever
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:camera_allow_session
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:camera_allow_single
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:camera_allow_timespan
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:camera_deny_forever
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:camera_deny_session
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:camera_deny_single
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:camera_deny_timespan
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:home_allow_forever
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:home_allow_session
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:home_allow_single
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:home_allow_timespan
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:home_deny_forever
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:home_deny_session
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:home_deny_single
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-smoke:home_deny_timespan
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-snapd-startup
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-support
  • openstack:ubuntu-25.10-64:tests/main/interfaces-requests-activates-handlers
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-flag-restart
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:audio_record_single
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:audio_record_timespan_allow
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:audio_record_timespan_deny
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:create_multiple_actioned_by_other_pid_always_allow
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:create_multiple_actioned_by_other_pid_always_deny
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:create_multiple_allow
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:create_multiple_deny
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:create_multiple_not_actioned_by_other_pid_single_allow
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:create_multiple_not_actioned_by_other_pid_single_deny
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:create_write_chmod_same_fd_single_allow
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:create_write_chmod_same_path_single_allow
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:create_write_write_same_path_single_deny
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:download_file_conflict
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:download_file_defaults
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:download_file_safer
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:read_single_allow
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:read_single_deny
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:special_characters
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:timespan_allow
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:timespan_deny
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:write_read_multiple_actioned_by_other_pid_allow_deny
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:write_read_multiple_actioned_by_other_pid_deny_allow
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:write_single_allow
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests:write_single_deny
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-prompt-restoration
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:audiorecord_allow_forever
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:audiorecord_allow_session
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:audiorecord_allow_single
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:audiorecord_allow_timespan
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:audiorecord_deny_forever
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:audiorecord_deny_session
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:audiorecord_deny_single
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:audiorecord_deny_timespan
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:camera_allow_forever
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:camera_allow_session
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:camera_allow_single
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:camera_allow_timespan
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:camera_deny_forever
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:camera_deny_session
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:camera_deny_single
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:camera_deny_timespan
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:home_allow_forever
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:home_allow_session
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:home_allow_single
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:home_allow_timespan
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:home_deny_forever
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:home_deny_session
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:home_deny_single
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke:home_deny_timespan
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-snapd-startup
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-support
  • openstack:ubuntu-26.04-64:tests/main/i18n
  • openstack:ubuntu-26.04-64:tests/main/interfaces-requests-activates-handlers

FdNameMemfdSecretState FdName = "memfd-secret-state"
)

var knownFdNames = map[FdName]bool{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe some explanation is needed here. Is this all the names that are not sockets that we just maintain within snapd? And in that case, will we need some way to register extra names here in the future?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this all the names that are not sockets that we just maintain within snapd?

Yes, I will add a comment to make it clear.

And in that case, will we need some way to register extra names here in the future?

I don't expect the list to grow that much, which is why I went with centralized hand written list.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this is fine. But if we do grow it we should start registering them in init().

Comment thread systemd/fdstore/fdstore.go Outdated
return removeUnlocked(name)
}

func removeUnlocked(name FdName) (err error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Unlocked"? What is unlocked?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should have been "Locked" but as in caller should hold the mutex lock, it's a mistake.

I will just name it remove and have the doc comment explicitly make it clear that the caller should hold the lock

Comment thread systemd/fdstore/fdstore.go Outdated
if name.isSocket() {
// Activation socket file descriptors should be accessed
// through ActivationSocketFds.
return -1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be an error return instead?

Comment thread systemd/sdnotify_linux.go
if err != nil {
return err
}
defer conn.Close()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should just keep it open. Just make sure to have O_CLOEXEC.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was copy-pasted from here, I will add a todo so the PR doesn't grow further

func SdNotify(notifyState string) error {
if notifyState == "" {
return fmt.Errorf("cannot use empty notify state")
}
notifySocket := osGetenv("NOTIFY_SOCKET")
if notifySocket == "" {
return fmt.Errorf("cannot find NOTIFY_SOCKET environment")
}
if !strings.HasPrefix(notifySocket, "@") && !strings.HasPrefix(notifySocket, "/") {
return fmt.Errorf("cannot use NOTIFY_SOCKET %q", notifySocket)
}
raddr := &net.UnixAddr{
Name: notifySocket,
Net: "unixgram",
}
conn, err := net.DialUnix("unixgram", nil, raddr)
if err != nil {
return err
}
defer conn.Close()
_, err = conn.Write([]byte(notifyState))
return err
}

Comment thread systemd/fdstore/fdstore.go Outdated
if len(fds) != 1 {
return -1
}
return fds[0]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned earlier, I think there's a potential ownership issue. Assume that the caller takes the Fd, calls os.NewFile() and proceeds to call Close(). At this point the fd is closed and should Get() be called again it returns an integer, but the value does not correspond to a valid fd anymore. It feels like that when someone Get()s the fd, they really get it and fdstore should stop tracking it, thus the actual fd is "consumed" by the caller. This applies to ActivationSocketFds() as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless we dup(), but I don't think there's any scenario in which this would be useful.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also note that the caller doesn't even need to call Close() on the file, it's enough to stop referencing the *os.File for the object to be collected and finalizer (which closes the fd) be called.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I left a comment/question in the removal code. I wonder if on removal consumed fds should be closed implicitly vs making it the burden of the consumer. I am leaning towards the latter and closing fds on removal only if they are not consumed.

Comment thread systemd/fdstore/fdstore.go Outdated
return -1, fmt.Errorf("%s: found more than one matching file descriptors", errPrefix)
}

consumed[name] = true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to keep track of what was consumed/removed?

Comment thread systemd/fdstore/fdstore.go Outdated
if name.isSocket() {
// Activation socket file descriptors should be accessed
// through ActivationSocketFds.
return -1, fmt.Errorf("%s: socket found, use ActivationSocketFds instead", errPrefix)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just an API misuse, let's make it known:

Suggested change
return -1, fmt.Errorf("%s: socket found, use ActivationSocketFds instead", errPrefix)
return -1, fmt.Errorf("internal error: %s: socket found, use ActivationSocketFds instead", errPrefix)

Comment thread netutil/activation.go Outdated
lns = make(map[string]net.Listener, len(socketFds))
for name, fds := range socketFds {
for _, fd := range fds {
f := os.NewFile(uintptr(fd), name)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe the API should really return *os.File instead of the OS abstraction which is an int value

Comment thread systemd/fdstore/fdstore.go Outdated
// ActivationSocketFds returns activation socket file descriptors
// that were passed from systemd. Only sockets whose name has a
// ".socket" suffix are returned.
func ActivationSocketFds() (socketFds map[string][]int) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func ActivationSocketFds() (socketFds map[string][]int) {
func ActivationSocketFds() (socketFds map[string][]*os.File) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this could return []net.Listener instead of files and then avoid the problem with dup entirely

Comment thread systemd/fdstore/fdstore.go Outdated
// matching file descriptors are found or if the passed name corresponds
// to a socket (i.e. ends in ".socket"). To get activation sockets use
// fdstore.ActivationSocketFds() instead.
func Get(name FdName) (fd int, err error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func Get(name FdName) (fd int, err error) {
func Get(name FdName) (f *os.File, err error) {

Comment thread systemd/fdstore/fdstore.go Outdated
//
// - The file descriptors can be retrieved by calling Get().
// - Only a single file descriptor can associated with a FdName.
func Add(name FdName, fd int) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func Add(name FdName, fd int) error {
func Add(name FdName, f *os.File) error {

Comment thread systemd/fdstore/fdstore.go Outdated
// matching file descriptors are found or if the passed name corresponds
// to a socket (i.e. ends in ".socket"). To get activation sockets use
// fdstore.ActivationSocketFds() instead.
func Get(name FdName) (fd int, err error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed with @ZeyadYasser how to tweak the API such that we would not have to deal with potential fd invalidation if the caller misuse the os.File objects they receive. One of the options is to have Add and Get (by extension ActivationSocketFds too) duplicate the fd before passing it to the caller.

Comment thread systemd/fdstore/fdstore.go Outdated
// ActivationSocketFds returns activation socket file descriptors
// that were passed from systemd. Only sockets whose name has a
// ".socket" suffix are returned.
func ActivationSocketFds() (socketFds map[string][]int) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this could return []net.Listener instead of files and then avoid the problem with dup entirely

@bboozzoo bboozzoo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just some smaller tweaks

Comment thread systemd/fdstore/fdstore.go Outdated
//
// https://www.freedesktop.org/software/systemd/man/latest/sd_pid_notify_with_fds.html#FDNAME=%E2%80%A6
if err := systemd.EnsureAtLeast(236); err != nil {
return fmt.Errorf("cannot add file descriptor to fdstore: %v", err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could use a distinct error, ErrUnsupported or something along those lines

Comment thread systemd/fdstore/fdstore.go Outdated
// - The file descriptors can be retrieved by calling Get().
// - Only a single file descriptor can associated with a FdName.
//
// It is the caller's responsibility to close f when finished.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// It is the caller's responsibility to close f when finished.
// Maintains a copy of the underlying file descriptor internally. It is the caller's
// responsibility to close f when finished.

Comment thread systemd/fdstore/fdstore.go Outdated
Comment on lines +192 to +194
// Note: Removing the all references of os.File will impicitly
// close opened fds by finalizer for os.File so no need to
// explicitly call close.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

though we could call Close(), couldn't we?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could, I think this was left over from when fdstore didn't hold a copy and instead shared the raw fds with consumers

Comment thread systemd/fdstore/fdstore.go Outdated
return nil
}

// Get retrieves file descriptor passed from systemd by its name.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Get retrieves file descriptor passed from systemd by its name.
// Get retrieves a duplicate of file descriptor passed from systemd by its name.

Comment thread systemd/fdstore/fdstore.go Outdated
// to a socket (i.e. ends in ".socket"). To get activation sockets use
// fdstore.ActivationListeners() instead.
//
// It is the caller's responsibility to close f when finished.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fdstore holds a copy of the file descriptor, the caller needs to call Remove() on top of closing
all privately held references in order to release all resources associated with a given fd.

This commit reintroduces reverted PR canonical#16119

* systemd: add fdstore helpers

The initial use case of using systemd's fdstore
is to keep sensitive data that cannot be persisted
to disk like recovery keys and passphrases while
having them survive snapd restarts to increase the
robustness of FDE operation.

* netutil: only use activation sockets passed from systemd

In upcoming work, more fds will be passed from systemd on
startup, previously the helper from go-system considered
all passed fds as activation fds which will not be true
in the future.

* packaging: remove go-systemd dependency

This was the only usage of go-systemd, so I am removing
its dependency as well.

---------

Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>
Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>
Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>
Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>
@ZeyadYasser
ZeyadYasser force-pushed the add-fdstore-helpers-second-attempt branch from a23b454 to b02ed00 Compare April 29, 2026 12:46
Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>
@ZeyadYasser
ZeyadYasser force-pushed the add-fdstore-helpers-second-attempt branch from b02ed00 to 6c74eb0 Compare April 30, 2026 07:53
Comment thread data/systemd/snapd.service.in Outdated
Comment on lines +32 to +34
# Should this be enabled to survive soft reboots and service stop/start
# starting with systmed v254+?
#FileDescriptorStorePreserve=yes

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think systemd ignores what it does not know. So we could set it. But I would do it only if we it was tested. Maybe with a spread test even. Otherwise I would not even have it as a comment.

Comment thread systemd/fdstore/fdstore.go Outdated
logger.Noticef("unexpected fdstore entry %q found: %v", name, err)
shouldRemove = true
}
// Only activation sockets can be associated with multiple fds.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose this is a restriction for us, not for systemd. Could you maybe rephrase to make it less misleading?

Comment thread systemd/fdstore/fdstore.go Outdated
if err != nil {
return nil, err
}
unixCloseOnExec(duplicatedFd)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add todo here too about check for error.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or rather check the error

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unix.CloseOnExec does not return an error, maybe I should switch to the raw fnctl?

return
}

// Make sure initialization only happens once, only here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw could we use sync.Once?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is good to always unset those to avoid anything else parsing them, it is to avoid other parts of the code base of parsing them afterwards.

Comment thread systemd/fdstore/fdstore.go Outdated
Comment on lines +61 to +63
osGetenv = os.Getenv
osUnsetenv = os.Unsetenv
osLookupEnv = os.LookupEnv

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're not running unit tests within a package in parallel, so you may as well mock specific things in the environment like we do elsewhere

Comment thread systemd/fdstore/fdstore.go Outdated

// Get retrieves a duplicate of the file descriptor passed from systemd by
// its name. close-on-exec is set on the returned file descriptor. An error
// is returned if no matching file descriptor is found, if more than one

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// is returned if no matching file descriptor is found, if more than one
// matching ErrNotFound is returned if no matching file descriptor is found.

Comment thread systemd/fdstore/fdstore.go Outdated

// Get retrieves a duplicate of the file descriptor passed from systemd by
// its name. close-on-exec is set on the returned file descriptor. An error
// is returned if no matching file descriptor is found, if more than one

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aren't we already dropping extraneous named file descriptors?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, updated comment

Comment thread systemd/fdstore/fdstore.go Outdated
Comment on lines +244 to +246
} else if len(fds) > 1 {
return nil, fmt.Errorf("%s: found more than one matching file descriptors", errPrefix)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this already prevented in initFdstore?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I added the check just in case. I will drop it as it can never happen

Comment thread systemd/fdstore/fdstore.go
Comment thread systemd/fdstore/fdstore.go Outdated
Comment on lines +292 to +296
duplicatedFd, err := unixDup(int(f.Fd()))
if err != nil {
return err
}
unixCloseOnExec(duplicatedFd)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this and the other instance of the same code in Get() could be moved to a helper?

Comment thread systemd/fdstore/fdstore.go Outdated
if err != nil {
return nil, err
}
unixCloseOnExec(duplicatedFd)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or rather check the error

Comment thread systemd/fdstore/fdstore.go
@ZeyadYasser ZeyadYasser modified the milestones: 2.76, 2.77 May 20, 2026
Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>
@ZeyadYasser
ZeyadYasser requested a review from bboozzoo May 20, 2026 15:05

@pedronis pedronis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some small comments

Comment thread systemd/fdstore/fdstore.go
Comment thread systemd/fdstore/fdstore.go Outdated
"golang.org/x/sys/unix"
)

const sd_LISTEN_FDS_START = 3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should have a doc comment, also maybe it should be moved closer to where it is used

Comment thread systemd/fdstore/fdstore.go
for i := 0; i < nfds; i++ {
fd := sd_LISTEN_FDS_START + i
name := FdName(names[i])
fdstore[name] = append(fdstore[name], os.NewFile(uintptr(fd), string(name)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should have a comment already here about support many for the sockets case

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread systemd/fdstore/fdstore.go
Comment thread systemd/fdstore/fdstore.go
// assigned by setting `FileDescriptorName=` on the socket unit.
//
// `FileDescriptorName=` was added in systemd version 227.
for name, fds := range fdstore {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side observation, but the order in which listeners are returned isn't deterministic. I don't think it's an issue though, but maybe worth a doc comment.

Comment thread systemd/fdstore/fdstore.go Outdated
// remove file descriptors from systemd given their name.
//
// Caller must hold the fdstore lock.
func remove(name FdName) (err error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func remove(name FdName) (err error) {
func remove(name FdName) error {

Comment thread systemd/fdstore/fdstore.go Outdated
//
// Maintains a copy of the underlying file descriptor internally. It
// is the caller's responsibility to close f when finished.
func Add(name FdName, f *os.File) (retErr error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func Add(name FdName, f *os.File) (retErr error) {
func Add(name FdName, f *os.File) error {

Comment thread systemd/fdstore/fdstore.go Outdated
// returned.
//
// It is the caller's responsibility to close returned listeners when finished.
func ActivationListeners() (listeners []net.Listener, retErr error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func ActivationListeners() (listeners []net.Listener, retErr error) {
func ActivationListeners() (listeners []net.Listener, err error) {

@ZeyadYasser
ZeyadYasser requested review from bboozzoo and pedronis June 1, 2026 15:14

@pedronis pedronis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you, couple more comments

Comment thread systemd/fdstore/fdstore.go
Comment thread systemd/fdstore/fdstore.go Outdated

state := fmt.Sprintf("FDSTORE=1\nFDNAME=%s", name)
if err := sdNotifyWithFds(state, duplicatedFile); err != nil {
duplicatedFile.Close() // clean up the duplicated fd

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this use osFileClose and be tested more directly?

Comment thread systemd/fdstore/fdstore.go Outdated
)

// Note: os.File is used to wrap raw fds so that the
// underlying fds are impicitly closed by finalizer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// underlying fds are impicitly closed by finalizer
// underlying fds are implicitly closed by finalizer

Comment thread systemd/fdstore/fdstore.go
Comment thread systemd/fdstore/fdstore.go
@ZeyadYasser
ZeyadYasser requested review from bboozzoo and pedronis June 3, 2026 07:23

@pedronis pedronis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks

Comment thread systemd/fdstore/fdstore.go
Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>
@ZeyadYasser
ZeyadYasser requested a review from bboozzoo June 5, 2026 10:14

@bboozzoo bboozzoo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@ZeyadYasser ZeyadYasser added Auto rerun spread Auto reruns spread up to 4 times in non-draft PRs w/ >=1 approval and <20 fails in any fund. system and removed Needs Samuele review Needs a review from Samuele before it can land labels Jun 8, 2026
@ZeyadYasser ZeyadYasser closed this Jun 8, 2026
@ZeyadYasser ZeyadYasser reopened this Jun 8, 2026
@ZeyadYasser
ZeyadYasser merged commit f70a425 into canonical:master Jun 9, 2026
568 of 618 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Auto rerun spread Auto reruns spread up to 4 times in non-draft PRs w/ >=1 approval and <20 fails in any fund. system Run nested The PR also runs tests inluded in nested suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants