net/tcp-ao: fix use-after-free of current_key on reconnect to another peer
Subsystems (3)
PATCH 1/1 net/tcp-ao: fix use-after-free of current_key on reconnect to another peer
tcp_inbound_ao_hash() is called before bh_lock_sock_nested() is taken, with only rcu_read_lock() held. On the fast path for established sockets, if the rnext_keyid sent by the peer differs from current_key->sndid, the key the peer asked for is looked up and stored in current_key. The lookup is inside the RCU read side, but current_key outlives it.
When the socket is disconnected and connect() is called again for another peer, tcp_ao_connect_init() unlinks every key that does not match the new peer and frees it with call_rcu(). If current_key points at such a key, it is cleared to NULL.
The fast path reads sk_state only once on entry, so a softirq that got into it while the socket was still established can update current_key after that loop has already run. The update is inside the RCU read side, so it comes before the call_rcu() callback, and once the callback frees the key, current_key is left pointing at freed memory.
The next transmission picks that pointer up in tcp_get_current_key(). tcp_ao_transmit_skb() then reads the traffic key from the freed object, which is the use-after-free.
Wait for one grace period before unlinking, and only if a key is going to be removed. By the time tcp_connect() runs the socket is already in TCP_SYN_SENT, and TCP_AO_ESTABLISHED does not contain TCPF_SYN_SENT, so a softirq entering after the wait cannot reach the fast path, and the ones already in it have finished. The existing NULL handling in the loop is then enough.
--- net/ipv4/tcp_ao.c | 9 +++++++++ 1 file changed, 9 insertions(+)
net/ipv4/tcp_ao.c +9 -0
@@ -1160,6 +1160,15 @@ void tcp_ao_connect_init(struct sock *sk) l3index =l3mdev_master_ifindex_by_index (sock_net (sk), sk->sk_bound_dev_if); +hlist_for_each_entry (key, &ao_info->head, node) { +if (tcp_ao_key_cmp (key, l3index, addr, key->prefixlen, + family,-1 ,-1 )) { +/* pairs with tcp_inbound_ao_hash() */ +synchronize_rcu (); +break ; + } + } +hlist_for_each_entry_safe (key, next, &ao_info->head, node) {if (!tcp_ao_key_cmp (key, l3index, addr, key->prefixlen, family,-1 ,-1 ))continue ;
Signature · 1 line
2.43.0
Quoted 29 lines · Hyunwoo Kim
tcp_inbound_ao_hash()is called beforebh_lock_sock_nested()is taken, with onlyrcu_read_lock()held. On the fast path for established sockets, if thernext_keyidsent by the peer differs fromcurrent_key->sndid, the key the peer asked for is looked up and stored incurrent_key. The lookup is inside the RCU read side, butcurrent_keyoutlives it.When the socket is disconnected and
connect()is called again for another peer,tcp_ao_connect_init()unlinks every key that does not match the new peer and frees it withcall_rcu(). Ifcurrent_keypoints at such a key, it is cleared to NULL.The fast path reads
sk_stateonly once on entry, so a softirq that got into it while the socket was still established can updatecurrent_keyafter that loop has already run. The update is inside the RCU read side, so it comes before thecall_rcu()callback, and once the callback frees the key,current_keyis left pointing at freed memory.The next transmission picks that pointer up in
tcp_get_current_key().tcp_ao_transmit_skb()then reads the traffic key from the freed object, which is the use-after-free.Wait for one grace period before unlinking, and only if a key is going to be removed. By the time
tcp_connect()runs the socket is already inTCP_SYN_SENT, andTCP_AO_ESTABLISHEDdoes not containTCPF_SYN_SENT, so a softirq entering after the wait cannot reach the fast path, and the ones already in it have finished. The existing NULL handling in the loop is then enough.
Quoted 3 lines
Fixes 0a3a809089eb ("net/tcp: Verify inbound TCP-AO signed segments")Signed-off-by Hyunwoo Kim <[email protected]>
I do have some concern about potential performance impact of using synchronize_rcu(). But on the balance I agree that this is a good bug-fix patch.
Quoted 41 lines · Hyunwoo Kim
tcp_inbound_ao_hash()is called beforebh_lock_sock_nested()is taken, with onlyrcu_read_lock()held. On the fast path for established sockets, if thernext_keyidsent by the peer differs fromcurrent_key->sndid, the key the peer asked for is looked up and stored incurrent_key. The lookup is inside the RCU read side, butcurrent_keyoutlives it.When the socket is disconnected and
connect()is called again for another peer,tcp_ao_connect_init()unlinks every key that does not match the new peer and frees it withcall_rcu(). Ifcurrent_keypoints at such a key, it is cleared to NULL.The fast path reads
sk_stateonly once on entry, so a softirq that got into it while the socket was still established can updatecurrent_keyafter that loop has already run. The update is inside the RCU read side, so it comes before thecall_rcu()callback, and once the callback frees the key,current_keyis left pointing at freed memory.The next transmission picks that pointer up in
tcp_get_current_key().tcp_ao_transmit_skb()then reads the traffic key from the freed object, which is the use-after-free.Wait for one grace period before unlinking, and only if a key is going to be removed. By the time
tcp_connect()runs the socket is already inTCP_SYN_SENT, andTCP_AO_ESTABLISHEDdoes not containTCPF_SYN_SENT, so a softirq entering after the wait cannot reach the fast path, and the ones already in it have finished. The existing NULL handling in the loop is then enough.Fixes 0a3a809089eb ("net/tcp: Verify inbound TCP-AO signed segments")Signed-off-by Hyunwoo Kim <[email protected]>--- net/ipv4/tcp_ao.c | 9 +++++++++ 1 file changed, 9 insertions(+)net/ipv4/tcp_ao.c +0
-0
net/ipv4/tcp_ao.c void tcp_ao_connect_init(struct sock *sk) · line 1167
Quoted 8 lines
net/ipv4/tcp_ao.c +5
-0@@ -1160,6 +1160,15 @@ void tcp_ao_connect_init(struct sock *sk) l3index =l3mdev_master_ifindex_by_index (sock_net (sk), sk->sk_bound_dev_if); +hlist_for_each_entry (key, &ao_info->head, node) { +if (tcp_ao_key_cmp (key, l3index, addr, key->prefixlen, + family,-1 ,-1 )) { +/* pairs with tcp_inbound_ao_hash() */ +synchronize_rcu ();
My understanding is that the above will rate-limit _only_ disconnect()/connect() sequences. Since the former op is basically evil, I think that the extremely severe constraint on it is acceptable.
Quoted 7 lines
net/ipv4/tcp_ao.c +4
-0+break ; + } + } +hlist_for_each_entry_safe (key, next, &ao_info->head, node) {if (!tcp_ao_key_cmp (key, l3index, addr, key->prefixlen, family,-1 ,-1 ))continue ;
This patch was applied to netdev/net.git (main) by Jakub Kicinski <[email protected]>:
Hello:
This patch was applied to netdev/net.git (main)
by Jakub Kicinski <[email protected]>:
Quoted 8 lines · On Mon, 17 Aug 2026 06:28:42 +0900 you wrote:
tcp_inbound_ao_hash()is called beforebh_lock_sock_nested()is taken, with onlyrcu_read_lock()held. On the fast path for established sockets, if thernext_keyidsent by the peer differs fromcurrent_key->sndid, the key the peer asked for is looked up and stored incurrent_key. The lookup is inside the RCU read side, butcurrent_keyoutlives it.[...]
Here is the summary with links:
- [net] net/tcp-ao: fix use-after-free of current_key on reconnect to another peer
https://git.kernel.org/netdev/net/c/da4471557f27
You are awesome, thank you!
Signature · 2 lines
Deet-doot-dot, I am a bot. https://korg.docs.kernel.org/patchwork/pwbot.html