From: Vinicius Costa Gomes <vinicius.gomes@intel.com>
To: Krystian Kaniewski <krystianmkaniewski@gmail.com>,
Jamal Hadi Salim <jhs@mojatatu.com>,
Jiri Pirko <jiri@resnulli.us>,
"David S . Miller" <davem@davemloft.net>,
Eric Dumazet <edumazet@google.com>,
Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>
Cc: Simon Horman <horms@kernel.org>,
Vladimir Oltean <vladimir.oltean@nxp.com>,
netdev@vger.kernel.org, linux-kernel@vger.kernel.org,
syzbot+e044a9b6370ed8ca9737@syzkaller.appspotmail.com
Subject: Re: [PATCH net v2 2/2] net/sched: taprio: do not replay missed entries in advance_sched()
Date: Mon, 12 Oct 2026 15:53:18 -0700 [thread overview]
Message-ID: <87ik36lee9.fsf@intel.com> (raw)
In-Reply-To: <20261009123309.284193-3-krystianmkaniewski@gmail.com>
Krystian Kaniewski <krystianmkaniewski@gmail.com> writes:
> advance_sched() advances the software schedule by one entry per hrtimer
> expiry and rearms the timer at the nominal end of that entry, without
> reading the clock. When that time has already passed, the hrtimer core
> runs the callback again inside the same interrupt, once for every missed
> entry.
>
> syzbot hits this with a single 127 ns entry on a veth device.
> fill_sched_entry() only rejects intervals below the transmission time of
> a minimum sized frame, which is 48 ns at the 10 Gb/s reported by veth,
> so 127 ns is accepted. One expiry takes longer than 127 ns, so the
> backlog grows instead of shrinking, the CPU stays in the timer interrupt
> and RCU reports a stall. Lateness from a forward step of CLOCK_REALTIME
> or CLOCK_TAI, from resume with CLOCK_BOOTTIME, or from a long time with
> interrupts disabled leads to the same replay with any interval.
>
> Record when the schedule starts and the period after which it repeats,
> which is the cycle time, or the sum of the intervals when the entries
> end before the cycle does. Whenever the entry that advance_sched() is
> about to start has already ended, find the entry in progress from these
> two values and continue from there. This selects the entry and the end
> time that the replay would have reached, so a schedule that keeps up
> with its timer behaves as before. The same check covers the first entry
> after taprio_change() resets the current entry of a running schedule,
> and the first entry of an admin schedule that takes over late.
>
> advance_sched() now evaluates should_change_schedules() once on the
> caught up end time instead of once per missed entry. That is only
> equivalent if the test is monotonic in the end time, so make the
> cycle_time_extension sum in that helper saturate instead of wrap. The
> extension comes from netlink as an unbounded signed value.
>
> This bounds the work done per expiry. It does not limit how often the
> timer fires. Unless the clock is stepped backward between the hrtimer
> core reading it and the callback reading it, the timer is rearmed after
> the current time and the CPU leaves the timer interrupt between
> expiries. On the high-resolution hard interrupt path, a schedule whose
> intervals are shorter than one expiry is then held back by the hang
> detection in hrtimer_interrupt().
>
> Fixes: 5a781ccbd19e ("tc: Add support for configuring the taprio scheduler")
> Reported-by: syzbot+e044a9b6370ed8ca9737@syzkaller.appspotmail.com
> Closes: https://syzkaller.appspot.com/bug?extid=e044a9b6370ed8ca9737
> Assisted-by: Codex:gpt-6.1-sol
> Assisted-by: Claude:claude-opus-5-5
> Signed-off-by: Krystian Kaniewski <krystianmkaniewski@gmail.com>
> ---
This one I need more time to review, from a quick look it looks good.
Just a few minor comments for now.
> struct taprio_sched {
> @@ -902,8 +909,19 @@ static bool should_change_schedules(const struct sched_gate_list *admin,
> * plus the amount that can be extended would fall after the
> * next schedule base_time, we can extend the current schedule
> * for that amount.
> + *
> + * cycle_time_extension is an unbounded signed value from netlink, so
> + * clamp the sum instead of letting it wrap. A wrap would make this
> + * test non-monotonic in end_time, and advance_sched() relies on it
> + * being monotonic to check it once after catching up instead of once
> + * per skipped entry.
> */
> - extension_time = ktime_add_ns(end_time, oper->cycle_time_extension);
> + if (oper->cycle_time_extension > 0 &&
> + end_time > KTIME_MAX - oper->cycle_time_extension)
> + extension_time = KTIME_MAX;
> + else
> + extension_time = ktime_add_ns(end_time,
> + oper->cycle_time_extension);
>
This one I could argue that it could be in a separate patch. As the
wrapping could cause issues even before these changes, no?
> /* FIXME: the IEEE 802.1Q-2018 Specification isn't clear about
> * how precisely the extension should be made. So after
> @@ -915,6 +933,51 @@ static bool should_change_schedules(const struct sched_gate_list *admin,
> return false;
> }
>
> +/* Returns the entry of @sched which is in progress at @now, the one that
> + * advance_sched() reaches by moving one entry per expiry from the start of
> + * the schedule, and sets @start and @end to the times at which it started
> + * and ends. Also sets the end of the cycle which contains it.
> + */
> +static struct sched_entry *taprio_entry_at(struct sched_gate_list *sched,
> + ktime_t now, ktime_t *start,
> + ktime_t *end)
> +{
I like the idea of having this primitive.
> + struct sched_entry *entry, *found = NULL;
> + ktime_t cycle_start = sched->start_time;
> + s64 offset, entry_end = 0, elapsed = 0;
> +
> + if (ktime_after(now, cycle_start)) {
> + s64 periods = div64_s64(ktime_sub(now, cycle_start),
> + sched->period);
> +
> + cycle_start = ktime_add_ns(cycle_start,
> + periods * sched->period);
> + }
> + offset = ktime_sub(now, cycle_start);
> +
> + /* The offset is less than one period, so an entry which ends after
> + * @now is found at the latest at the entry which reaches the end of
> + * the cycle, or at the last entry. A schedule has at least one entry,
> + * so the loop always sets found.
> + */
> + list_for_each_entry(entry, &sched->entries, list) {
> + entry_end = min_t(s64, elapsed + entry->interval,
> + sched->cycle_time);
> + if (offset < entry_end ||
> + list_is_last(&entry->list, &sched->entries)) {
> + found = entry;
> + break;
> + }
> + elapsed = entry_end;
> + }
> +
> + *start = ktime_add_ns(cycle_start, elapsed);
> + *end = ktime_add_ns(cycle_start, entry_end);
> + sched->cycle_end_time = ktime_add_ns(cycle_start,
> sched->cycle_time);
Having this side effect here was unexpected. I think this would make
more sense outside this function.
> +
> + return found;
> +}
> +
> static enum hrtimer_restart advance_sched(struct hrtimer *timer)
> {
> struct taprio_sched *q = container_of(timer, struct taprio_sched,
> @@ -923,8 +986,8 @@ static enum hrtimer_restart advance_sched(struct hrtimer *timer)
> struct sched_gate_list *oper, *admin;
> int num_tc = netdev_get_num_tc(dev);
> struct sched_entry *entry, *next;
> + ktime_t end_time, start, now;
> struct Qdisc *sch = q->root;
> - ktime_t end_time;
> int tc;
>
> spin_lock(&q->current_entry_lock);
> @@ -938,6 +1001,15 @@ static enum hrtimer_restart advance_sched(struct hrtimer *timer)
> if (!oper)
> switch_schedules(q, &admin, &oper);
>
> + /* The entry selected below can be over already, after a clock step,
> + * a long time with interrupts disabled, or because the intervals are
> + * shorter than one expiry takes. Moving on by one entry and arming the
> + * timer in the past would run this function again for every missed
> + * entry without leaving the timer interrupt. Take the entry which is in
> + * progress now instead, so that the timer is always armed after now.
> + */
> + now = taprio_get_time(q);
> +
> /* This can happen in two cases: 1. this is the very first run
> * of this function (i.e. we weren't running any schedule
> * previously); 2. The previous schedule just ended. The first
> @@ -948,27 +1020,26 @@ static enum hrtimer_restart advance_sched(struct hrtimer *timer)
> next = list_first_entry(&oper->entries, struct sched_entry,
> list);
> end_time = next->end_time;
> - goto first_run;
> - }
> -
> - if (should_restart_cycle(oper, entry)) {
> - next = list_first_entry(&oper->entries, struct sched_entry,
> - list);
> - oper->cycle_end_time = ktime_add_ns(oper->cycle_end_time,
> - oper->cycle_time);
> + if (likely(ktime_after(end_time, now)))
> + goto first_run;
> + /* Also after taprio_change() while the schedule runs */
> + next = taprio_entry_at(oper, now, &start, &end_time);
> } else {
> - next = list_next_entry(entry, list);
> - }
> -
> - end_time = ktime_add_ns(entry->end_time, next->interval);
> - end_time = min_t(ktime_t, end_time, oper->cycle_end_time);
> + if (should_restart_cycle(oper, entry)) {
> + next = list_first_entry(&oper->entries,
> + struct sched_entry, list);
> + oper->cycle_end_time =
> + ktime_add_ns(oper->cycle_end_time,
> + oper->cycle_time);
> + } else {
> + next = list_next_entry(entry, list);
> + }
>
> - for (tc = 0; tc < num_tc; tc++) {
> - if (next->gate_duration[tc] == oper->cycle_time)
> - next->gate_close_time[tc] = KTIME_MAX;
> - else
> - next->gate_close_time[tc] = ktime_add_ns(entry->end_time,
> - next->gate_duration[tc]);
> + start = entry->end_time;
> + end_time = ktime_add_ns(start, next->interval);
> + end_time = min_t(ktime_t, end_time, oper->cycle_end_time);
> + if (unlikely(!ktime_after(end_time, now)))
> + next = taprio_entry_at(oper, now, &start, &end_time);
> }
>
> if (should_change_schedules(admin, oper, end_time)) {
> @@ -978,8 +1049,20 @@ static enum hrtimer_restart advance_sched(struct hrtimer *timer)
> */
> next = list_first_entry(&oper->entries, struct sched_entry, list);
> end_time = next->end_time;
> + if (likely(ktime_after(end_time, now)))
> + goto set_budgets;
> + next = taprio_entry_at(oper, now, &start, &end_time);
> + }
> +
> + for (tc = 0; tc < num_tc; tc++) {
> + if (next->gate_duration[tc] == oper->cycle_time)
> + next->gate_close_time[tc] = KTIME_MAX;
> + else
> + next->gate_close_time[tc] =
> + ktime_add_ns(start, next->gate_duration[tc]);
> }
>
> +set_budgets:
> next->end_time = end_time;
> taprio_set_budgets(q, oper, next);
>
> @@ -1285,7 +1368,8 @@ static void setup_first_end_time(struct taprio_sched *q,
> {
> struct net_device *dev = qdisc_dev(q->root);
> int num_tc = netdev_get_num_tc(dev);
> - struct sched_entry *first;
> + struct sched_entry *first, *entry;
> + s64 intervals = 0;
> ktime_t cycle;
> int tc;
>
> @@ -1297,6 +1381,11 @@ static void setup_first_end_time(struct taprio_sched *q,
> /* FIXME: find a better place to do this */
> sched->cycle_end_time = ktime_add_ns(base, cycle);
>
> + list_for_each_entry(entry, &sched->entries, list)
> + intervals += entry->interval;
> + sched->start_time = base;
> + sched->period = min_t(s64, intervals, cycle);
> +
> first->end_time = ktime_add_ns(base, first->interval);
> taprio_set_budgets(q, sched, first);
>
> --
> 2.53.0
>
--
Vinicius
prev parent reply other threads:[~2026-10-12 22:53 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-09 12:33 [PATCH net v2 0/2] net/sched: taprio: fix RCU stall from replayed schedule entries Krystian Kaniewski
2026-10-09 12:33 ` [PATCH net v2 1/2] net/sched: taprio: reject software schedules that overflow their timestamps Krystian Kaniewski
2026-10-12 22:36 ` Vinicius Costa Gomes
2026-10-09 12:33 ` [PATCH net v2 2/2] net/sched: taprio: do not replay missed entries in advance_sched() Krystian Kaniewski
2026-10-12 22:53 ` Vinicius Costa Gomes [this message]
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=87ik36lee9.fsf@intel.com \
--to=vinicius.gomes@intel.com \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=horms@kernel.org \
--cc=jhs@mojatatu.com \
--cc=jiri@resnulli.us \
--cc=krystianmkaniewski@gmail.com \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=syzbot+e044a9b6370ed8ca9737@syzkaller.appspotmail.com \
--cc=vladimir.oltean@nxp.com \
/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
all inboxes | Powered by JetHome®