Rust for Linux List
 help / color / mirror / Atom feed
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



  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