* [PATCH net] pppox: drain queued packets on channel handoff
@ 2026-08-11 3:53 Qingfang Deng
2026-08-13 0:15 ` Jakub Kicinski
0 siblings, 1 reply; 3+ messages in thread
From: Qingfang Deng @ 2026-08-11 3:53 UTC (permalink / raw)
To: Pali Rohár, linux-ppp, Andrew Lunn, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Qingfang Deng,
Arnd Bergmann, Guillaume Nault, netdev, linux-kernel
PPPIOCGCHAN both returns the channel index and marks a PPPOX socket as
bound to generic PPP, despite its getter semantic. Packets received
before that transition are queued on sk_receive_queue, but a bound
socket is no longer readable. Such packets therefore remain queued until
the socket is destroyed.
After marking a socket bound, wait for receive paths that observed the
old state to finish queueing packets, and then drain the queue into
generic PPP.
Assisted-by: Codex:GPT-5.6
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Signed-off-by: Qingfang Deng <qingfang.deng@linux.dev>
---
drivers/net/ppp/pppox.c | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/drivers/net/ppp/pppox.c b/drivers/net/ppp/pppox.c
index 5861a2f6ce3e..a6f72c813bef 100644
--- a/drivers/net/ppp/pppox.c
+++ b/drivers/net/ppp/pppox.c
@@ -74,7 +74,9 @@ int pppox_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg)
switch (cmd) {
case PPPIOCGCHAN: {
+ struct sk_buff *skb;
int index;
+
rc = -ENOTCONN;
if (!(sk->sk_state & PPPOX_CONNECTED))
break;
@@ -85,7 +87,22 @@ int pppox_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg)
break;
rc = 0;
+ /* PPPIOCGCHAN historically marks the userspace handoff to
+ * generic PPP; pppd then attaches the returned channel to
+ * /dev/ppp.
+ */
sk->sk_state |= PPPOX_BOUND;
+ /* Let lockless receive paths finish queueing against the old
+ * state.
+ */
+ synchronize_net();
+ /* Drain packets queued before the handoff because a bound
+ * socket is no longer readable.
+ */
+ while ((skb = skb_dequeue(&sk->sk_receive_queue))) {
+ skb_orphan(skb);
+ ppp_input(&po->chan, skb);
+ }
break;
}
default:
--
2.43.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [PATCH net] pppox: drain queued packets on channel handoff
2026-08-11 3:53 [PATCH net] pppox: drain queued packets on channel handoff Qingfang Deng
@ 2026-08-13 0:15 ` Jakub Kicinski
2026-08-13 4:01 ` Qingfang Deng
0 siblings, 1 reply; 3+ messages in thread
From: Jakub Kicinski @ 2026-08-13 0:15 UTC (permalink / raw)
To: qingfang.deng
Cc: Jakub Kicinski, pali, linux-ppp, andrew+netdev, davem, edumazet,
pabeni, arnd, gnault, netdev, linux-kernel
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
pppox: drain queued packets on channel handoff
PPPIOCGCHAN marks an AF_PPPOX socket as bound to generic PPP as a side
effect of returning the channel index. This patch adds a synchronize_net()
after setting PPPOX_BOUND and then drains sk->sk_receive_queue into
ppp_input(), since a bound socket is no longer readable from userspace.
A few questions about the new code below.
> diff --git a/drivers/net/ppp/pppox.c b/drivers/net/ppp/pppox.c
> index 5861a2f6ce3e6..a6f72c813bef9 100644
> --- a/drivers/net/ppp/pppox.c
> +++ b/drivers/net/ppp/pppox.c
[ ... ]
> @@ -85,7 +87,22 @@ int pppox_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg)
> break;
>
> rc = 0;
> + /* PPPIOCGCHAN historically marks the userspace handoff to
> + * generic PPP; pppd then attaches the returned channel to
> + * /dev/ppp.
> + */
> sk->sk_state |= PPPOX_BOUND;
> + /* Let lockless receive paths finish queueing against the old
> + * state.
> + */
> + synchronize_net();
[Severity: Medium]
Should this skip the grace period when PPPOX_BOUND is already set?
The only gate before it in pppox_ioctl() is:
rc = -ENOTCONN;
if (!(sk->sk_state & PPPOX_CONNECTED))
break;
so every repeated PPPIOCGCHAN on the same connected socket re-sets
PPPOX_BOUND and runs another full RCU grace period. Neither pppox_create()
nor pppox_ioctl() has a capability check, and pppoe_connect() only needs a
session tuple plus an up device, so an ordinary local process can loop the
ioctl and drive grace periods back to back.
synchronize_net() escalates to synchronize_rcu_expedited() whenever
rtnl_is_locked() is true:
net/core/dev.c:synchronize_net() {
might_sleep();
if (from_cleanup_net() || rtnl_is_locked())
synchronize_rcu_expedited();
else
synchronize_rcu();
}
which sends IPIs to every CPU. The wait also happens with the socket lock
held, so the socket's receive path is pushed into sk_backlog for the whole
duration.
Would something like
if (!(sk->sk_state & PPPOX_BOUND)) {
sk->sk_state |= PPPOX_BOUND;
synchronize_net();
...
}
be enough here? For PPTP sockets pptp_bind() already sets PPPOX_BOUND
before any ioctl, so for that transport the first synchronize_net() has
nothing to wait for either.
[Severity: Medium]
Can this reorder frames for the pppol2tp transport?
PPPOX_BOUND becomes visible before the sleep, but the already-queued frames
are only delivered after it. Meanwhile pppol2tp_recv() reads the state
under rcu_read_lock() only, with no socket lock:
net/l2tp/l2tp_ppp.c:pppol2tp_recv() {
...
if (sk->sk_state & PPPOX_BOUND) {
struct pppox_sock *po;
po = pppox_sk(sk);
ppp_input(&po->chan, skb);
} else {
...
}
So for the length of the grace period newly arriving frames go straight to
ppp_input() while the older ones still sit in sk->sk_receive_queue, and get
injected only afterwards. PPP receive processing is stateful (VJ header
decompression, CCP/MPPE history and sequence numbers, multilink
reassembly) - would injecting the stale frames after the newer ones
desynchronize that state?
Would draining before publishing PPPOX_BOUND, or draining under the
receive queue lock in the same critical section that sets the state, avoid
the inversion?
> + /* Drain packets queued before the handoff because a bound
> + * socket is no longer readable.
> + */
> + while ((skb = skb_dequeue(&sk->sk_receive_queue))) {
> + skb_orphan(skb);
> + ppp_input(&po->chan, skb);
> + }
[Severity: Medium]
Does this feed raw PPTP-GRE frames into ppp_input()?
pppox_ioctl() is also the .ioctl of pptp_ops, and pptp queues frames that
are not yet decapsulated:
drivers/net/ppp/pptp.c:pptp_rcv_core() {
...
if (!(sk->sk_state & PPPOX_CONNECTED)) {
if (sock_queue_rcv_skb(sk, skb))
goto drop;
return NET_RX_SUCCESS;
}
...
}
That branch runs before the skb_pull(headersize), before the
GRE_IS_SEQ()/payload_len validation, before the seq_recv/ack_recv check,
and before the PPP_ALLSTATIONS/PPP_UI strip that the connected path does
prior to calling ppp_input(). Once drained, skb->data points at the GRE
flags/version word, which ppp_decompress_proto()/PPP_PROTO() then
reinterpret as the PPP protocol field, and ownership is gone so the frame
cannot be recovered - it lands on pch->file.rq and pppd reads it as a bogus
PPP control frame, or it is dropped with rx_length_errors.
The commit message says "a bound socket is no longer readable", but for
pptp PPPOX_BOUND has a different meaning entirely:
drivers/net/ppp/pptp.c:pptp_bind() {
...
if (add_chan(po, &sp->sa_addr.pptp))
error = -EBUSY;
else
sk->sk_state |= PPPOX_BOUND;
...
}
pptp_rcv_core() never consults PPPOX_BOUND, and pptp_ops.recvmsg is
sock_no_recvmsg, so that queue was never readable by userspace to begin
with.
The queueing path does look narrow: lookup_chan() only matches when
if (opt->dst_addr.sin_addr.s_addr != s_addr)
sock = NULL;
and dst_addr is zero before pptp_connect(), so a frame with IP source
0.0.0.0 is required; net/ipv4/route.c treats that as martian for ordinary
unicast but still routes it through brd_input() when daddr is
255.255.255.255, and a unicast destination MAC keeps skb->pkt_type ==
PACKET_HOST as pptp_rcv() requires. A same-segment sender that guesses the
bound call-id could therefore park a raw GRE frame on the queue before
connect.
Should the drain be limited to the transports whose queued frames really
are bare PPP frames gated on PPPOX_BOUND (pppoe, pppol2tp), with the
queue simply purged for pptp?
[Severity: Medium]
Does the socket lock actually exclude concurrent readers here?
recvmsg() consumes the same queue without lock_sock():
drivers/net/ppp/pppoe.c:pppoe_recvmsg() {
...
if (sk->sk_state & PPPOX_BOUND)
return -EIO;
skb = skb_recv_datagram(sk, flags, &error);
...
}
net/l2tp/l2tp_ppp.c:pppol2tp_recvmsg() is the same shape. Only the receive
queue spinlock is shared with the drain, and the PPPOX_BOUND test is a
check-then-act.
Three things seem to follow:
A reader that passed the PPPOX_BOUND test just before the ioctl set it can
dequeue a packet the drain meant to hand to PPP, so the PPP session loses
it silently.
The drain can also steal the packet a blocking recvmsg() was woken for.
__skb_wait_for_more_packets() watches the queue, socket errors and
shutdown, but not PPPOX_BOUND, so the waiter finds an empty queue and
sleeps again - and since PPPOX_BOUND is now set nothing is ever queued
again, leaving a recvmsg() with no timeout asleep indefinitely. Is that
possible?
With MSG_PEEK, __skb_try_recv_from_queue() bumps skb->users and leaves the
skb linked. The drain's skb_dequeue() then unlinks the skb the reader is
still copying from, skb_orphan() runs sock_rfree() and clears skb->sk, and
ppp_input() mutates the buffer (ppp_decompress_proto() pull/push, then
skb_queue_tail() to pch->file.rq or netif_rx()) while userspace copies from
it. Should the drain check skb_shared()/skb_cloned(), or otherwise exclude
peeking readers, before handing the skb on?
> break;
> }
> default:
--
pw-bot: cr
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH net] pppox: drain queued packets on channel handoff
2026-08-13 0:15 ` Jakub Kicinski
@ 2026-08-13 4:01 ` Qingfang Deng
0 siblings, 0 replies; 3+ messages in thread
From: Qingfang Deng @ 2026-08-13 4:01 UTC (permalink / raw)
To: Jakub Kicinski
Cc: pali, linux-ppp, andrew+netdev, davem, edumazet, pabeni, arnd,
gnault, netdev, linux-kernel
On 2026/8/13 8:15, Jakub Kicinski wrote:
> This is an AI-generated review of your patch. The human sending this
> email has considered the AI review valid, or at least plausible.
> ---
> pppox: drain queued packets on channel handoff
>
> PPPIOCGCHAN marks an AF_PPPOX socket as bound to generic PPP as a side
> effect of returning the channel index. This patch adds a synchronize_net()
> after setting PPPOX_BOUND and then drains sk->sk_receive_queue into
> ppp_input(), since a bound socket is no longer readable from userspace.
>
> A few questions about the new code below.
>
>> diff --git a/drivers/net/ppp/pppox.c b/drivers/net/ppp/pppox.c
>> index 5861a2f6ce3e6..a6f72c813bef9 100644
>> --- a/drivers/net/ppp/pppox.c
>> +++ b/drivers/net/ppp/pppox.c
> [ ... ]
>
>> @@ -85,7 +87,22 @@ int pppox_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg)
>> break;
>>
>> rc = 0;
>> + /* PPPIOCGCHAN historically marks the userspace handoff to
>> + * generic PPP; pppd then attaches the returned channel to
>> + * /dev/ppp.
>> + */
>> sk->sk_state |= PPPOX_BOUND;
>> + /* Let lockless receive paths finish queueing against the old
>> + * state.
>> + */
>> + synchronize_net();
> [Severity: Medium]
> Should this skip the grace period when PPPOX_BOUND is already set?
>
> The only gate before it in pppox_ioctl() is:
>
> rc = -ENOTCONN;
> if (!(sk->sk_state & PPPOX_CONNECTED))
> break;
>
> so every repeated PPPIOCGCHAN on the same connected socket re-sets
> PPPOX_BOUND and runs another full RCU grace period. Neither pppox_create()
> nor pppox_ioctl() has a capability check, and pppoe_connect() only needs a
> session tuple plus an up device, so an ordinary local process can loop the
> ioctl and drive grace periods back to back.
This is theoretically possible, but in practice pppd does not ioctl
PPPIOCGCHAN twice on the same socket.
> [Severity: Medium]
> Can this reorder frames for the pppol2tp transport?
>
> PPPOX_BOUND becomes visible before the sleep, but the already-queued frames
> are only delivered after it. Meanwhile pppol2tp_recv() reads the state
> under rcu_read_lock() only, with no socket lock:
>
> net/l2tp/l2tp_ppp.c:pppol2tp_recv() {
> ...
> if (sk->sk_state & PPPOX_BOUND) {
> struct pppox_sock *po;
>
> po = pppox_sk(sk);
> ppp_input(&po->chan, skb);
> } else {
> ...
> }
>
> So for the length of the grace period newly arriving frames go straight to
> ppp_input() while the older ones still sit in sk->sk_receive_queue, and get
> injected only afterwards. PPP receive processing is stateful (VJ header
> decompression, CCP/MPPE history and sequence numbers, multilink
> reassembly) - would injecting the stale frames after the newer ones
> desynchronize that state?
> Would draining before publishing PPPOX_BOUND, or draining under the
> receive queue lock in the same critical section that sets the state, avoid
> the inversion?
Neither suggested change alone is sufficient:
- Draining before publishing PPPOX_BOUND does not eliminate the window
between CONNECTED and BOUND.
- Holding sk_receive_queue.lock while setting the state and draining
does not help because the bound branch never takes that lock. A receiver
that already chose the unbound branch can also block inside
sock_queue_rcv_skb() and enqueue after the critical section.
If out-of-order is not acceptable, dropping them seems to be much easier.
>
>> + /* Drain packets queued before the handoff because a bound
>> + * socket is no longer readable.
>> + */
>> + while ((skb = skb_dequeue(&sk->sk_receive_queue))) {
>> + skb_orphan(skb);
>> + ppp_input(&po->chan, skb);
>> + }
> [Severity: Medium]
> Does this feed raw PPTP-GRE frames into ppp_input()?
>
> pppox_ioctl() is also the .ioctl of pptp_ops, and pptp queues frames that
> are not yet decapsulated:
>
> drivers/net/ppp/pptp.c:pptp_rcv_core() {
> ...
> if (!(sk->sk_state & PPPOX_CONNECTED)) {
> if (sock_queue_rcv_skb(sk, skb))
> goto drop;
> return NET_RX_SUCCESS;
> }
> ...
> }
This has been fixed by "pptp: drop packets received before connect".
>
> [Severity: Medium]
> Does the socket lock actually exclude concurrent readers here?
>
> recvmsg() consumes the same queue without lock_sock():
>
> drivers/net/ppp/pppoe.c:pppoe_recvmsg() {
> ...
> if (sk->sk_state & PPPOX_BOUND)
> return -EIO;
>
> skb = skb_recv_datagram(sk, flags, &error);
> ...
> }
>
> net/l2tp/l2tp_ppp.c:pppol2tp_recvmsg() is the same shape. Only the receive
> queue spinlock is shared with the drain, and the PPPOX_BOUND test is a
> check-then-act.
I may add a socket lock in recvmsg, but again this is only theoretically
possible. There are no concurrent readers in practice.
>
> Three things seem to follow:
>
> A reader that passed the PPPOX_BOUND test just before the ioctl set it can
> dequeue a packet the drain meant to hand to PPP, so the PPP session loses
> it silently.
>
> The drain can also steal the packet a blocking recvmsg() was woken for.
> __skb_wait_for_more_packets() watches the queue, socket errors and
> shutdown, but not PPPOX_BOUND, so the waiter finds an empty queue and
> sleeps again - and since PPPOX_BOUND is now set nothing is ever queued
> again, leaving a recvmsg() with no timeout asleep indefinitely. Is that
> possible?
This is also a pre-existing issue. The ioctl does not wake up blocking
readers which are already waiting in recvmsg().
>
> With MSG_PEEK, __skb_try_recv_from_queue() bumps skb->users and leaves the
> skb linked. The drain's skb_dequeue() then unlinks the skb the reader is
> still copying from, skb_orphan() runs sock_rfree() and clears skb->sk, and
> ppp_input() mutates the buffer (ppp_decompress_proto() pull/push, then
> skb_queue_tail() to pch->file.rq or netif_rx()) while userspace copies from
> it. Should the drain check skb_shared()/skb_cloned(), or otherwise exclude
> peeking readers, before handing the skb on?
>
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-08-13 4:01 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-11 3:53 [PATCH net] pppox: drain queued packets on channel handoff Qingfang Deng
2026-08-13 0:15 ` Jakub Kicinski
2026-08-13 4:01 ` Qingfang Deng
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox