From: Andreas Hindborg <a.hindborg@kernel.org>
To: FUJITA Tomonori <tomo@flapping.org>
Cc: gary@garyguo.net, tomo@flapping.org, ojeda@kernel.org,
acourbot@nvidia.com, aliceryhl@google.com,
anna-maria@linutronix.de, bjorn3_gh@protonmail.com,
boqun@kernel.org, dakr@kernel.org, daniel.almeida@collabora.com,
frederic@kernel.org, jstultz@google.com, lossin@kernel.org,
lyude@redhat.com, sboyd@kernel.org, tamird@kernel.org,
tglx@kernel.org, tmgross@umich.edu, work@onurozkan.dev,
rust-for-linux@vger.kernel.org, fujita.tomonori@gmail.com
Subject: Re: [PATCH 0/4] Fix forward()/expires() racing with concurrent arming
Date: Tue, 18 Aug 2026 11:02:27 +0200 [thread overview]
Message-ID: <8733wbajj0.fsf@t14s.mail-host-address-is-not-set> (raw)
In-Reply-To: <20260818.112656.263099326344775009.tomo@flapping.org>
FUJITA Tomonori <tomo@flapping.org> writes:
> On Mon, 17 Aug 2026 17:26:41 +0200
> Andreas Hindborg <a.hindborg@kernel.org> wrote:
>
>> "Gary Guo" <gary@garyguo.net> writes:
>>
>>> On Thu Aug 13, 2026 at 2:48 PM BST, FUJITA Tomonori wrote:
>>>> From: FUJITA Tomonori <fujita.tomonori@gmail.com>
>>>>
>>>> This series started from the review of patches 3 and 4 [1]: a hrtimer
>>>> can be armed from any CPU at any time, including while its callback
>>>> runs, so restricting HrTimer::expires() to the callback context is not
>>>> by itself enough to remove the race.
>>>>
>>>> It turned out that expires() is not the only problem. A callback may
>>>> also change its expiry time with hrtimer_forward(), which is sound
>>>> only because __run_hrtimer() dequeues the timer for the duration of
>>>> the callback. Arming the same timer from another CPU puts it back into
>>>> the rbtree while the callback runs, so hrtimer_forward() then changes
>>>> the expiry of a timer that is queued, without the base lock and
>>>> without re-checking the ordering, which leaves the tree unsorted.
>>>>
>>>> Two of the four pointer types cannot construct that
>>>> situation. Starting a Pin<Box<T, A>> moves the box into the handle,
>>>> and starting a Pin<&mut T> consumes the exclusive borrow, so in both
>>>> cases nothing is left to arm the timer with. Arc<T> is Clone and
>>>> Pin<&T> is Copy, and both of their start functions are reachable from
>>>> safe code, so safe Rust could arm a timer whose callback was running.
>>>>
>>>> "No arming while the callback runs" cannot be expressed in the type
>>>> system, because the callback begins when the timer expires rather than
>>>> at any point in the Rust program, so patches 1 and 2 use the stronger
>>>> "no arming while armed" instead. hrtimer_cancel() waits for the
>>>> handler to return, which makes that the point where the right to arm
>>>> can be handed back. The right to arm is split out of Arc<T> into
>>>> HrTimerArc<T> and out of Pin<&T> into HrTimerPin<'a, T>, both
>>>> non-clonable and consumed by start, modelled on ListArc; the object
>>>> itself stays shareable through plain Arc references and shared pinned
>>>> references respectively.
>>>>
>>>> Patches 3 and 4 are the previously posted expires() and
>>>> repr(transparent) patches, unchanged. With patches 1 and 2 in place,
>>>> the callback context has no concurrent writer of node.expires. So
>>>> HrTimerCallbackContext::expires() is sound.
>>>
>>> I am thinking about this and I wonder about a different approach: the only
>>> reason that we're having this issue, is that `expires()` call and
>>> `forward`/`forward_now` is executed outside the protection of the base lock.
>>>
>>> The fix is easy -- to ensure that they are executed with the base lock held.
>>> The callback wants either:
>>> * Do not restart the timer
>>> * Call hrtimer_forward[_now] and restart the timer
>>>
>>> So, if we change the order from
>>>
>>> unlock base
>>> restart = fn(timer)
>>> lock base
>>> if restart {
>>> queue
>>> }
>>>
>>> to
>>>
>>> get expires
>>> unlock base
>>> restart = fn(timer, expires)
>>> lock base
>>> match restart {
>>> Restart(now, interval) => {
>>> hrtimer_forward(timer, now, interval);
>>> queue
>>> }
>>> NoRestart => (),
>>> }
>>>
>>> then we completely eradicate this issue.
>>>
>>> Alternatively, we can add another spinlock to protect `expires` from race
>>> condition from within callback and concurrent restart -- that is what perf core
>>> does: perf_mux_hrtimer_handler and perf_mux_hrtimer_restart uses the same
>>> hrtimer_lock to prevent race.
>>
>> With this solution we would have to restrict calls to `forward` and
>> `expires`. Maybe that would be OK, but it would be restricting the API
>> further.
>>
>> As I understand the problem space, we have (on Rust side):
>>
>> - `start` and `forward` may race. `forward` is callable on exclusive
>> reference to HrTimer or in callback context, but otherwise lacks
>> synchronization. `start` is serialized on the base lock but is
>> callable at any time.
>> - `start` and `expires` may race because `start` writes the expiration and
>> `expires` reads it. The latter has no synchronization and is callable
>> on shared reference to `HrTimer`.
>> - `forward` and `expires` may race because `HrTimer::expires` takes a
>> shared reference and is callable at any time concurrently.
>>
>> I think the solution suggested by Tomo is OK, but we could also add
>> synchronization to `start`, `forward` and `expires` on the rust side.
>> Would that not solve the problem for us?
>>
>> This way we can still run the handler without lock. Only if we call
>> `forward` or read the expiry in the handler would we take the lock.
>>
>> This would allow the API as originally described on the rust side.
>
> That would work, but I think it needs more than the lock. The lock makes
> start and forward safe against each other, but one of them still loses. If
> start runs first, hrtimer_forward() returns 0 and does nothing, so the
> overrun it would have returned is lost. Some callers use that return
> value.
We can put the lock on the rust side of things. Existing C callers would
not be affected. Calling `forward` from the handler and racing a `start`
from outside needs handling anyway. The zero return value would be an
indicator.
> perf and CFS bandwidth have a flag as well as a lock. The flag is "do
> not arm while armed", which is the same rule the types enforce
> here. rtc and the softlockup watchdog look like they cancel first and
> then start instead. None of them arms a timer that is active, so I
> would rather the abstraction did not allow it either. Does that seem
> reasonable?
I am fine with preventing starting a timer that is Started or Running,
but I am not liking the `UniqueArc` requirement.
I have a use case in `rnull` where I have to start a timer behind an
`Arc` with no way to obtain a `UniqueArc`, so I would prefer if that use
case keeps on working. Without this, I would have to allocate a box and
put it behind a lock, leading to double indirection.
If we can fold in this logic into the API, callers can be simpler.
Best regards,
Andreas Hindborg
next prev parent reply other threads:[~2026-08-18 9:02 UTC|newest]
Thread overview: 28+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-13 13:48 [PATCH 0/4] Fix forward()/expires() racing with concurrent arming FUJITA Tomonori
2026-08-13 13:48 ` [PATCH v1 1/4] rust: hrtimer: Introduce HrTimerArc to make arming exclusive FUJITA Tomonori
2026-08-13 13:48 ` [PATCH v1 2/4] rust: hrtimer: Introduce HrTimerPin " FUJITA Tomonori
2026-08-13 13:48 ` [PATCH v1 3/4] rust: hrtimer: Restrict expires() to safe contexts FUJITA Tomonori
2026-08-13 13:48 ` [PATCH v1 4/4] rust: hrtimer: Make HrTimer repr(transparent) FUJITA Tomonori
2026-08-13 14:16 ` [PATCH 0/4] Fix forward()/expires() racing with concurrent arming Gary Guo
2026-08-13 23:47 ` FUJITA Tomonori
2026-08-14 0:54 ` Gary Guo
2026-08-14 13:48 ` FUJITA Tomonori
2026-08-14 14:24 ` Gary Guo
2026-08-18 0:56 ` FUJITA Tomonori
2026-08-17 15:40 ` Gary Guo
2026-08-18 1:35 ` FUJITA Tomonori
2026-08-17 15:26 ` Andreas Hindborg
2026-08-18 2:26 ` FUJITA Tomonori
2026-08-18 9:02 ` Andreas Hindborg [this message]
2026-08-18 11:21 ` FUJITA Tomonori
2026-08-18 11:59 ` Andreas Hindborg
2026-08-19 13:01 ` FUJITA Tomonori
2026-08-20 9:00 ` Andreas Hindborg
2026-08-20 9:21 ` FUJITA Tomonori
2026-08-20 12:34 ` Andreas Hindborg
2026-08-20 12:53 ` FUJITA Tomonori
2026-08-21 7:13 ` Andreas Hindborg
2026-08-21 9:53 ` FUJITA Tomonori
2026-08-24 10:44 ` Andreas Hindborg
2026-08-24 10:59 ` Miguel Ojeda
2026-08-24 11:07 ` FUJITA Tomonori
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=8733wbajj0.fsf@t14s.mail-host-address-is-not-set \
--to=a.hindborg@kernel.org \
--cc=acourbot@nvidia.com \
--cc=aliceryhl@google.com \
--cc=anna-maria@linutronix.de \
--cc=bjorn3_gh@protonmail.com \
--cc=boqun@kernel.org \
--cc=dakr@kernel.org \
--cc=daniel.almeida@collabora.com \
--cc=frederic@kernel.org \
--cc=fujita.tomonori@gmail.com \
--cc=gary@garyguo.net \
--cc=jstultz@google.com \
--cc=lossin@kernel.org \
--cc=lyude@redhat.com \
--cc=ojeda@kernel.org \
--cc=rust-for-linux@vger.kernel.org \
--cc=sboyd@kernel.org \
--cc=tamird@kernel.org \
--cc=tglx@kernel.org \
--cc=tmgross@umich.edu \
--cc=tomo@flapping.org \
--cc=work@onurozkan.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox