From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from desiato.infradead.org (desiato.infradead.org [90.155.92.199]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 4992D26AE5 for ; Sun, 21 Jun 2026 22:01:19 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=90.155.92.199 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1782079282; cv=none; b=Gp/2MPstaquiHebeud2t7Z4qQJbSqfFZE2raVqLJQxURO7uJEji8to+5Dr+Zjj6bWK/M1d1OR5CEIMU5ii6ka2G0i8A3FE95Yt7/THb9dqA+ztIaCwgdQccFFaTiDCtkIGF19YExuKwMykQ48wvsrUn2JO/7Hvq8xa3ctKwFM6k= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1782079282; c=relaxed/simple; bh=i80d2boqJH9Rj9HiUDstxVu7TMJkPC8k3AyUWIpnrj4=; h=From:To:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=Rfanc800uOUxgU/za1IadQRBCFZ9ytMuKfX/3YUDHqbbLYv9ep9euTvpB6JfZvys4FI1VBh3gURYEcXFjnF7Ew7ovM323lGDbocNkjaXnegVK+ICPDp0a0qvc2wR0BSL1J1UB0GsXz2GefRU2KuRY8IwBbJI6D6C8oAZ+5RfxGY= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=infradead.org; spf=none smtp.mailfrom=desiato.srs.infradead.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b=qpbTQuiz; arc=none smtp.client-ip=90.155.92.199 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=infradead.org Authentication-Results: smtp.subspace.kernel.org; spf=none smtp.mailfrom=desiato.srs.infradead.org Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b="qpbTQuiz" DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=infradead.org; s=desiato.20200630; h=Sender:Content-Transfer-Encoding: Content-Type:MIME-Version:References:In-Reply-To:Message-ID:Date:Subject:To: From:Reply-To:Cc:Content-ID:Content-Description; bh=4nQ1ku43t0EkGeByIW2UDRpKVpc8ofSher605fPMsSc=; b=qpbTQuizBdUcqXUXiKLcqr9njd CtbDkAV73eKBd0/gPoCiYw/iwwqGnMUtKq9lOwAykQ/Pzl1FSdbHKw6olj4q8bcHsypmqc1iTQXI+ 4FbjFD0UOgMfE3uv9cZVGjaeZi4d0lU0ruQTYtY09hxeK/NvzmlKiPj/3qb8BrD7OPPNa7faEJWG2 bWKFkxX/lFrbcD+3CDt1pAVY3920LKz1LDaNqbK9+itosq4J4AXmYAXgIA7EOBNp/6WtldTCBOVt7 Meyda6B/77IyPoVWebm8Ag48kmY3fsDZ2oPlipLJgx9tORqedUoiSvXA43kJMbBtYkd9fXd74d0si Pwg4qd7w==; Received: from [2001:8b0:10b:1::425] (helo=i7.infradead.org) by desiato.infradead.org with esmtpsa (Exim 4.99.2 #2 (Red Hat Linux)) id 1wbQDo-0000000GjbB-1ZWj; Sun, 21 Jun 2026 22:00:56 +0000 Received: from dwoodhou by i7.infradead.org with local (Exim 4.99.2 #2 (Red Hat Linux)) id 1wbQDk-00000004K81-3zqt; Sun, 21 Jun 2026 23:00:52 +0100 From: David Woodhouse To: Richard Cochran , Wen Gu , David Woodhouse , Andrew Lunn , "David S. Miller" , Eric Dumazet , Jakub Kicinski , Paolo Abeni , John Stultz , Thomas Gleixner , Stephen Boyd , Anna-Maria Behnsen , Frederic Weisbecker , Shuah Khan , Peter Zijlstra , =?UTF-8?q?Thomas=20Wei=C3=9Fschuh?= , Arnd Bergmann , Miroslav Lichvar , Julien Ridoux , Ryan Luu , linux-kernel@vger.kernel.org Subject: [PATCH v7 6/7] timekeeping: Settle competing time_offset and time_adjust skew Date: Sun, 21 Jun 2026 22:53:59 +0100 Message-ID: <20260621220051.1030462-7-dwmw2@infradead.org> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260621220051.1030462-1-dwmw2@infradead.org> References: <20260621220051.1030462-1-dwmw2@infradead.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sender: David Woodhouse X-SRS-Rewrite: SMTP reverse-path rewritten from by desiato.infradead.org. See http://www.infradead.org/rpr.html From: David Woodhouse time_offset (the exponential PLL phase slew) and time_adjust (the linear adjtime() slew) can be asked to move the clock in opposite directions. second_overflow() folds only their *net* into the per-tick skew_delta, so the cancelling overlap would never be drained from either tracker by the per-tick code — and if they cancel exactly, skew_delta is zero and neither converges at all. Arguably we could just let one of them entirely cancel out the other immediately, but that would be a change in userspace-visible behaviour. Instead, preserve the existing behaviour by calculating the "conflict" portion between the opposing skew each second, and transferring that amount directly from one tracker to the other. Signed-off-by: David Woodhouse Assisted-by: Kiro:claude-opus-4.8 --- kernel/time/ntp.c | 75 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/kernel/time/ntp.c b/kernel/time/ntp.c index 20d211b27908..73a47dd775a0 100644 --- a/kernel/time/ntp.c +++ b/kernel/time/ntp.c @@ -514,6 +514,69 @@ s64 ntp_drain_skew(unsigned int tkid, s64 amount, unsigned int shift) return amount - unclaimed; } +/* + * time_offset (drained exponentially) and time_adjust (drained linearly at the + * MAX_TICKADJ rate) can be asked to slew the clock in opposite directions. + * second_overflow() only folds their *net* into skew_delta, so the cancelling + * part would never be drained from either tracker via the per-tick code -- and + * if they cancel exactly, skew_delta is zero and neither converges at all. + * + * Settle that cancelling phase directly between the two here. No clock motion + * results (the opposing slews annihilate), but both move toward zero so neither + * stalls. @amount is the phase to take off time_offset, in its (÷HZ) units and + * with its sign; the same real magnitude comes off time_adjust in the opposite + * direction. Clamped so neither tracker is driven past zero. + */ +static void ntp_transfer_offset_adjust(struct ntp_data *ntpdata, s64 amount) +{ + s64 frac_delta, carry; + + /* + * Don't drain time_offset past zero. @amount shares its sign and is + * normally bounded below it by ntp_offset_chunk(), but the ±1 skew_delta + * floor for a tiny time_offset can exceed it, so clamp. + */ + if (abs(amount) > abs(ntpdata->time_offset)) + amount = ntpdata->time_offset; + if (!amount) + return; + + /* + * Remove the matching phase from time_adjust, in plain shifted-ns. No + * clamp against time_adjust's zero is needed: @amount is bounded by the + * adjtime chunk, which second_overflow() never lets exceed time_adjust's + * own pending phase, so this cannot overshoot. + */ + frac_delta = amount * NTP_INTERVAL_FREQ; + + ntpdata->time_offset -= amount; + + /* Add the matching phase to time_adjust, carrying whole µs (O(1)). */ + ntpdata->time_adjust_frac += frac_delta; + if (ntpdata->time_adjust_frac >= ONE_US_NS || + ntpdata->time_adjust_frac <= -ONE_US_NS) { + carry = div64_s64(ntpdata->time_adjust_frac, ONE_US_NS); + ntpdata->time_adjust += carry; + ntpdata->time_adjust_frac -= carry * ONE_US_NS; + } + + /* + * Keep time_adjust and its sub-µs remainder the same sign. The + * truncating carry above can leave them opposed (e.g. +4 µs paired + * with -250 ns), and ntp_drain_time_adjust() treats abs(time_adjust_frac) + * as same-direction drawer capacity -- an opposing remainder there makes + * it over-deliver phase that was never removed from the pile. Borrow or + * repay a single whole µs to realign; the total phase is unchanged. + */ + if (ntpdata->time_adjust > 0 && ntpdata->time_adjust_frac < 0) { + ntpdata->time_adjust--; + ntpdata->time_adjust_frac += ONE_US_NS; + } else if (ntpdata->time_adjust < 0 && ntpdata->time_adjust_frac > 0) { + ntpdata->time_adjust++; + ntpdata->time_adjust_frac -= ONE_US_NS; + } +} + /** * ntp_get_next_leap - Returns the next leapsecond in CLOCK_REALTIME ktime_t * @tkid: Timekeeper ID @@ -651,6 +714,18 @@ int second_overflow(unsigned int tkid, time64_t secs) adj_chunk = signof(ntpdata->time_adjust_frac); } + /* + * If the two slews oppose, only their net would drive the + * per-tick drain, so the cancelling part would never drain from + * either tracker and an exact cancellation would stall both. + * Settle that overlap directly between them (no clock motion). + */ + if (off_chunk && adj_chunk && signof(off_chunk) != signof(adj_chunk)) { + s64 conflict = min(abs(off_chunk), abs(adj_chunk)); + + ntp_transfer_offset_adjust(ntpdata, signof(off_chunk) * conflict); + } + /* Net is what the clock delivers; reduce to per-tick, then floor. */ net = off_chunk + adj_chunk; ntpdata->skew_delta = div_s64(net, NTP_INTERVAL_FREQ); -- 2.54.0