Commit Graph
90627 Commits
Author SHA1 Message Date
Lennart Poettering 64a3846ced sysupdate: Add ListFeatures() and ListTargets() varlink methods (#42900)
Following on from adding the basic varlink scaffolding to sysupdate,
let’s varlinkify a couple of the D-Bus methods. Because varlink doesn’t
have a concept of object paths, the D-Bus path structure which allows a
target to be selected has been squashed down to a target argument for
each relevant method.

Varlinkify the way to list targets, and also the way to list features
because that was simple to do at the same time.

More methods need varlinkifying in the future, but let’s do it in small
and manageable chunks.
2026-07-14 23:14:22 +02:00
lzwind 2dcdbae4dd timedatectl: display RTCTimeUSec in UTC format
Fixes #39930

The RTCTimeUSec property was being displayed in local time format
when using 'timedatectl show', while 'timedatectl status' correctly
displayed it in UTC format. This inconsistency was due to the
bus_print_all_properties() function using TIMESTAMP_PRETTY style
for all timestamps, which formats in local time.

This fix adds special handling for RTCTimeUSec to use TIMESTAMP_UTC
style, ensuring consistent UTC display across both commands.
2026-07-14 18:29:21 +01:00
Yu Watanabe 01e1303f9c Rename basic-forward.h, sd-forward.h, and shared-forward.h to forward.h
Follow-up for 74d392ed1b.
2026-07-15 02:18:05 +09:00
Zbigniew Jędrzejewski-Szmek 0e6fe7f51f network: do not use assert() on a call with side effects
Fixes bf943a9d49.
2026-07-14 19:05:48 +02:00
Philip Withnall 75a247d4de sysupdate: Allow multiple documentation URLs for a feature
Change the varlink API for Feature structs to allow multiple
documentation URLs for them, to match what systemd already does for
units etc.

This is a deviation from what the sysupdated D-Bus API allows and, for
the moment, from what’s supported internally by sysupdate. Internally it
continues to support 0-1 URLs for now.

But by defining the API as a strv, multiple URLs can be supported in
future without API breaks.
2026-07-14 15:03:35 +01:00
Philip Withnall a8a88fc3ba sysupdate: Run ListFeatures in offline mode
Historically, the ListFeatures API (in both D-Bus and now varlink) was
run without an `--offline` flag. This appears like it’s an oversight, but
actually `verb_features()` always unconditionally loaded in offline
mode.

In any case, there doesn’t appear to be a reason for the context to be
online (i.e. for it to check sources for available updates). Features are
defined in local config files and are loaded by `read_features()`, which
is called in both offline and online mode.

When the varlink ListFeatures API was added, it was put into online mode
in order to match the lack of `--offline` argument in the existing D-Bus
API implementation. Change both of them to be explicitly in offline mode.

Signed-off-by: Philip Withnall <pwithnall@gnome.org>
2026-07-14 14:58:02 +01:00
Philip Withnall f6cd126137 test: Remove a redundant exit call
`[[ blah ]] || exit 1` is equivalent to `[[ blah ]]`.

Fixes: b0ca987cd9
2026-07-14 14:57:39 +01:00
Philip Withnall 8c166bc35e sysupdate: Downgrade an info to a debug log message
Since we now enumerate all targets on a varlink call (to validate the
requested target), this message gets printed multiple times in the log.
It’s not really necessary, so downgrade it to a debug message.
2026-07-14 14:57:35 +01:00
Philip Withnall 8cb7531390 sysupdate: Add varlink ListTargets() method
And add integration tests for it using `jq`.
2026-07-14 14:57:31 +01:00
Philip Withnall 3e34ccf6df sysupdate: Add varlink ListFeatures() method
And add integration tests for it using `jq`.
2026-07-14 14:57:13 +01:00
Philip Withnall 8085a05f86 sysupdate: Factor out core of features verb
This will be used in the following commit to add a varlink interface for
it.

This introduces no functional changes.
2026-07-14 14:57:00 +01:00
CrtlTom 9ac37aa0c7 man: update gpt-auto-generator ESP mounting behavior
Remove the stipulation that /boot/ must exist for the ESP to be mounted
there to reflect the change in #34550.
2026-07-14 14:05:57 +02:00
Yu Watanabe 944e1d3d81 meson: speed up building standalone binaries
Previously, files listed in 'sources' were built twice:
once when building the main binary, and again when building the
statically linked one.

This change ensures that all object files from the main binary are
reused when building the static binary. Hence, the only step now
necessary for the static binary is linking the object files.

Follow-up for 39d00e1d20.
2026-07-14 13:51:03 +02:00
Jonas Dreßler 04e3d8933b repart: Properly pre-calculate auto size of images
When passing --size=auto to repart, it will pre-calculate the image size and
resize the image to that size before partitioning. Currently, that fails when
passing a large grain size, complaining that the auto-sized image is too small
to fit the data.

The reason for this is that the current code simply assumes the GPT metadata
size taken away from the usable size by fdisk is static (1044KiB), when it
actually is more complicated than that:

There's two ranges of GPT metadata: One at the beginning of the image, and one
at the end of the image. And there's the first usable block that is defined by
fdisk when creating the partition table.

The static value of 1044KiB usually works, because fdisk sets the first usable
block to 1MiB (so 1024KiB), leaving 20KiB of leeway for the secondary GPT at
the end of the image.

Now as soon as the first partition starts at an offset higher than 1024KiB, we
lose the 20KiB leeway for the secondary GPT, and the partitions will no longer
fit.

What we should do, is first of all round up to the grain size instead of 4096
(as that's the minimum offset our first partition will start at), and second of
all properly subtract the secondary GPT at the end.

Also confirm we don't regress on this anymore by adding a test that uses a 2MiB
grain size, breaking the old code.
2026-07-14 12:46:50 +01:00
Luca Boccassi 321a321787 Assorted resolved hardening fixes flagged by kres (#43011) 2026-07-14 12:24:01 +01:00
Philip Withnall f8f92584db sysupdate: Improve empty table when printing features
Rather than just printing out the table header and then exiting, print
“No features” similarly to what `loginctl` or `storagectl` do.
2026-07-14 12:09:23 +01:00
Luca Boccassi bd7c113e24 boot: honour "read-only" vfat flag on random seed file (#43012)
Inspired by #42979
2026-07-14 11:51:43 +01:00
Luca Boccassi a5ac81ae4c systemd-run: reject unsupported option combinations (#43003)
Boundary tests were conducted on the "run" command tool, and the
identified issues were resolved.
2026-07-14 11:29:11 +01:00
Lennart Poettering 89025e8f86 repart: Fix growing the partition preceding a FreeArea from leftover space (#42969)
The "Donate to preceding partition" logic is dead code since commit

https://github.com/jonas2515/systemd/commit/19903a433507897449c086b72abb5e133e431336
("repart: split out context_grow_partition_one()").
context_grow_partition_one() gets passed a free area and a partition,
and it has
an early-return check to ensure the partition it got passed belongs to
the free
area it got passed. That means we compare the FreeArea a to the FreeArea
a->after->allocated_to_area, which always yields FALSE.

Fix the behavior of donating any left over space to the preceding
partition
by adding that partition to the loop below (and relying on the
partitions list
being ordered according to physical partition offsets).

Since this behavior is not that easy to trigger, mention how to trigger
it in a
comment, and add a test for it as well.
2026-07-14 11:27:57 +02:00
dongshengyuan 63b30468a5 portablectl: retry inspect with PORTABLE_PREFIXES
When no prefix is specified, portablectl inspect first tries the
prefix derived from the image name. This keeps inspect aligned with
attach behavior.

If that lookup finds no matching units, retry with validated
PORTABLE_PREFIXES read from the image os-release. This makes inspect
work for images whose filename does not match their portable service
prefix.

Keep metadata error handling explicit so request-construction failures
are not logged twice, while sd_bus_call() failures still include the
inspect context.

Add a TEST-29-PORTABLE regression case for a directory image whose
name does not match the portable service prefix.

Fixes #37296.

Signed-off-by: dongshengyuan <dongshengyuan@uniontech.com>
2026-07-14 11:26:40 +02:00
dongshengyuan 34649e3d66 sysext: honor confext config during notify refresh
systemd-sysext handles the sysupdate notification hook,
but the hook refreshes both sysexts and confexts.

Build the refresh context for each image class so confext settings,
such as Mutable=yes, are applied when confexts are refreshed.

Add regression coverage for the mutable confext overlay.

Fixes #42873
Signed-off-by: dongshengyuan <dongshengyuan@uniontech.com>
2026-07-14 11:26:10 +02:00
Luca Boccassi e509a6a4cb dns-answer: preserve shared aliases when removing records
dns_answer_remove_by_rr() mutats the backing OrderedSet even when the answer
has multiple references. Callers such as trust-anchor revocation could thus
change snapshots held elsewhere in the resolver.

Clone a shared answer before removal.

Follow-up for 71aee23dba
2026-07-14 10:09:31 +01:00
Luca Boccassi 3734b69f03 resolved: roll back partial DNS zone publication
dns_zone_link_item() inserts a new item into the by-key hashmap before
publishing it by name. If the second hashmap insertion fails, _cleanup_
frees the item while by-key retains a dangling key and value.

Follow-up for 623a4c97b9
2026-07-14 10:09:31 +01:00
hanjinpeng 0e7033215d man: clarify that --when= is a lower bound, not a condition
`systemctl reboot --when=yesterday` reboots the machine immediately, which
surprised users enough to be reported as a bug. It is not one: the timestamp
passed to --when= (and to ScheduleShutdown(), and to shutdown(8)) declares the
earliest point in time the action may be taken, it is not a condition that is
evaluated and that could fail.

Behaving any differently would be racy and surprising: "--when=now" refers to
the past by the time the request is processed, and "--when=+50ms" may well have
elapsed already due to scheduling latencies. In both cases we must still carry
out the action the user asked for.

Document the semantics explicitly in systemctl(1), shutdown(8) and the
org.freedesktop.login1(5) D-Bus interface documentation.

Fixes: #42437
2026-07-14 09:43:12 +01:00
Lennart Poettering de39ad2cfe boot: skip boot counter logic for entries marked read-only
If the read-only FAT file attribute is set on a boot entry file with a
counter in its name, don't attempt to rename it, but simply skip the
boot counter logic for it, taking the flag as a hint that the entry
shall not be subject to boot assessment.
2026-07-14 10:13:17 +02:00
Lennart Poettering 38f63d28c2 boot-secret: don't initialize secret mixin file if marked read-only
If the boot secret mixin file exists but is empty we'd normally fill it
with a fresh mixin. Refrain from that if the read-only FAT file
attribute is set on it, taking that as a hint that the file shall not
be initialized. Reading an existing, fully populated mixin file remains
unaffected by the flag.
2026-07-14 10:12:50 +02:00
Lennart Poettering 78f81f02e8 boot: skip random seed handling if seed file is marked read-only
If the read-only FAT file attribute is set on /loader/random-seed,
don't update the seed file — and hence don't use it either, since a
seed we cannot update would be the same on every boot.

This gives users an explicit way to turn off random seed handling by
marking the file read-only, useful for example in pre-built OS images
that are replicated to many systems, where the baked-in seed is shared
and hence must not be credited.

The check is done upfront in process_random_seed(), before any other
work, mirroring the existing check for read-only volumes. This covers
both systemd-boot and systemd-stub, which share this code.

Inspired-by: #42979
2026-07-14 10:12:50 +02:00
dongshengyuan 06247b0804 run: reject waiting for remain-after-exit services
Reproducer:
  unit=run-wait-rae-$(date +%s)
  sudo timeout 3s systemd-run --wait --remain-after-exit \
      --unit="$unit" /bin/true
  echo $?
  systemctl is-active "$unit.service"

Before, the command timed out with exit status 124 while the service
stayed active. --wait waits for deactivation, but RemainAfterExit=yes
keeps the service active after the command exits.

Follow-up for 2a453c2ee3
2026-07-14 12:03:22 +08:00
dongshengyuan 7e76e93142 run: reject JSON output with verbose logs
Reproducer:
  sudo systemd-run --wait --verbose --json=short /bin/echo hi

Before, systemd-run printed JSON metadata to stdout while --verbose
also spawned journalctl output on stdout. The resulting stream mixed JSON
with journal lines, so reject the conflicting options.

Follow-up for 744ca8f616
2026-07-14 12:03:22 +08:00
dongshengyuan 2a4750c3e1 run: reject JSON output in scope mode
Reproducer:
  sudo systemd-run --scope --json=short /bin/echo hi

Before, systemd-run printed JSON metadata to stdout and then executed
the scope command on the same stdout. The combined stream was not valid
JSON, so reject --json= in scope mode.

Follow-up for fe5a6c47af
2026-07-14 12:03:22 +08:00
dongshengyuan 3fa641d01f run: reject JSON output for trigger units
Reproducer:
  unit=run-json-trigger-$(date +%s)
  sudo systemd-run --json=short --unit="$unit" \
      --on-active=30s \
      /bin/true

Before, trigger mode accepted --json=short but printed only human-readable
"Running timer as unit" and "Will run service" lines. Reject the option
until trigger mode has structured output.

Follow-up for fe5a6c47af
2026-07-14 12:03:22 +08:00
dongshengyuan ecf38b0e22 run: reject JSON output with stdio forwarding
Reproducer:
  sudo systemd-run --wait --pipe --json=short /bin/echo hi

Before, systemd-run wrote its JSON metadata to stdout and then passed
the command stdout through the same stream. The combined output was not
valid machine-readable JSON, so reject the conflicting modes.

Follow-up for fe5a6c47af
2026-07-14 12:03:22 +08:00
dongshengyuan df3a08b244 run: reset groups before scope uid switch
Reproducer:
  sudo systemd-run --scope --uid=nobody /usr/bin/id

Before, the command ran as nobody but kept the caller supplementary
root group, for example groups=65534(nogroup),0(root). Scope mode
performs the uid/gid switch locally, so initialize the target user groups
before dropping privileges.

Follow-up for 4de33e7f32
2026-07-14 12:03:22 +08:00
dongshengyuan eeb8ef0366 run: accept explicit trigger unit names
Reproducer:
  unit=run-explicit-path-$(date +%s)
  systemd-run --unit="$unit.path" \
      --path-property=PathExists=/tmp \
      /bin/true
  echo $?

Before, an explicit .path unit name was not recognized as the
trigger unit. It was mangled again as a service name, so PID 1 rejected
the transient request with an already-loaded unit conflict.

Follow-up for d59ef3e243
2026-07-14 12:03:22 +08:00
dongshengyuan dd6c127d6b run: reject --ignore-failure in scope mode
Reproducer:
  sudo systemd-run --scope --ignore-failure /bin/false
  echo $?

Before, the option was accepted but had no effect because scope mode
executes the command locally after creating the scope. The flag is only
encoded into service ExecStart properties, so accept it only where it can
be applied.

Follow-up for 1072d94731
2026-07-14 12:03:22 +08:00
dongshengyuan d8f86dff81 run: honor --no-block for trigger units
Reproducer:
  unit=run-nb-trigger-$(date +%s)
  systemd-run --no-block --collect --unit="$unit" \
      --socket-property=ListenStream=/proc/systemd-run-repro/socket \
      /usr/bin/true
  echo $?

Before, systemd-run still waited for the trigger unit job and
propagated the socket start failure. With --no-block it should only
verify and enqueue the request, as the service path already does.

Follow-up for 3d161f991e
2026-07-14 12:03:22 +08:00
Luca Boccassi 4e67935b77 userdb: suppress userdb queries for backends indicating uid/gid/name range info via xattrs on entrypoint sockets (#42961)
Let's optimize userdb queries a bit: by encoding the covered UID/GID
ranges and user/group name patterns on the varlink entrypoint sockets
for userdb backends we can make them wake up less and reduce the work
triggered by queries.
2026-07-14 00:19:32 +01:00
Luca Boccassi 1fcd92a214 sysupdate: add "suggestion" concept to feature and component enablement (#42970) 2026-07-13 19:07:50 +01:00
Lennart Poettering 6ce0759d99 repart: allow empty EncryptedVolume= volume name (#42889)
Treat an empty volume name alongside other fields as unset instead of
rejecting it as invalid.

Example use case:
```
EncryptedVolume=:none:discard
```

In this case, the volume name is not specified so it can be generated as
luks-UUID.

From the docs:

> EncryptedVolume=
> Specifies how the encrypted partition should be set up. Takes at least
one and at most four fields separated with a colon (":"). The first
field specifies the encrypted volume name under /dev/mapper/. If not
specified, "luks-UUID" will be used where "UUID" is the LUKS UUID.
2026-07-13 18:47:09 +02:00
Jonas Dreßler d3ebe511b8 repart: Make use of partitions list being ordered by offsets while looping
Let's do the same thing we did above with the last commit here too and make use
of the partitions list being ordered by physical offsets. This makes the code a
little simpler.
2026-07-13 18:24:27 +02:00
Jonas Dreßler 0ea9bdfec8 repart: Fix growing the partition preceding a FreeArea from leftover space
The "Donate to preceding partition" logic is dead code since commit
19903a4 ("repart: split out context_grow_partition_one()").
context_grow_partition_one() gets passed a free area and a partition, and it has
an early-return check to ensure the partition it got passed belongs to the free
area it got passed. That means we compare the FreeArea a to the FreeArea
a->after->allocated_to_area, which always yields FALSE.

Fix the behavior of donating any left over space to the preceding partition
by adding that partition to the loop below (and relying on the partitions list
being ordered according to physical partition offsets).

Since this behavior is not that easy to trigger, mention how to trigger it in a
comment, and add a test for it as well.
2026-07-13 18:24:25 +02:00
Luca Boccassi 4f397da505 process-util: introduce prctl_safe() and port everything over to it (#43006)
prctl() is an API full of pitfalls: it is variadic, and some interfaces
don't expect zero-initialization of excess arguments, and others do.
Moreover, the parameters are "long", and nonetheless we usually pass
"int" to them. If we are too dumb to call it properly, let's just not
call it directly anymore, but let's add a wrapper around it that makes
the function non-variadic and declares the right types. Then, let's port
over everything to it.

This is inspired by #42996, but we had issues with this many times,
before and looking at this PR one can see that we otherwise still are
having the issue at numerous other places.
2026-07-13 17:02:57 +01:00
Lennart Poettering c8cffba715 cryptsetup: add Argon2id-based PIN mode for TPM2 enrollment (#41859)
The current TPM2 PIN mode is flawed as a compromised TPM directly
exposes
the sealed secret which is the LUKS volume key itself
(https://github.com/systemd/systemd/pull/27502 and
https://github.com/systemd/systemd/issues/37386).

Goal: add Argon2id-based PIN hardening to TPM2 enrollment, making
the TPM a second factor rather than a single point of failure:

1. Password + salt → Argon2id → 512-bit key split into Key1 + Key2
2. Key2 (base64-encoded) is used as the PIN to seal a random secret
in the TPM
3. Key1 + unsealed secret → HKDF-SHA256 → final LUKS volume key

This implementation ensures that if the TPM is compromised, an attacker
still needs the password to derive Key1 and combine it with the unsealed
secret.

The --tpm2-with-pin= option now accepts three values:
- false (no PIN used)
- true (PIN hardened with Argon2id - default)
- "direct" (legacy PIN without Argon2id for backward compatibility)

Argon2id parameters are customizable via:

--tpm2-argon2id-memory=
--tpm2-argon2id-iterations=
--tpm2-argon2id-parallelism=
--tpm2-argon2id-iter-time=

These default to a function of available CPUs and physical memory, with
a benchmark that scales iterations to the target time (default: 2s) and
falls back to ARGON2ID_PARAMETERS_DEFAULT (64 MiB, 8 iter, 4 lanes) when
auto detection fails.
Also if the runtime OpenSSL lacks Argon2id support (< 3.2), the feature
silently falls back to direct PIN mode with a warning.

Added includes:
- src/cryptenroll/cryptenroll.c: cpu-set-util.h, limits-util.h,
time-util.h
for Argon2id benchmark auto-tuning (cpus_online, physical_memory_scale,
  now/usec_t)
- src/cryptenroll/cryptenroll-tpm2.c: crypto-util.h for
Argon2IdParameters
  struct in load_volume_key_tpm2()
- src/shared/tpm2-util.h: crypto-util.h for Argon2IdParameters in
  tpm2_make_luks2_json() API
- src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c: crypto-util.h for
  kdf_argon2id_derive()/kdf_hkdf_sha256() on the token unlock path
2026-07-13 17:55:47 +02:00
Lennart Poettering 4481661a75 sysupdate: add support for --component-all + --feature-all + --feature-suggested to enable-feature verb
This adds similar concepts as we already have for enable-component:
let's add a way to enable all features or the suggeste dones, and
possibly on all components.

Or in other words, with this:

  systemd-sysupdate enable-component -S
  systemd-sysupdate enable-feature -A -s

We'll automatically enable all suggested components, and all features of
them.
2026-07-13 17:36:11 +02:00
Lennart Poettering 9f3b604d64 sysupdate: validate feature name syntax before using it
Let's tighten the rules on features, and enforce a sensible naming
policy.
2026-07-13 17:36:10 +02:00
Lennart Poettering f23224a7ca sysupdate: add feature select
Prepartion for the next commit: also allow selecting the all or
suggested features.
2026-07-13 17:36:10 +02:00
Lennart Poettering 52d9906a54 sysupdate: implement --component-all and --component-suggested for enable-component and disable-component
Let's now introduce "systemd-sysupdate enable-component
--component-suggested" and systemd-sysupdate enable-component
--component-all" for enabling all or all suggested components at once.
Similar, if used for disable-component will disable all components or
those not suggested.
2026-07-13 17:36:10 +02:00
Lennart Poettering 8f9a2f0c82 sysupdate: introduce --component-suggested similar to --component-all
While --component-all really picks all components --component-suggested
picks only the suggested ones.

Note that none of the verbs currently implement the concept, they will
all refuse the option. Hooking this up is going to be added next.
2026-07-13 17:36:10 +02:00
Lennart Poettering 40e727c93b sysupdate: add a "suggests" concept to features and components
Let's make it possible to "suggest" that certain features or components
are enabled under some conditions.

For this, both features and components gain two things:

1. A Suggested= field which takes a boolean. If true the
   feature/component will be suggested for installation, if false it
   will not.

2. A set of SuggestedOnXYZ= settings are modelled after ConditionXYZ= in
   unit files (and implement a subset of them), will suggest some
   component/feature under specific conditions.

The result of the condition is shown in the various output tools.
2026-07-13 17:36:10 +02:00
Frantisek Sumsal 5f2d15abf4 timer: avoid re-arming WakeSystem=yes timer after suspend
There's an edge case where a single-shot timer using WakeSystem=yes
could get re-armed after elapsing. For example, when a timer is created
using:

$ systemd-run --user --on-active="1m" --timer-property=WakeSystem=yes flatpak run io.bassi.Amberol

and the system is then suspended, following sequence of events may
happen:

Jul 08 06:57:25 systemd[2640]: run-p192456-i199160.timer: Installed new job run-p192456-i199160.timer/start as 7675
Jul 08 06:57:25 systemd[2640]: run-p192456-i199160.timer: Enqueued job run-p192456-i199160.timer/start as 7675
Jul 08 06:57:25 systemd[2640]: run-p192456-i199160.timer: Monotonic timer elapses in 59.999999s.
Jul 08 06:57:25 systemd[2640]: run-p192456-i199160.timer: Changed dead -> waiting
Jul 08 06:57:25 systemd[2640]: run-p192456-i199160.timer: Job 7675 run-p192456-i199160.timer/start finished, result=done
Jul 08 06:57:25 systemd[2640]: Started [systemd-run] /usr/bin/flatpak run io.bassi.Amberol.
Jul 08 06:58:13 systemd[2640]: run-p192456-i199160.timer: Time change, recalculating next elapse.
Jul 08 06:58:13 systemd[2640]: run-p192456-i199160.timer: Monotonic timer elapses in 12.785674s.
Jul 08 06:58:26 systemd[2640]: run-p192456-i199160.timer: Timer elapsed.
Jul 08 06:58:26 systemd[2640]: run-p192456-i199160.timer: Changed waiting -> running
Jul 08 06:58:29 systemd[2640]: run-p192456-i199160.timer: Got notified about unit deactivation.
Jul 08 06:58:29 systemd[2640]: run-p192456-i199160.timer: Monotonic timer elapses in 33.544681s.
Jul 08 06:58:29 systemd[2640]: run-p192456-i199160.timer: Changed running -> waiting
Jul 08 06:59:48 systemd[2640]: run-p192456-i199160.timer: Timer elapsed.

1) The timer is armed at 06:57:25. timer_enter_waiting()
   calculates following values:

   base = M0 (current CLOCK_MONOTONIC)
   usec_shift_clock(base, CLOCK_MONOTONIC, CLOCK_BOOTTIME_ALARM) = M0 (no delta yet)
   v->next_elapse = M0 + 60s

   The timer is on CLOCK_BOOTTIME_ALARM at M0 + 60s

2) System suspends. If the system ran for ~12s and was suspended for
   ~36s, at resume we'd get:

   CLOCK_MONOTONIC = M0 + 12s
   CLOCK_BOOTTIME = M0 + 12s + 36s = M0 + 48s

3) timer_time_change() fires and calls timer_enter_waiting(t, true)

   Because time_change=true, v->next_elapse is not recalculated and
   keeps the original M0 + 60s value. The timer then correctly computes
   the remaining time as ~12.8 seconds:

        Jul 08 06:58:13 ...: Monotonic timer elapses in 12.785674s.

4) Timer elapses at 06:58:26

        Jul 08 06:58:26 ...: Timer elapsed.

   timer_enter_running() sets last_trigger.monotonic to CLOCK_MONOTONIC
   (which equals to M0 + 12s before suspend + 13s after suspend, thus
   + 25s)

5) Unit deactivates at 06:58:29

   timer_trigger_notify() calls timer_enter_waiting(t, false) -
   time_change=false; that means that this time v->next_elapse gets
   recalculated:

   base = inactive_exit_timestamp.monotonic = M0 (i.e. when the timer was originally armed)
   usec_shift_clock(M0, CLOCK_MONOTONIC, CLOCK_BOOTTIME_ALARM)
        a = now(CLOCK_MONOTONIC) = M0 + 28s
        b = now(CLOCK_BOOTTIME_ALARM) = M0 + 64s
        result = b - (a - M0) = (M0 + 64s) - 28s = M0 + 36s => the time spent in suspend

   Hence v->next_elapse = (M0 + 36s) + 60s = M0 + 96s

   This then skips disabling of one-shot timers in the following check,
   because the expression

        v->next_elapse < triple_timestamp_by_clock(&ts, TIMER_MONOTONIC_CLOCK(t))

   is false, because the v->next_elapse (M0 + 96s) is not less than the
   current time (M0 + 64s), so the timer is re-armed again:

        Jul 08 06:58:29 ...: Monotonic timer elapses in 33.544681s

Let's mitigate this by skipping the next elapse timestamp recalculations
for one-shot timers for which we've already calculated the value in this
activation cycle.

Resolves: #42929
2026-07-13 16:23:59 +01:00