* [PATCH net v4 1/5] amt: fix use-after-free when a Membership Update re-arms a freed tunnel
2026-10-15 22:06 [PATCH net v4 0/5] amt: relay tunnel lifetime, validation and request-path hardening Omar Ramadan
@ 2026-10-15 22:06 ` Omar Ramadan
2026-10-19 22:39 ` netdev-bot+sashiko
2026-10-15 22:06 ` [PATCH net v4 2/5] amt: reject a gateway Query or Data from 0.0.0.0 before a relay is learned Omar Ramadan
` (4 subsequent siblings)
5 siblings, 1 reply; 11+ messages in thread
From: Omar Ramadan @ 2026-10-15 22:06 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni
Cc: netdev, linux-kernel, Simon Horman
amt_update_handler() refreshes a relay tunnel's garbage-collection timer
with mod_delayed_work(&tunnel->gc_wq, ...) on a tunnel found by a lockless
list_for_each_entry_rcu() walk, holding only rcu_read_lock(). Nothing
serialises this against amt_tunnel_expire(), which runs from that same
gc_wq: once expire has taken amt->lock, done list_del_rcu() and queued
kfree_rcu(), a concurrent Update that already cached the tunnel pointer
re-arms gc_wq on an object that is about to be freed. After the grace
period the delayed_work timer is left armed in freed memory, so it fires
into a use-after-free (and double-list_del_rcu/nr_tunnels underflow on the
way).
Make amt_tunnel_expire() the only context that frees a tunnel on the
timeout path and let it re-arm itself: the Update path only stamps
tunnel->last_seen, and expire re-reads last_seen under amt->lock to decide
whether to re-arm or free. The Update path therefore never touches gc_wq,
so it can no longer re-queue the work of a tunnel being torn down.
amt_dev_stop() is the other freer, and it walked tunnel_list without
amt->lock while a running gc_wq could free the next entry under it. Mark
every tunnel dead under amt->lock first, so a gc_wq that is already
running sees ->dead and bails instead of freeing in parallel; then unlink
the tunnels one at a time with list_del_rcu() under amt->lock, and cancel,
clear and free each one outside it. The tunnel is freed exactly once, and
because readers walk tunnel_list locklessly under RCU, the entries are
unlinked in place rather than spliced onto a local list head.
Fixes: cbc21dc1cfe9 ("amt: add data plane of amt interface")
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
diff --git a/drivers/net/amt.c b/drivers/net/amt.c
index b53f8ec..b06916c 100644
--- a/drivers/net/amt.c
+++ b/drivers/net/amt.c
@@ -215,9 +215,14 @@ static struct amt_group_node *amt_lookup_group(struct amt_tunnel_list *tunnel,
static void amt_destroy_source(struct amt_source_node *snode)
{
- struct amt_group_node *gnode = snode->gnode;
+ struct amt_group_node *gnode;
struct amt_tunnel_list *tunnel;
+ if (READ_ONCE(snode->dead))
+ return;
+ WRITE_ONCE(snode->dead, true);
+ gnode = snode->gnode;
+
tunnel = gnode->tunnel_list;
if (!gnode->v6) {
@@ -249,6 +254,9 @@ static void amt_del_group(struct amt_dev *amt, struct amt_group_node *gnode)
struct hlist_node *t;
int i;
+ if (READ_ONCE(gnode->dead))
+ return;
+ WRITE_ONCE(gnode->dead, true);
if (cancel_delayed_work(&gnode->group_timer))
dev_put(amt->dev);
hlist_del_rcu(&gnode->node);
@@ -282,13 +290,20 @@ static void amt_source_work(struct work_struct *work)
struct amt_source_node *snode = container_of(to_delayed_work(work),
struct amt_source_node,
source_timer);
- struct amt_group_node *gnode = snode->gnode;
- struct amt_dev *amt = gnode->amt;
+ struct amt_group_node *gnode;
+ struct amt_dev *amt;
struct amt_tunnel_list *tunnel;
- tunnel = gnode->tunnel_list;
- spin_lock_bh(&tunnel->lock);
rcu_read_lock();
+ gnode = READ_ONCE(snode->gnode);
+ amt = READ_ONCE(gnode->amt);
+ tunnel = READ_ONCE(gnode->tunnel_list);
+ spin_lock_bh(&tunnel->lock);
+ if (READ_ONCE(snode->dead) || READ_ONCE(gnode->dead)) {
+ spin_unlock_bh(&tunnel->lock);
+ rcu_read_unlock();
+ return;
+ }
if (gnode->filter_mode == MCAST_INCLUDE) {
amt_destroy_source(snode);
if (!gnode->nr_sources)
@@ -299,8 +314,8 @@ static void amt_source_work(struct work_struct *work)
*/
snode->status = AMT_SOURCE_STATUS_D_FWD;
}
- rcu_read_unlock();
spin_unlock_bh(&tunnel->lock);
+ rcu_read_unlock();
}
static void amt_act_src(struct amt_tunnel_list *tunnel,
@@ -403,26 +418,35 @@ static void amt_group_work(struct work_struct *work)
struct amt_group_node *gnode = container_of(to_delayed_work(work),
struct amt_group_node,
group_timer);
- struct amt_tunnel_list *tunnel = gnode->tunnel_list;
- struct amt_dev *amt = gnode->amt;
+ struct amt_tunnel_list *tunnel;
+ struct amt_dev *amt;
struct amt_source_node *snode;
bool delete_group = true;
struct hlist_node *t;
int i, buckets;
+ rcu_read_lock();
+ tunnel = READ_ONCE(gnode->tunnel_list);
+ amt = READ_ONCE(gnode->amt);
buckets = amt->hash_buckets;
spin_lock_bh(&tunnel->lock);
+ if (READ_ONCE(gnode->dead)) {
+ spin_unlock_bh(&tunnel->lock);
+ rcu_read_unlock();
+ dev_put(amt->dev);
+ return;
+ }
if (gnode->filter_mode == MCAST_INCLUDE) {
/* Not Used */
spin_unlock_bh(&tunnel->lock);
- goto out;
+ rcu_read_unlock();
+ dev_put(amt->dev);
+ return;
}
- rcu_read_lock();
- for (i = 0; i < buckets; i++) {
- hlist_for_each_entry_safe(snode, t,
- &gnode->sources[i], node) {
+ for (i = 0; i < buckets; i++)
+ hlist_for_each_entry_safe(snode, t, &gnode->sources[i], node) {
if (!delayed_work_pending(&snode->source_timer) ||
snode->status == AMT_SOURCE_STATUS_D_FWD) {
amt_destroy_source(snode);
@@ -431,14 +455,12 @@ static void amt_group_work(struct work_struct *work)
snode->status = AMT_SOURCE_STATUS_FWD;
}
}
- }
if (delete_group)
amt_del_group(amt, gnode);
else
gnode->filter_mode = MCAST_INCLUDE;
- rcu_read_unlock();
spin_unlock_bh(&tunnel->lock);
-out:
+ rcu_read_unlock();
dev_put(amt->dev);
}
@@ -1328,8 +1350,30 @@ static void amt_tunnel_expire(struct work_struct *work)
struct amt_tunnel_list,
gc_wq);
struct amt_dev *amt = tunnel->amt;
+ unsigned long deadline;
spin_lock_bh(&amt->lock);
+ /* amt_dev_stop() has taken ownership of the teardown. */
+ if (READ_ONCE(tunnel->dead)) {
+ spin_unlock_bh(&amt->lock);
+ return;
+ }
+ /* A Membership Update may have refreshed the tunnel after this work
+ * was queued. Re-arm from last_seen rather than letting
+ * amt_update_handler() re-arm gc_wq: this handler is then the only
+ * context that frees the tunnel on the timeout path; amt_dev_stop()
+ * still owns teardown during device shutdown.
+ */
+ deadline = READ_ONCE(tunnel->last_seen) +
+ msecs_to_jiffies(amt_gmi(amt)) * 3;
+ if (time_before(jiffies, deadline)) {
+ mod_delayed_work(amt_wq, &tunnel->gc_wq, deadline - jiffies);
+ spin_unlock_bh(&amt->lock);
+ return;
+ }
+ spin_lock_bh(&tunnel->lock);
+ WRITE_ONCE(tunnel->dead, true);
+ spin_unlock_bh(&tunnel->lock);
rcu_read_lock();
list_del_rcu(&tunnel->list);
amt->nr_tunnels--;
@@ -2494,9 +2538,13 @@ static bool amt_update_handler(struct amt_dev *amt, struct sk_buff *skb)
if (tunnel->ip4 == saddr) {
if ((nonce == tunnel->nonce &&
response_mac == tunnel->mac)) {
- mod_delayed_work(amt_wq, &tunnel->gc_wq,
- msecs_to_jiffies(amt_gmi(amt))
- * 3);
+ /* Refresh liveness only. gc_wq is
+ * re-armed by amt_tunnel_expire() itself
+ * from last_seen, never from here, so this
+ * cannot re-queue the work of a tunnel
+ * that is being freed.
+ */
+ WRITE_ONCE(tunnel->last_seen, jiffies);
goto report;
} else {
netdev_dbg(amt->dev, "Invalid MAC\n");
@@ -2732,6 +2780,7 @@ static bool amt_request_handler(struct amt_dev *amt, struct sk_buff *skb)
for (i = 0; i < amt->hash_buckets; i++)
INIT_HLIST_HEAD(&tunnel->groups[i]);
+ tunnel->last_seen = jiffies;
INIT_DELAYED_WORK(&tunnel->gc_wq, amt_tunnel_expire);
list_add_tail_rcu(&tunnel->list, &amt->tunnel_list);
@@ -2739,7 +2788,7 @@ static bool amt_request_handler(struct amt_dev *amt, struct sk_buff *skb)
__amt_update_relay_status(tunnel, AMT_STATUS_RECEIVED_REQUEST, true);
amt->nr_tunnels++;
mod_delayed_work(amt_wq, &tunnel->gc_wq,
- msecs_to_jiffies(amt_gmi(amt)));
+ msecs_to_jiffies(amt_gmi(amt)) * 3);
spin_unlock_bh(&amt->lock);
send:
@@ -3050,7 +3099,7 @@ static int amt_dev_open(struct net_device *dev)
static int amt_dev_stop(struct net_device *dev)
{
struct amt_dev *amt = netdev_priv(dev);
- struct amt_tunnel_list *tunnel, *tmp;
+ struct amt_tunnel_list *tunnel;
struct sk_buff *skb;
struct sock *sk;
int i;
@@ -3079,9 +3128,34 @@ static int amt_dev_stop(struct net_device *dev)
amt->req_cnt = 0;
WRITE_ONCE(amt->remote_ip, 0);
- list_for_each_entry_safe(tunnel, tmp, &amt->tunnel_list, list) {
- list_del_rcu(&tunnel->list);
- amt->nr_tunnels--;
+ /* Mark every tunnel dead first, so a concurrent amt_tunnel_expire()
+ * sees ->dead and bails instead of freeing a tunnel in parallel; this
+ * path is then the sole freer.
+ */
+ spin_lock_bh(&amt->lock);
+ list_for_each_entry(tunnel, &amt->tunnel_list, list) {
+ spin_lock_bh(&tunnel->lock);
+ WRITE_ONCE(tunnel->dead, true);
+ spin_unlock_bh(&tunnel->lock);
+ }
+ spin_unlock_bh(&amt->lock);
+
+ /* Unlink one tunnel at a time with list_del_rcu() rather than splicing
+ * the list onto a local head: a lockless reader walking tunnel_list
+ * must always terminate at &amt->tunnel_list, never at a stack head.
+ */
+ for (;;) {
+ spin_lock_bh(&amt->lock);
+ tunnel = list_first_entry_or_null(&amt->tunnel_list,
+ struct amt_tunnel_list, list);
+ if (tunnel) {
+ list_del_rcu(&tunnel->list);
+ amt->nr_tunnels--;
+ }
+ spin_unlock_bh(&amt->lock);
+ if (!tunnel)
+ break;
+
cancel_delayed_work_sync(&tunnel->gc_wq);
amt_clear_groups(tunnel);
kfree_rcu(tunnel, rcu);
diff --git a/include/net/amt.h b/include/net/amt.h
index 2846dde..ffe231b 100644
--- a/include/net/amt.h
+++ b/include/net/amt.h
@@ -240,6 +240,16 @@ struct amt_tunnel_list {
u32 nr_sources;
enum amt_status status;
struct delayed_work gc_wq;
+ /* Last time this tunnel was refreshed. gc_wq
+ * re-arms itself from this rather than being re-armed by the Update
+ * path, so the Update path cannot re-queue the work of a tunnel that
+ * amt_tunnel_expire() is freeing.
+ */
+ unsigned long last_seen;
+ /* Set under amt->lock and tunnel->lock by whichever freer reaches the
+ * tunnel first; the other then leaves it alone, so it is freed once.
+ */
+ bool dead;
__be16 source_port;
__be32 ip4;
__be32 nonce;
@@ -282,6 +292,8 @@ struct amt_source_node {
#define AMT_SOURCE_OLD 0
#define AMT_SOURCE_NEW 1
u8 flags;
+ /* Protected by gnode->tunnel_list->lock. */
+ bool dead;
struct rcu_head rcu;
};
@@ -296,6 +308,8 @@ struct amt_group_node {
struct amt_tunnel_list *tunnel_list;
struct hlist_node node;
struct delayed_work group_timer;
+ /* Protected by tunnel_list->lock. */
+ bool dead;
struct rcu_head rcu;
struct hlist_head sources[];
};
--
2.43.0
^ permalink raw reply [flat|nested] 11+ messages in thread* Re: [PATCH net v4 1/5] amt: fix use-after-free when a Membership Update re-arms a freed tunnel
2026-10-15 22:06 ` [PATCH net v4 1/5] amt: fix use-after-free when a Membership Update re-arms a freed tunnel Omar Ramadan
@ 2026-10-19 22:39 ` netdev-bot+sashiko
0 siblings, 0 replies; 11+ messages in thread
From: netdev-bot+sashiko @ 2026-10-19 22:39 UTC (permalink / raw)
To: omar
Cc: ap420073, andrew+netdev, davem, edumazet, kuba, pabeni, netdev,
linux-kernel, horms
Thank you for your contribution! Sashiko AI review found 3 potential
issue(s) to consider.
Critical: 0 · High: 1 (1) · Medium: 2 · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [High] In this patch's version, amt_tunnel_expire()
(drivers/net/amt.c:1374-1383) sets tunnel->dead under tunnel->lock.
- [Medium] The commit message only describes tunnel gc_wq,
amt_update_handler(), amt_tunnel_expire() and amt_dev_stop().
- [Medium] In this patch, amt_request_handler()
(drivers/net/amt.c:2783-2791) sets tunnel->last_seen = jiffies and…
Pre-existing issues:
- [High] This problem wasn't introduced by this patch, but the new
gnode->dead/snode->dead checks do not stop a use-after-free when a…
--
amt: fix use-after-free when a Membership Update re-arms a freed tunnel
With this patch, amt_update_handler() only records tunnel->last_seen and no
longer re-arms gc_wq. amt_tunnel_expire() now decides from last_seen,
under amt->lock, whether to re-arm itself or free the tunnel. The patch
also changes amt_dev_stop() to mark tunnels dead under amt->lock and
unlink them one at a time, and it adds dead flags to the tunnel, group
and source nodes.
> diff --git a/drivers/net/amt.c b/drivers/net/amt.c
> index b53f8ec556615..b06916c7d57d5 100644
> --- a/drivers/net/amt.c
> +++ b/drivers/net/amt.c
> @@ -215,9 +215,14 @@ static struct amt_group_node *amt_lookup_group(struct amt_tunnel_list *tunnel,
>
> static void amt_destroy_source(struct amt_source_node *snode)
> {
> - struct amt_group_node *gnode = snode->gnode;
> + struct amt_group_node *gnode;
> struct amt_tunnel_list *tunnel;
>
> + if (READ_ONCE(snode->dead))
> + return;
> + WRITE_ONCE(snode->dead, true);
> + gnode = snode->gnode;
> +
> tunnel = gnode->tunnel_list;
>
> if (!gnode->v6) {
> @@ -249,6 +254,9 @@ static void amt_del_group(struct amt_dev *amt, struct amt_group_node *gnode)
> struct hlist_node *t;
> int i;
>
> + if (READ_ONCE(gnode->dead))
> + return;
> + WRITE_ONCE(gnode->dead, true);
> if (cancel_delayed_work(&gnode->group_timer))
> dev_put(amt->dev);
> hlist_del_rcu(&gnode->node);
[Severity: Medium]
The commit message covers the tunnel gc_wq race and the changes to
amt_update_handler(), amt_tunnel_expire() and amt_dev_stop(). Several hunks
in the diff are outside that description:
- a dead flag added to struct amt_source_node and struct amt_group_node
- amt_destroy_source() and amt_del_group() made idempotent
- amt_source_work() and amt_group_work() restructured: rcu_read_lock()
moved ahead of the gnode reads, the RCU/spinlock nesting swapped, and
new early exits added, one of which calls dev_put(amt->dev) when
gnode->dead is set
Which race do the group/source changes fix? And why is the dev_put() on
the new gnode->dead exit path balanced?
The last patch in the series, "amt: harden relay authentication and
teardown", describes these same changes in its own commit message ("Mark
tunnels, groups, and sources dead under their locks; have group and source
workers hold RCU while finding their owners..."). The code itself goes in
here, though.
Could these hunks either move to that patch or be described in this commit
message?
[ ... ]
> @@ -403,26 +418,35 @@ static void amt_group_work(struct work_struct *work)
> struct amt_group_node *gnode = container_of(to_delayed_work(work),
> struct amt_group_node,
> group_timer);
> - struct amt_tunnel_list *tunnel = gnode->tunnel_list;
> - struct amt_dev *amt = gnode->amt;
> + struct amt_tunnel_list *tunnel;
> + struct amt_dev *amt;
> struct amt_source_node *snode;
> bool delete_group = true;
> struct hlist_node *t;
> int i, buckets;
>
> + rcu_read_lock();
> + tunnel = READ_ONCE(gnode->tunnel_list);
> + amt = READ_ONCE(gnode->amt);
> buckets = amt->hash_buckets;
>
> spin_lock_bh(&tunnel->lock);
> + if (READ_ONCE(gnode->dead)) {
> + spin_unlock_bh(&tunnel->lock);
> + rcu_read_unlock();
> + dev_put(amt->dev);
> + return;
> + }
[Severity: High]
This isn't a bug introduced by this patch, but can the new gnode->dead
check actually protect amt_group_work() from amt_del_group()?
amt_del_group() does a non-sync cancel and then frees the gnode:
if (cancel_delayed_work(&gnode->group_timer))
dev_put(amt->dev);
hlist_del_rcu(&gnode->node);
...
kfree_rcu(gnode, rcu);
process_one_work() clears PENDING before it calls amt_group_work(), so
cancel_delayed_work() returns false and does not wait. The rcu_read_lock()
above is only taken after the callback has started. A kfree_rcu() queued
between the PENDING clear and rcu_read_lock() is therefore not held off.
With CONFIG_PREEMPT_RCU the kworker can be preempted in that window and a
grace period can complete.
In that case the callback reads gnode->tunnel_list, gnode->amt and
gnode->dead from freed memory. It then calls spin_lock_bh() through a stale
tunnel pointer and dev_put() on a stale amt->dev.
A remote gateway can reach amt_del_group() at the same time, for example
with an IGMPv2 Leave through amt_igmpv2_leave_handler() or an MLDv1 Done
sent just as group_timer fires.
amt_source_work() has the same pattern with snode->gnode and
gnode->tunnel_list. __amt_source_gc_work() also frees snodes without
calling cancel_delayed_work_sync() on snode->source_timer.
The flag lives inside the object being freed, so it can't protect the
callback that loads it. Would this need synchronous cancellation, or a
reference that covers the callback's entry?
As far as I can tell, the final patch in the series ("amt: harden relay
authentication and teardown") does not change amt_del_group(),
amt_group_work(), amt_source_work() or __amt_source_gc_work(), so the
problem is still there at the end of the series.
[ ... ]
> @@ -1328,8 +1350,30 @@ static void amt_tunnel_expire(struct work_struct *work)
> struct amt_tunnel_list,
> gc_wq);
> struct amt_dev *amt = tunnel->amt;
> + unsigned long deadline;
>
> spin_lock_bh(&amt->lock);
[ ... ]
> + deadline = READ_ONCE(tunnel->last_seen) +
> + msecs_to_jiffies(amt_gmi(amt)) * 3;
> + if (time_before(jiffies, deadline)) {
> + mod_delayed_work(amt_wq, &tunnel->gc_wq, deadline - jiffies);
> + spin_unlock_bh(&amt->lock);
> + return;
> + }
> + spin_lock_bh(&tunnel->lock);
> + WRITE_ONCE(tunnel->dead, true);
> + spin_unlock_bh(&tunnel->lock);
> rcu_read_lock();
> list_del_rcu(&tunnel->list);
> amt->nr_tunnels--;
[ ... ]
> @@ -2494,9 +2538,13 @@ static bool amt_update_handler(struct amt_dev *amt, struct sk_buff *skb)
> if (tunnel->ip4 == saddr) {
> if ((nonce == tunnel->nonce &&
> response_mac == tunnel->mac)) {
> - mod_delayed_work(amt_wq, &tunnel->gc_wq,
> - msecs_to_jiffies(amt_gmi(amt))
> - * 3);
> + /* Refresh liveness only. gc_wq is
> + * re-armed by amt_tunnel_expire() itself
> + * from last_seen, never from here, so this
> + * cannot re-queue the work of a tunnel
> + * that is being freed.
> + */
> + WRITE_ONCE(tunnel->last_seen, jiffies);
> goto report;
[Severity: High]
Does this fully close the race the commit message describes? The Update
path still holds the tunnel pointer it cached from the RCU walk. After
goto report it does:
amt_update_handler()
spin_lock_bh(&tunnel->lock);
amt_igmp_report_handler(amt, skb, tunnel);
spin_unlock_bh(&tunnel->lock);
The amt_mld_report_handler() path is the same, and neither path checks
tunnel->dead.
last_seen is written here without amt->lock. amt_tunnel_expire() can
therefore read the old value, decide the tunnel has expired, and run:
amt_tunnel_expire()
WRITE_ONCE(tunnel->dead, true);
list_del_rcu(&tunnel->list);
amt_clear_groups(tunnel);
kfree_rcu(tunnel, rcu);
Suppose the Update takes tunnel->lock after amt_clear_groups() has released
it. Could amt_add_group() then link a new gnode into the dying tunnel and
arm group_timer with dev_hold()? For example, in
amt_igmpv2_report_handler():
if (!mod_delayed_work(amt_wq, &gnode->group_timer,
msecs_to_jiffies(amt_gmi(amt))))
dev_hold(amt->dev);
When group_timer fires later, amt_group_work() reads gnode->tunnel_list
and calls spin_lock_bh(&tunnel->lock) on the freed tunnel. The gnode and
the device reference also look leaked.
This is the same class of problem the commit message sets out to close: an
Update holding a cached tunnel pointer arms delayed work that outlives the
tunnel.
The later patch in this series, "amt: harden relay authentication and
teardown", adds a tunnel->dead check after both spin_lock_bh(&tunnel->lock)
calls in amt_update_handler(). This patch carries the Fixes: tag, so would
it make sense to move that check here?
[ ... ]
> @@ -2732,6 +2780,7 @@ static bool amt_request_handler(struct amt_dev *amt, struct sk_buff *skb)
> for (i = 0; i < amt->hash_buckets; i++)
> INIT_HLIST_HEAD(&tunnel->groups[i]);
>
> + tunnel->last_seen = jiffies;
> INIT_DELAYED_WORK(&tunnel->gc_wq, amt_tunnel_expire);
>
> list_add_tail_rcu(&tunnel->list, &amt->tunnel_list);
> @@ -2739,7 +2788,7 @@ static bool amt_request_handler(struct amt_dev *amt, struct sk_buff *skb)
> __amt_update_relay_status(tunnel, AMT_STATUS_RECEIVED_REQUEST, true);
> amt->nr_tunnels++;
> mod_delayed_work(amt_wq, &tunnel->gc_wq,
> - msecs_to_jiffies(amt_gmi(amt)));
> + msecs_to_jiffies(amt_gmi(amt)) * 3);
> spin_unlock_bh(&amt->lock);
[Severity: Medium]
Is this lifetime change intended? Before this patch, a tunnel created by a
Request had gc_wq armed for GMI. Only an authenticated Membership Update
extended it to 3*GMI.
Now the initial arm here and the re-arm in amt_tunnel_expire() both use
last_seen + 3*GMI. A tunnel that never sends an Update therefore lives
3*GMI.
Requests carry no MAC, so spoofed sources can now hold each of the
max_tunnels slots three times as long. The commit message doesn't mention
this change.
The later patch "amt: harden relay authentication and teardown" makes
amt_request_handler() stateless, so tunnels are only created after a
verified Update. That removes the problem in the final tree.
Could this patch keep the old GMI lifetime for tunnels that have never
sent an Update, or at least describe the change in the commit message?
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261015220648.1545576-1-omar%40blockcast.net
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH net v4 2/5] amt: reject a gateway Query or Data from 0.0.0.0 before a relay is learned
2026-10-15 22:06 [PATCH net v4 0/5] amt: relay tunnel lifetime, validation and request-path hardening Omar Ramadan
2026-10-15 22:06 ` [PATCH net v4 1/5] amt: fix use-after-free when a Membership Update re-arms a freed tunnel Omar Ramadan
@ 2026-10-15 22:06 ` Omar Ramadan
2026-10-19 22:39 ` netdev-bot+sashiko
2026-10-15 22:06 ` [PATCH net v4 3/5] amt: require an exact IPv4-sized IFLA_AMT_{LOCAL,DISCOVERY}_IP Omar Ramadan
` (3 subsequent siblings)
5 siblings, 1 reply; 11+ messages in thread
From: Omar Ramadan @ 2026-10-15 22:06 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni
Cc: netdev, linux-kernel, Simon Horman
A gateway's amt_rcv() admits a Membership Query or Multicast Data message
when "saddr != remote_ip" is false. remote_ip is 0 before discovery has
learned a relay (and is reset to 0 by amt_dev_open() and when discovery
gives up), so a packet whose source is 0.0.0.0 -- for example a
0.0.0.0 -> 255.255.255.255 broadcast delivered to the socket -- satisfies
0 != 0 and is accepted. With the nonce freshly reset to 0, a spoofed
Query is then taken as genuine: it sets ready4/qi and the response MAC and
suppresses rediscovery until the discovery timeout, and a spoofed Data
message is injected the same way.
Reject both when no relay is learned: a 0 remote_ip must match nothing, so
guard the check with !remote_ip. Discovery is unaffected -- Relay
Advertisements are validated against discovery_ip, not remote_ip.
Fixes: b9022b53adad ("amt: add control plane of amt interface")
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
diff --git a/drivers/net/amt.c b/drivers/net/amt.c
index b06916c..2e0d3aa 100644
--- a/drivers/net/amt.c
+++ b/drivers/net/amt.c
@@ -2413,6 +2413,10 @@ static bool amt_membership_query_handler(struct amt_dev *amt,
if (amtmq->reserved || amtmq->version)
return true;
+ /* A deferred query may outlive a relay address change. */
+ if (ip_hdr(skb)->saddr != READ_ONCE(amt->remote_ip))
+ return true;
+
if (amtmq->nonce != amt->nonce)
return true;
@@ -2882,7 +2886,7 @@ static int amt_rcv(struct sock *sk, struct sk_buff *skb)
}
goto out;
case AMT_MSG_MULTICAST_DATA:
- if (saddr != remote_ip) {
+ if (!remote_ip || saddr != remote_ip) {
netdev_dbg(amt->dev, "Invalid Relay IP\n");
err = true;
goto drop;
@@ -2893,7 +2897,7 @@ static int amt_rcv(struct sock *sk, struct sk_buff *skb)
else
goto out;
case AMT_MSG_MEMBERSHIP_QUERY:
- if (saddr != remote_ip) {
+ if (!remote_ip || saddr != remote_ip) {
netdev_dbg(amt->dev, "Invalid Relay IP\n");
err = true;
goto drop;
--
2.43.0
^ permalink raw reply [flat|nested] 11+ messages in thread* Re: [PATCH net v4 2/5] amt: reject a gateway Query or Data from 0.0.0.0 before a relay is learned
2026-10-15 22:06 ` [PATCH net v4 2/5] amt: reject a gateway Query or Data from 0.0.0.0 before a relay is learned Omar Ramadan
@ 2026-10-19 22:39 ` netdev-bot+sashiko
0 siblings, 0 replies; 11+ messages in thread
From: netdev-bot+sashiko @ 2026-10-19 22:39 UTC (permalink / raw)
To: omar
Cc: ap420073, andrew+netdev, davem, edumazet, kuba, pabeni, netdev,
linux-kernel, horms
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] The commit message does not mention one of the three hunks,
and that hunk seems to belong to a later patch in the series.
--
amt: reject a gateway Query or Data from 0.0.0.0 before a relay is learned
This commit adds a !remote_ip guard to the Membership Query and Multicast
Data source checks in amt_rcv(). With the guard, a gateway that has not
learned a relay no longer accepts these messages from 0.0.0.0. It also
adds a source re-check in amt_membership_query_handler().
> diff --git a/drivers/net/amt.c b/drivers/net/amt.c
> index b06916c7d57d5..2e0d3aae3759b 100644
> --- a/drivers/net/amt.c
> +++ b/drivers/net/amt.c
> @@ -2413,6 +2413,10 @@ static bool amt_membership_query_handler(struct amt_dev *amt,
> if (amtmq->reserved || amtmq->version)
> return true;
>
> + /* A deferred query may outlive a relay address change. */
> + if (ip_hdr(skb)->saddr != READ_ONCE(amt->remote_ip))
> + return true;
> +
[Severity: Medium]
The commit message doesn't seem to describe this hunk. It only covers the
remote_ip == 0 case in amt_rcv():
"Reject both when no relay is learned: a 0 remote_ip must match
nothing, so guard the check with !remote_ip."
This check covers a different case. amt_rcv() checks the Query against
remote_ip when the packet arrives, and the handler runs later from the
event worker:
amt_rcv()
amt_queue_event(amt, AMT_EVENT_RECEIVE, skb)
...
amt_event_work()
amt_gw_rcv()
amt_membership_query_handler()
In that window, amt_advertisement_handler() can change remote_ip. It can
also be reset to 0 by amt_event_send_request(), amt_dev_open() or
amt_dev_stop(). With this hunk, a Query that passed the check when it was
queued is dropped if the relay address changed before the worker ran.
The later patch in the series, "amt: harden relay authentication and
teardown", says in its commit message:
"A deferred gateway Query is accepted only if its relay address still
matches the current relay"
However, that patch's diff doesn't touch amt_membership_query_handler(),
and it carries a different Fixes: tag (cbc21dc1cfe9).
Did this hunk end up in the wrong patch? This commit has
Fixes: b9022b53adad and is likely to be picked for stable. Could the hunk
move to the patch that describes it, or could this commit message mention
it?
> if (amtmq->nonce != amt->nonce)
> return true;
>
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261015220648.1545576-1-omar%40blockcast.net
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH net v4 3/5] amt: require an exact IPv4-sized IFLA_AMT_{LOCAL,DISCOVERY}_IP
2026-10-15 22:06 [PATCH net v4 0/5] amt: relay tunnel lifetime, validation and request-path hardening Omar Ramadan
2026-10-15 22:06 ` [PATCH net v4 1/5] amt: fix use-after-free when a Membership Update re-arms a freed tunnel Omar Ramadan
2026-10-15 22:06 ` [PATCH net v4 2/5] amt: reject a gateway Query or Data from 0.0.0.0 before a relay is learned Omar Ramadan
@ 2026-10-15 22:06 ` Omar Ramadan
2026-10-15 22:06 ` [PATCH net v4 4/5] amt: make pre-query report drops visible Omar Ramadan
` (2 subsequent siblings)
5 siblings, 0 replies; 11+ messages in thread
From: Omar Ramadan @ 2026-10-15 22:06 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni
Cc: netdev, linux-kernel, Simon Horman
The netlink policy for IFLA_AMT_LOCAL_IP and IFLA_AMT_DISCOVERY_IP is an
untyped entry with .len set to the size of an IPv4 address, which netlink
validation treats as a minimum length only. Any longer payload passes,
and nla_get_in_addr() then reads just its first four bytes: an iproute2
that puts a 16-byte IPv6 literal in one of these IPv4 attributes turns
"2001:db8::" into 32.1.13.184, and the device is created with an address
the user never asked for.
Use NLA_POLICY_EXACT_LEN(sizeof(__be32)) for both, so every payload that
is not exactly four bytes long is refused with a standard extack instead
of being truncated. The range check this expands to fails the attribute
even under the liberal parsing rtnetlink applies to IFLA_INFO_DATA.
amt_get_size() and amt_fill_info() already emit exactly four bytes for
these attributes. IFLA_AMT_REMOTE_IP is only ever reported, never read
from a request, so it is left unchanged.
Fixes: b9022b53adad ("amt: add control plane of amt interface")
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
diff --git a/drivers/net/amt.c b/drivers/net/amt.c
index 2e0d3aa..470dbbd 100644
--- a/drivers/net/amt.c
+++ b/drivers/net/amt.c
@@ -3230,9 +3230,9 @@ static const struct nla_policy amt_policy[IFLA_AMT_MAX + 1] = {
[IFLA_AMT_RELAY_PORT] = { .type = NLA_U16 },
[IFLA_AMT_GATEWAY_PORT] = { .type = NLA_U16 },
[IFLA_AMT_LINK] = { .type = NLA_U32 },
- [IFLA_AMT_LOCAL_IP] = { .len = sizeof_field(struct iphdr, daddr) },
+ [IFLA_AMT_LOCAL_IP] = NLA_POLICY_EXACT_LEN(sizeof(__be32)),
[IFLA_AMT_REMOTE_IP] = { .len = sizeof_field(struct iphdr, daddr) },
- [IFLA_AMT_DISCOVERY_IP] = { .len = sizeof_field(struct iphdr, daddr) },
+ [IFLA_AMT_DISCOVERY_IP] = NLA_POLICY_EXACT_LEN(sizeof(__be32)),
[IFLA_AMT_MAX_TUNNELS] = { .type = NLA_U32 },
};
--
2.43.0
^ permalink raw reply [flat|nested] 11+ messages in thread* [PATCH net v4 4/5] amt: make pre-query report drops visible
2026-10-15 22:06 [PATCH net v4 0/5] amt: relay tunnel lifetime, validation and request-path hardening Omar Ramadan
` (2 preceding siblings ...)
2026-10-15 22:06 ` [PATCH net v4 3/5] amt: require an exact IPv4-sized IFLA_AMT_{LOCAL,DISCOVERY}_IP Omar Ramadan
@ 2026-10-15 22:06 ` Omar Ramadan
2026-10-15 22:06 ` [PATCH net v4 5/5] amt: harden relay authentication and teardown Omar Ramadan
2026-10-15 22:14 ` [PATCH net v4 0/5] amt: relay tunnel lifetime, validation and request-path hardening netdev-bot+sinfo
5 siblings, 0 replies; 11+ messages in thread
From: Omar Ramadan @ 2026-10-15 22:06 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni
Cc: netdev, linux-kernel, Simon Horman
A gateway cannot forward an IGMP or MLD report until it has received the
relay's Membership Query for that family. The Query supplies the
response MAC the Update echoes, so dropping an early report is required,
but doing so silently leaves operators with a dark multicast path and no
indication why the join never happened.
Emit a family-specific debug message before taking the drop path.
tx_dropped could not show these drops either: amt_dev_xmit() reaches the
same tx_dropped increment after amt_send_membership_update() has handed
the report to the tunnel, so in gateway mode every forwarded report was
also counted as a drop. Return NETDEV_TX_OK directly once the report has
been sent, so tx_dropped counts only reports that were discarded.
The device is LLTX, so amt_dev_xmit() can run on several CPUs at once;
use DEV_STATS_INC() for its tx_dropped and tx_errors updates so
concurrent increments are not lost.
Fixes: cbc21dc1cfe9 ("amt: add data plane of amt interface")
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
diff --git a/drivers/net/amt.c b/drivers/net/amt.c
index 470dbbd..c6924ab 100644
--- a/drivers/net/amt.c
+++ b/drivers/net/amt.c
@@ -650,7 +650,7 @@ static void amt_send_discovery(struct amt_dev *amt)
IPPROTO_UDP, 0,
amt->stream_dev->ifindex);
if (IS_ERR(rt)) {
- amt->dev->stats.tx_errors++;
+ DEV_STATS_INC(amt->dev, tx_errors);
goto out;
}
@@ -660,7 +660,7 @@ static void amt_send_discovery(struct amt_dev *amt)
skb = netdev_alloc_skb_ip_align(amt->dev, len);
if (!skb) {
ip_rt_put(rt);
- amt->dev->stats.tx_errors++;
+ DEV_STATS_INC(amt->dev, tx_errors);
goto out;
}
@@ -705,7 +705,7 @@ static void amt_send_discovery(struct amt_dev *amt)
ip_send_check(iph);
err = ip_local_out(amt->net, sk, skb);
if (unlikely(net_xmit_eval(err)))
- amt->dev->stats.tx_errors++;
+ DEV_STATS_INC(amt->dev, tx_errors);
amt_update_gw_status(amt, AMT_STATUS_SENT_DISCOVERY, true);
out:
@@ -741,7 +741,7 @@ static void amt_send_request(struct amt_dev *amt, bool v6)
IPPROTO_UDP, 0,
amt->stream_dev->ifindex);
if (IS_ERR(rt)) {
- amt->dev->stats.tx_errors++;
+ DEV_STATS_INC(amt->dev, tx_errors);
goto out;
}
@@ -751,7 +751,7 @@ static void amt_send_request(struct amt_dev *amt, bool v6)
skb = netdev_alloc_skb_ip_align(amt->dev, len);
if (!skb) {
ip_rt_put(rt);
- amt->dev->stats.tx_errors++;
+ DEV_STATS_INC(amt->dev, tx_errors);
goto out;
}
@@ -798,7 +798,7 @@ static void amt_send_request(struct amt_dev *amt, bool v6)
ip_send_check(iph);
err = ip_local_out(amt->net, sk, skb);
if (unlikely(net_xmit_eval(err)))
- amt->dev->stats.tx_errors++;
+ DEV_STATS_INC(amt->dev, tx_errors);
out:
rcu_read_unlock();
@@ -820,7 +820,7 @@ static void amt_send_igmp_gq(struct amt_dev *amt,
skb_pull(skb, sizeof(struct ethhdr));
if (amt_send_membership_query(amt, skb, tunnel, false)) {
- amt->dev->stats.tx_dropped++;
+ DEV_STATS_INC(amt->dev, tx_dropped);
kfree_skb(skb);
}
}
@@ -864,7 +864,7 @@ static struct sk_buff *amt_build_mld_gq(struct amt_dev *amt)
if (ipv6_dev_get_saddr(amt->net, amt->dev, &ip6h->daddr, 0,
&ip6h->saddr)) {
- amt->dev->stats.tx_errors++;
+ DEV_STATS_INC(amt->dev, tx_errors);
kfree_skb(skb);
return NULL;
}
@@ -908,7 +908,7 @@ static void amt_send_mld_gq(struct amt_dev *amt, struct amt_tunnel_list *tunnel)
skb_pull(skb, sizeof(struct ethhdr));
if (amt_send_membership_query(amt, skb, tunnel, true)) {
- amt->dev->stats.tx_dropped++;
+ DEV_STATS_INC(amt->dev, tx_dropped);
kfree_skb(skb);
}
}
@@ -1258,7 +1258,7 @@ static netdev_tx_t amt_dev_xmit(struct sk_buff *skb, struct net_device *dev)
group.ip6 = ipv6_hdr(skb)->daddr;
#endif
} else {
- dev->stats.tx_errors++;
+ DEV_STATS_INC(dev, tx_errors);
goto free;
}
@@ -1271,12 +1271,21 @@ static netdev_tx_t amt_dev_xmit(struct sk_buff *skb, struct net_device *dev)
/* Gateway only passes IGMP/MLD packets */
if (!report)
goto free;
+ /* A validated report can only be forwarded after the relay's
+ * family-specific Membership Query supplies the state echoed
+ * by the Membership Update. Log this readiness failure before
+ * the shared drop path accounts it.
+ */
if ((!v6 && !READ_ONCE(amt->ready4)) ||
- (v6 && !READ_ONCE(amt->ready6)))
+ (v6 && !READ_ONCE(amt->ready6))) {
+ netdev_dbg(dev, "drop %s report: waiting for Membership Query\n",
+ v6 ? "MLD" : "IGMP");
goto free;
+ }
if (amt_send_membership_update(amt, skb, v6))
goto free;
- goto unlock;
+ /* Sent: the tunnel consumed the skb. Not a drop. */
+ return NETDEV_TX_OK;
} else if (amt->mode == AMT_MODE_RELAY) {
if (!data)
goto free;
@@ -1305,8 +1314,7 @@ found:
return NETDEV_TX_OK;
free:
dev_kfree_skb(skb);
-unlock:
- dev->stats.tx_dropped++;
+ DEV_STATS_INC(dev, tx_dropped);
return NETDEV_TX_OK;
}
@@ -2653,7 +2661,7 @@ static void amt_send_advertisement(struct amt_dev *amt, __be32 nonce,
IPPROTO_UDP, 0,
amt->stream_dev->ifindex);
if (IS_ERR(rt)) {
- amt->dev->stats.tx_errors++;
+ DEV_STATS_INC(amt->dev, tx_errors);
goto out;
}
@@ -2663,7 +2671,7 @@ static void amt_send_advertisement(struct amt_dev *amt, __be32 nonce,
skb = netdev_alloc_skb_ip_align(amt->dev, len);
if (!skb) {
ip_rt_put(rt);
- amt->dev->stats.tx_errors++;
+ DEV_STATS_INC(amt->dev, tx_errors);
goto out;
}
@@ -2709,7 +2717,7 @@ static void amt_send_advertisement(struct amt_dev *amt, __be32 nonce,
ip_send_check(iph);
err = ip_local_out(amt->net, sk, skb);
if (unlikely(net_xmit_eval(err)))
- amt->dev->stats.tx_errors++;
+ DEV_STATS_INC(amt->dev, tx_errors);
out:
rcu_read_unlock();
--
2.43.0
^ permalink raw reply [flat|nested] 11+ messages in thread* [PATCH net v4 5/5] amt: harden relay authentication and teardown
2026-10-15 22:06 [PATCH net v4 0/5] amt: relay tunnel lifetime, validation and request-path hardening Omar Ramadan
` (3 preceding siblings ...)
2026-10-15 22:06 ` [PATCH net v4 4/5] amt: make pre-query report drops visible Omar Ramadan
@ 2026-10-15 22:06 ` Omar Ramadan
2026-10-19 22:39 ` netdev-bot+sashiko
2026-10-15 22:14 ` [PATCH net v4 0/5] amt: relay tunnel lifetime, validation and request-path hardening netdev-bot+sinfo
5 siblings, 1 reply; 11+ messages in thread
From: Omar Ramadan @ 2026-10-15 22:06 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni
Cc: netdev, linux-kernel, Simon Horman
Keep relay Requests stateless: derive the Membership Query MAC from the
source address, source port, nonce, and current key, then authenticate the
Membership Update before decapsulating it or creating tunnel state. Key tunnel
state by the (address, port) endpoint and reject new endpoints at the global
or per-source cap without reflecting ICMP. Keep four key generations so a
gateway can renew its MAC across rotations.
Make tunnel and multicast state teardown safe against concurrent RCU readers
and delayed workers. Mark tunnels, groups, and sources dead under their locks;
have group and source workers hold RCU while finding their owners and checking
that state; and release the group timer's device reference on every exit path.
Recheck tunnel liveness before a Membership Update can arm group work.
A deferred gateway Query is accepted only if its relay address still matches
the current relay, and transmit error/drop counters use the concurrent-safe
netdev statistics helpers.
Fixes: cbc21dc1cfe9 ("amt: add data plane of amt interface")
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
diff --git a/drivers/net/amt.c b/drivers/net/amt.c
index c6924ab..ba2734c 100644
--- a/drivers/net/amt.c
+++ b/drivers/net/amt.c
@@ -309,8 +309,8 @@ static void amt_source_work(struct work_struct *work)
if (!gnode->nr_sources)
amt_del_group(amt, gnode);
} else {
- /* When a router filter-mode for a group is EXCLUDE,
- * source records are only deleted when the group timer expires
+ /* When a router filter-mode for the group is EXCLUDE, source
+ * records are only deleted when the group timer expires.
*/
snode->status = AMT_SOURCE_STATUS_D_FWD;
}
@@ -806,11 +806,16 @@ out:
static bool amt_send_membership_query(struct amt_dev *amt,
struct sk_buff *skb,
- struct amt_tunnel_list *tunnel,
+ __be32 daddr, __be16 dport,
+ __be32 nonce, u64 mac,
bool v6);
-static void amt_send_igmp_gq(struct amt_dev *amt,
- struct amt_tunnel_list *tunnel)
+/* The destination is passed by value rather than as a tunnel: the requesting
+ * source is not authenticated yet and no tunnel state exists for it - see
+ * amt_request_handler().
+ */
+static void amt_send_igmp_gq(struct amt_dev *amt, __be32 daddr, __be16 dport,
+ __be32 nonce, u64 mac)
{
struct sk_buff *skb;
@@ -819,7 +824,8 @@ static void amt_send_igmp_gq(struct amt_dev *amt,
return;
skb_pull(skb, sizeof(struct ethhdr));
- if (amt_send_membership_query(amt, skb, tunnel, false)) {
+ if (amt_send_membership_query(amt, skb, daddr, dport, nonce, mac,
+ false)) {
DEV_STATS_INC(amt->dev, tx_dropped);
kfree_skb(skb);
}
@@ -898,7 +904,8 @@ static struct sk_buff *amt_build_mld_gq(struct amt_dev *amt)
return skb;
}
-static void amt_send_mld_gq(struct amt_dev *amt, struct amt_tunnel_list *tunnel)
+static void amt_send_mld_gq(struct amt_dev *amt, __be32 daddr, __be16 dport,
+ __be32 nonce, u64 mac)
{
struct sk_buff *skb;
@@ -907,13 +914,15 @@ static void amt_send_mld_gq(struct amt_dev *amt, struct amt_tunnel_list *tunnel)
return;
skb_pull(skb, sizeof(struct ethhdr));
- if (amt_send_membership_query(amt, skb, tunnel, true)) {
+ if (amt_send_membership_query(amt, skb, daddr, dport, nonce, mac,
+ true)) {
DEV_STATS_INC(amt->dev, tx_dropped);
kfree_skb(skb);
}
}
#else
-static void amt_send_mld_gq(struct amt_dev *amt, struct amt_tunnel_list *tunnel)
+static void amt_send_mld_gq(struct amt_dev *amt, __be32 daddr, __be16 dport,
+ __be32 nonce, u64 mac)
{
}
#endif
@@ -947,7 +956,14 @@ static void amt_secret_work(struct work_struct *work)
secret_wq);
spin_lock_bh(&amt->lock);
- get_random_bytes(&amt->key, sizeof(siphash_key_t));
+ /* Keep three previous generations for a 180-240s MAC lifetime.
+ * Its 180s floor exceeds the advertised QQIC (amt->qi, 125s),
+ * allowing the gateway to renew its MAC with a periodic Request.
+ */
+ amt->key[3] = amt->key[2];
+ amt->key[2] = amt->key[1];
+ amt->key[1] = amt->key[0];
+ get_random_bytes(&amt->key[0], sizeof(siphash_key_t));
spin_unlock_bh(&amt->lock);
mod_delayed_work(amt_wq, &amt->secret_wq,
msecs_to_jiffies(AMT_SECRET_TIMEOUT));
@@ -1137,9 +1153,16 @@ static void amt_send_multicast_data(struct amt_dev *amt,
0);
}
+/* Send a Membership Query to a source that has not been authenticated yet.
+ *
+ * No per-source state exists at this point and none is created: the nonce and
+ * the keyed MAC are carried in the Query and recomputed from the Membership
+ * Update when it comes back, so everything this needs is passed by value.
+ */
static bool amt_send_membership_query(struct amt_dev *amt,
struct sk_buff *skb,
- struct amt_tunnel_list *tunnel,
+ __be32 daddr, __be16 dport,
+ __be32 nonce, u64 mac,
bool v6)
{
struct amt_header_membership_query *amtmq;
@@ -1160,13 +1183,13 @@ static bool amt_send_membership_query(struct amt_dev *amt,
skb_reset_inner_headers(skb);
memset(&fl4, 0, sizeof(struct flowi4));
fl4.flowi4_oif = amt->stream_dev->ifindex;
- fl4.daddr = tunnel->ip4;
+ fl4.daddr = daddr;
fl4.saddr = amt->local_ip;
fl4.flowi4_dscp = inet_dsfield_to_dscp(AMT_TOS);
fl4.flowi4_proto = IPPROTO_UDP;
rt = ip_route_output_key(amt->net, &fl4);
if (IS_ERR(rt)) {
- netdev_dbg(amt->dev, "no route to %pI4\n", &tunnel->ip4);
+ netdev_dbg(amt->dev, "no route to %pI4\n", &daddr);
return true;
}
@@ -1176,8 +1199,8 @@ static bool amt_send_membership_query(struct amt_dev *amt,
amtmq->reserved = 0;
amtmq->l = 0;
amtmq->g = 0;
- amtmq->nonce = tunnel->nonce;
- amtmq->response_mac = tunnel->mac;
+ amtmq->nonce = nonce;
+ amtmq->response_mac = mac;
if (!v6)
skb_set_inner_protocol(skb, htons(ETH_P_IP));
@@ -1190,11 +1213,10 @@ static bool amt_send_membership_query(struct amt_dev *amt,
ip4_dst_hoplimit(&rt->dst),
0,
amt->relay_port,
- tunnel->source_port,
+ dport,
false,
false,
0);
- amt_update_relay_status(tunnel, AMT_STATUS_SENT_QUERY, true);
return false;
}
@@ -2517,16 +2539,106 @@ static bool amt_membership_query_handler(struct amt_dev *amt,
return false;
}
+/* The tunnel of the gateway endpoint @saddr:@sport, or NULL. */
+static struct amt_tunnel_list *amt_tunnel_lookup(struct amt_dev *amt,
+ __be32 saddr, __be16 sport)
+{
+ struct amt_tunnel_list *tunnel;
+
+ list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list)
+ if (tunnel->ip4 == saddr && tunnel->source_port == sport)
+ return tunnel;
+ return NULL;
+}
+
+/* Look up the tunnel for a source whose Membership Update has already been
+ * authenticated, creating it on first contact.
+ *
+ * This is now the only place tunnel state is allocated, so amt->max_tunnels
+ * bounds the number of *verified* gateways rather than the number of
+ * unverified Requests anyone can send. A fixed per-source cap additionally
+ * stops one verified source address from monopolising the table.
+ */
+static struct amt_tunnel_list *amt_tunnel_get_or_create(struct amt_dev *amt,
+ __be32 saddr,
+ __be16 sport)
+{
+ struct amt_tunnel_list *tunnel;
+ int per_src = 0;
+ int i;
+
+ tunnel = amt_tunnel_lookup(amt, saddr, sport);
+ if (tunnel)
+ return tunnel;
+
+ spin_lock_bh(&amt->lock);
+
+ /* Re-check under the lock in case a concurrent Update won the race, and
+ * count how many tunnels this source already holds on the same walk.
+ */
+ list_for_each_entry(tunnel, &amt->tunnel_list, list) {
+ if (tunnel->ip4 != saddr)
+ continue;
+ if (tunnel->source_port == sport) {
+ spin_unlock_bh(&amt->lock);
+ return tunnel;
+ }
+ per_src++;
+ }
+
+ if (amt->nr_tunnels >= amt->max_tunnels ||
+ per_src >= AMT_MAX_TUNNELS_PER_SRC) {
+ spin_unlock_bh(&amt->lock);
+ return NULL;
+ }
+
+ if (!netif_running(amt->dev)) {
+ spin_unlock_bh(&amt->lock);
+ return NULL;
+ }
+
+ tunnel = kzalloc(sizeof(*tunnel) +
+ (sizeof(struct hlist_head) * amt->hash_buckets),
+ GFP_ATOMIC);
+ if (!tunnel) {
+ spin_unlock_bh(&amt->lock);
+ return NULL;
+ }
+
+ tunnel->source_port = sport;
+ tunnel->ip4 = saddr;
+ tunnel->amt = amt;
+ spin_lock_init(&tunnel->lock);
+ for (i = 0; i < amt->hash_buckets; i++)
+ INIT_HLIST_HEAD(&tunnel->groups[i]);
+
+ tunnel->last_seen = jiffies;
+ INIT_DELAYED_WORK(&tunnel->gc_wq, amt_tunnel_expire);
+ __amt_update_relay_status(tunnel, AMT_STATUS_RECEIVED_UPDATE, false);
+
+ /* Publish only once the entry is fully initialised. */
+ list_add_tail_rcu(&tunnel->list, &amt->tunnel_list);
+ amt->nr_tunnels++;
+ mod_delayed_work(amt_wq, &tunnel->gc_wq,
+ msecs_to_jiffies(amt_gmi(amt)) * 3);
+ spin_unlock_bh(&amt->lock);
+
+ return tunnel;
+}
+
static bool amt_update_handler(struct amt_dev *amt, struct sk_buff *skb)
{
struct amt_header_membership_update *amtmu;
struct amt_tunnel_list *tunnel;
struct ethhdr *eth;
struct iphdr *iph;
- int len, hdr_size;
+ siphash_key_t key[4];
+ int len, hdr_size, i;
u64 response_mac;
+ bool verified = false;
__be32 saddr;
__be32 nonce;
+ __be16 sport;
saddr = ip_hdr(skb)->saddr;
@@ -2540,34 +2652,58 @@ static bool amt_update_handler(struct amt_dev *amt, struct sk_buff *skb)
nonce = amtmu->nonce;
response_mac = amtmu->response_mac;
+ /* Snapshot the tunnel endpoint port before the encap is stripped. */
+ sport = udp_hdr(skb)->source;
- if (iptunnel_pull_header(skb, hdr_size, skb->protocol, false))
- return true;
+ /* Recompute the MAC handed out in the Membership Query. Four live
+ * secrets cover 180-240s, exceeding the advertised QQIC (amt->qi,
+ * 125s), so a periodic Request can renew the MAC before it expires.
+ * Authenticate before decapsulation or allocating tunnel state.
+ */
+ spin_lock_bh(&amt->lock);
+ for (i = 0; i < ARRAY_SIZE(key); i++)
+ key[i] = amt->key[i];
+ spin_unlock_bh(&amt->lock);
- skb_reset_network_header(skb);
+ for (i = 0; i < ARRAY_SIZE(key); i++) {
+ u64 mac = siphash_3u32((__force u32)saddr,
+ (__force u32)sport,
+ (__force u32)nonce,
+ &key[i]) >> 16;
- list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list) {
- if (tunnel->ip4 == saddr) {
- if ((nonce == tunnel->nonce &&
- response_mac == tunnel->mac)) {
- /* Refresh liveness only. gc_wq is
- * re-armed by amt_tunnel_expire() itself
- * from last_seen, never from here, so this
- * cannot re-queue the work of a tunnel
- * that is being freed.
- */
- WRITE_ONCE(tunnel->last_seen, jiffies);
- goto report;
- } else {
- netdev_dbg(amt->dev, "Invalid MAC\n");
- return true;
- }
+ if (response_mac == mac) {
+ verified = true;
+ break;
}
}
- return true;
+ if (!verified) {
+ netdev_dbg(amt->dev, "Invalid MAC\n");
+ return true;
+ }
+
+ tunnel = amt_tunnel_get_or_create(amt, saddr, sport);
+ if (!tunnel) {
+ /* Out of tunnel slots. Unlike the Request path this reply only
+ * ever goes to a source that proved it received our Query,
+ * so it
+ * cannot be used to reflect at a spoofed third party.
+ */
+ icmp_ndo_send(skb, ICMP_DEST_UNREACH, ICMP_HOST_UNREACH, 0);
+ return true;
+ }
+
+ /* Refresh liveness only. gc_wq is re-armed by amt_tunnel_expire()
+ * itself from last_seen, never from here, so this cannot re-queue the
+ * work of a tunnel that is being freed.
+ */
+ WRITE_ONCE(tunnel->last_seen, jiffies);
+
+ if (iptunnel_pull_header(skb, hdr_size, skb->protocol, false))
+ return true;
+
+ skb_reset_network_header(skb);
-report:
if (!pskb_may_pull(skb, sizeof(*iph)))
return true;
@@ -2582,6 +2718,10 @@ report:
}
spin_lock_bh(&tunnel->lock);
+ if (READ_ONCE(tunnel->dead)) {
+ spin_unlock_bh(&tunnel->lock);
+ return true;
+ }
amt_igmp_report_handler(amt, skb, tunnel);
spin_unlock_bh(&tunnel->lock);
@@ -2602,6 +2742,10 @@ report:
}
spin_lock_bh(&tunnel->lock);
+ if (READ_ONCE(tunnel->dead)) {
+ spin_unlock_bh(&tunnel->lock);
+ return true;
+ }
amt_mld_report_handler(amt, skb, tunnel);
spin_unlock_bh(&tunnel->lock);
@@ -2744,15 +2888,29 @@ static bool amt_discovery_handler(struct amt_dev *amt, struct sk_buff *skb)
return false;
}
+/* Handle an AMT Relay Membership Request.
+ *
+ * The source of a Request is unauthenticated: it is a bare UDP datagram with a
+ * trivially spoofable source address. Allocating tunnel state here let an
+ * attacker fill the table from spoofed addresses, and let one spoofed packet
+ * overwrite the nonce/MAC of an established tunnel and cut that gateway off.
+ *
+ * Nothing needs to be remembered at this point. Every input to the keyed MAC
+ * is carried in the packet or is device state, so the MAC is generated here,
+ * echoed back by the gateway in its Membership Update, and recomputed and
+ * verified there - see amt_update_handler(). Tunnel state is created only once
+ * that verification succeeds.
+ */
static bool amt_request_handler(struct amt_dev *amt, struct sk_buff *skb)
{
struct amt_header_request *amtrh;
struct amt_tunnel_list *tunnel;
- unsigned long long key;
struct udphdr *udph;
struct iphdr *iph;
+ siphash_key_t key;
+ bool full, have = false;
+ int per_src = 0;
u64 mac;
- int i;
if (!pskb_may_pull(skb, sizeof(*udph) + sizeof(*amtrh)))
return true;
@@ -2764,60 +2922,40 @@ static bool amt_request_handler(struct amt_dev *amt, struct sk_buff *skb)
if (amtrh->reserved1 || amtrh->reserved2 || amtrh->version)
return true;
- list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list)
- if (tunnel->ip4 == iph->saddr)
- goto send;
-
- spin_lock_bh(&amt->lock);
- if (amt->nr_tunnels >= amt->max_tunnels) {
- spin_unlock_bh(&amt->lock);
- icmp_ndo_send(skb, ICMP_DEST_UNREACH, ICMP_HOST_UNREACH, 0);
+ if (!netif_running(amt->dev) || !netif_running(amt->stream_dev))
return true;
- }
- tunnel = kzalloc(sizeof(*tunnel) +
- (sizeof(struct hlist_head) * amt->hash_buckets),
- GFP_ATOMIC);
- if (!tunnel) {
- spin_unlock_bh(&amt->lock);
- return true;
+ /* Snapshot the secret and tunnel limits on the same locked walk. */
+ spin_lock_bh(&amt->lock);
+ key = amt->key[0];
+ full = amt->nr_tunnels >= amt->max_tunnels;
+ list_for_each_entry(tunnel, &amt->tunnel_list, list) {
+ if (tunnel->ip4 != iph->saddr)
+ continue;
+ per_src++;
+ if (tunnel->source_port == udph->source)
+ have = true;
}
-
- tunnel->source_port = udph->source;
- tunnel->ip4 = iph->saddr;
-
- memcpy(&key, &tunnel->key, sizeof(unsigned long long));
- tunnel->amt = amt;
- spin_lock_init(&tunnel->lock);
- for (i = 0; i < amt->hash_buckets; i++)
- INIT_HLIST_HEAD(&tunnel->groups[i]);
-
- tunnel->last_seen = jiffies;
- INIT_DELAYED_WORK(&tunnel->gc_wq, amt_tunnel_expire);
-
- list_add_tail_rcu(&tunnel->list, &amt->tunnel_list);
- tunnel->key = amt->key;
- __amt_update_relay_status(tunnel, AMT_STATUS_RECEIVED_REQUEST, true);
- amt->nr_tunnels++;
- mod_delayed_work(amt_wq, &tunnel->gc_wq,
- msecs_to_jiffies(amt_gmi(amt)) * 3);
spin_unlock_bh(&amt->lock);
-send:
- tunnel->nonce = amtrh->nonce;
- mac = siphash_3u32((__force u32)tunnel->ip4,
- (__force u32)tunnel->source_port,
- (__force u32)tunnel->nonce,
- &tunnel->key);
- tunnel->mac = mac >> 16;
-
- if (!netif_running(amt->dev) || !netif_running(amt->stream_dev))
+ /* Withhold the Query when a new endpoint cannot get a tunnel, so the
+ * gateway rediscovers. Existing endpoints must still renew their MAC.
+ * Drop silently: an unauthenticated Request must not reflect ICMP.
+ */
+ if (!have && (full || per_src >= AMT_MAX_TUNNELS_PER_SRC))
return true;
+ mac = siphash_3u32((__force u32)iph->saddr,
+ (__force u32)udph->source,
+ (__force u32)amtrh->nonce,
+ &key) >> 16;
+
if (!amtrh->p)
- amt_send_igmp_gq(amt, tunnel);
+ amt_send_igmp_gq(amt, iph->saddr, udph->source, amtrh->nonce,
+ mac);
else
- amt_send_mld_gq(amt, tunnel);
+ amt_send_mld_gq(amt, iph->saddr, udph->source, amtrh->nonce,
+ mac);
return false;
}
@@ -3085,6 +3223,14 @@ static int amt_dev_open(struct net_device *dev)
enable_delayed_work(&amt->discovery_wq);
enable_delayed_work(&amt->req_wq);
+ /* Seed the keys before amt_socket_create() enables the encap receive
+ * path, so a Request or Update that arrives immediately is never
+ * verified against an all-zero (predictable) secret.
+ */
+ spin_lock_bh(&amt->lock);
+ get_random_bytes(&amt->key, sizeof(amt->key));
+ spin_unlock_bh(&amt->lock);
+
err = amt_socket_create(amt);
if (err) {
disable_delayed_work(&amt->req_wq);
@@ -3095,7 +3241,6 @@ static int amt_dev_open(struct net_device *dev)
amt->req_cnt = 0;
WRITE_ONCE(amt->remote_ip, 0);
amt->nonce = 0;
- get_random_bytes(&amt->key, sizeof(siphash_key_t));
amt->status = AMT_STATUS_INIT;
if (amt->mode == AMT_MODE_GATEWAY) {
diff --git a/include/net/amt.h b/include/net/amt.h
index ffe231b..1b4283e 100644
--- a/include/net/amt.h
+++ b/include/net/amt.h
@@ -242,20 +242,16 @@ struct amt_tunnel_list {
struct delayed_work gc_wq;
/* Last time this tunnel was refreshed. gc_wq
* re-arms itself from this rather than being re-armed by the Update
- * path, so the Update path cannot re-queue the work of a tunnel that
- * amt_tunnel_expire() is freeing.
+ * path, so the Update path cannot re-queue work for a tunnel that
+ * amt_tunnel_expire() is freeing on the timeout path.
*/
unsigned long last_seen;
- /* Set under amt->lock and tunnel->lock by whichever freer reaches the
- * tunnel first; the other then leaves it alone, so it is freed once.
+ /* Set under amt->lock by whichever freer reaches the tunnel first;
+ * the other then leaves it alone, so the tunnel is freed exactly once.
*/
bool dead;
__be16 source_port;
__be32 ip4;
- __be32 nonce;
- siphash_key_t key;
- u64 mac:48,
- reserved:16;
struct rcu_head rcu;
struct hlist_head groups[];
};
@@ -339,8 +335,10 @@ struct amt_dev {
struct work_struct event_wq;
/* AMT status */
enum amt_status status;
- /* Generated key */
- siphash_key_t key;
+ /* Current key and three previous generations keep MACs valid for
+ * 180-240s, beyond the advertised QQIC (see amt_secret_work()).
+ */
+ siphash_key_t key[4];
struct sock __rcu *sk;
u32 max_groups;
u32 max_sources;
@@ -397,6 +395,10 @@ struct amt_dev {
#define AMT_SECRET_TIMEOUT 60000
#define IANA_AMT_UDP_PORT 2268
#define AMT_MAX_TUNNELS 128
+/* Fixed per-source cap stops one verified source address from monopolising
+ * the AMT_MAX_TUNNELS-slot table.
+ */
+#define AMT_MAX_TUNNELS_PER_SRC 16
#define AMT_MAX_REQS 128
#define AMT_GW_HLEN (sizeof(struct iphdr) + \
sizeof(struct udphdr) + \
--
2.43.0
^ permalink raw reply [flat|nested] 11+ messages in thread* Re: [PATCH net v4 5/5] amt: harden relay authentication and teardown
2026-10-15 22:06 ` [PATCH net v4 5/5] amt: harden relay authentication and teardown Omar Ramadan
@ 2026-10-19 22:39 ` netdev-bot+sashiko
0 siblings, 0 replies; 11+ messages in thread
From: netdev-bot+sashiko @ 2026-10-19 22:39 UTC (permalink / raw)
To: omar
Cc: ap420073, andrew+netdev, davem, edumazet, kuba, pabeni, netdev,
linux-kernel, horms
Thank you for your contribution! Sashiko AI review found 5 potential
issue(s) to consider.
Critical: 0 · High: 1 · Medium: 2 · Low: 2
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [High] The commit message claims tunnel and multicast-state teardown is
now "safe against concurrent RCU readers and delayed workers" because…
- [Medium] Much of the commit message describes work that this commit
does not contain.
- [Medium] Every unauthenticated AMT Request now takes the device-wide
amt->lock with BH disabled and walks the entire amt->tunnel_list while…
- [Low] The patch rewrites the comment on struct amt_tunnel_list::dead in
include/net/amt.h.
- [Low] amt_update_handler() sends ICMP_DEST_UNREACH/ICMP_HOST_UNREACH to
the gateway whenever amt_tunnel_get_or_create() returns NULL, and its…
--
amt: harden relay authentication and teardown
This commit makes relay Membership Requests stateless. The Query MAC is
derived from the source address, port, nonce and a rotating key, and the
Membership Update is authenticated before any tunnel state is created.
Tunnels are now keyed by (address, port) with a per-source cap, and four
key generations are kept so a gateway can renew its MAC across rotations.
> Keep relay Requests stateless: derive the Membership Query MAC from the
> source address, source port, nonce, and current key, then authenticate the
> Membership Update before decapsulating it or creating tunnel state. Key tunnel
> state by the (address, port) endpoint and reject new endpoints at the global
> or per-source cap without reflecting ICMP. Keep four key generations so a
> gateway can renew its MAC across rotations.
>
> Make tunnel and multicast state teardown safe against concurrent RCU readers
> and delayed workers. Mark tunnels, groups, and sources dead under their locks;
> have group and source workers hold RCU while finding their owners and checking
> that state; and release the group timer's device reference on every exit path.
> Recheck tunnel liveness before a Membership Update can arm group work.
>
> A deferred gateway Query is accepted only if its relay address still matches
> the current relay, and transmit error/drop counters use the concurrent-safe
> netdev statistics helpers.
[Severity: Medium]
Does this message match what the patch contains?
Compared with the parent commit ("amt: make pre-query report drops
visible"), most of the teardown work in the second paragraph is not in
this patch:
- amt_source_work() only gets a comment rewording.
- amt_group_work() is not modified.
- The only new liveness check is the tunnel->dead recheck in
amt_update_handler().
The dead flags on groups and sources, the rcu_read_lock() in the group
and source workers, and the dev_put() on every amt_group_work() exit path
appear to come from the earlier commit "amt: fix use-after-free when a
Membership Update re-arms a freed tunnel".
The third paragraph also describes code that already exists. The parent
already has this in amt_membership_query_handler():
/* A deferred query may outlive a relay address change. */
if (ip_hdr(skb)->saddr != READ_ONCE(amt->remote_ip))
return true;
The tx_errors/tx_dropped sites there already use DEV_STATS_INC() as well.
The first paragraph says new endpoints at the cap are rejected "without
reflecting ICMP". However, amt_update_handler() still calls
icmp_ndo_send() when amt_tunnel_get_or_create() returns NULL.
The message also doesn't describe the bug being fixed. Before this patch,
one spoofed Request with an established gateway's source address
overwrote tunnel->nonce and tunnel->mac and cut that gateway off. Spoofed
Requests could also fill max_tunnels. Right now this is only explained in
the comment above amt_request_handler().
Could the message be reworded to describe what this patch actually
changes and which problem it fixes?
> diff --git a/drivers/net/amt.c b/drivers/net/amt.c
> index c6924ab40f223..ba2734c672f78 100644
> --- a/drivers/net/amt.c
> +++ b/drivers/net/amt.c
> @@ -309,8 +309,8 @@ static void amt_source_work(struct work_struct *work)
> if (!gnode->nr_sources)
> amt_del_group(amt, gnode);
> } else {
> - /* When a router filter-mode for a group is EXCLUDE,
> - * source records are only deleted when the group timer expires
> + /* When a router filter-mode for the group is EXCLUDE, source
> + * records are only deleted when the group timer expires.
> */
> snode->status = AMT_SOURCE_STATUS_D_FWD;
> }
[Severity: High]
The commit message says teardown is now safe against delayed workers
because the group and source workers hold RCU while finding their owners.
Does calling rcu_read_lock() inside the work function close that window?
amt_source_work() starts like this:
rcu_read_lock();
gnode = READ_ONCE(snode->gnode);
amt = READ_ONCE(gnode->amt);
tunnel = READ_ONCE(gnode->tunnel_list);
spin_lock_bh(&tunnel->lock);
if (READ_ONCE(snode->dead) || READ_ONCE(gnode->dead)) {
amt_group_work() does the same with gnode->tunnel_list and gnode->amt.
By the time either function runs, process_one_work() has already cleared
the pending bit. The freers use non-sync cancels, so they don't wait for
a work that is already running:
amt_del_group()
if (cancel_delayed_work(&gnode->group_timer)) <- false if running
dev_put(amt->dev);
hlist_del_rcu(&gnode->node);
...
kfree_rcu(gnode, rcu);
The other freers follow the same pattern:
- amt_destroy_source() calls cancel_delayed_work(&snode->source_timer)
and hands snode to __amt_source_gc_work(), which frees it with
kfree_rcu().
- amt_tunnel_expire() calls amt_clear_groups() and then
kfree_rcu(tunnel).
- amt_dev_stop() does the same, but synchronously cancels only
tunnel->gc_wq.
On a preemptible RCU kernel, a kworker preempted after the dequeue but
before its rcu_read_lock() does not hold up the grace period:
kworker other CPU
process_one_work()
clears pending bit
<preempted>
amt_update_handler() leave path
amt_del_group()
cancel_delayed_work() == false
kfree_rcu(gnode, rcu)
grace period ends, gnode freed
amt_group_work()
rcu_read_lock()
tunnel = READ_ONCE(gnode->tunnel_list) <- freed memory
spin_lock_bh(&tunnel->lock)
Can gnode, snode or the tunnel be freed before the worker dereferences
them? The dead checks only run after those loads. The dev_hold() taken
for the group timer keeps amt->dev alive, but not gnode or the tunnel.
The worker code itself isn't changed here, but the message claims this
class is now handled. Would the freers need to wait for these works (a
sync cancel outside the spinlocks), or the work hold a reference on
gnode and the tunnel?
[ ... ]
> @@ -2540,34 +2652,58 @@ static bool amt_update_handler(struct amt_dev *amt, struct sk_buff *skb)
[ ... ]
> + tunnel = amt_tunnel_get_or_create(amt, saddr, sport);
> + if (!tunnel) {
> + /* Out of tunnel slots. Unlike the Request path this reply only
> + * ever goes to a source that proved it received our Query,
> + * so it
> + * cannot be used to reflect at a spoofed third party.
> + */
> + icmp_ndo_send(skb, ICMP_DEST_UNREACH, ICMP_HOST_UNREACH, 0);
> + return true;
> + }
[Severity: Low]
Is every NULL return from amt_tunnel_get_or_create() really "out of
tunnel slots"? It also returns NULL when the device isn't running and
when the GFP_ATOMIC allocation fails:
if (!netif_running(amt->dev)) {
spin_unlock_bh(&amt->lock);
return NULL;
}
tunnel = kzalloc(sizeof(*tunnel) + ...
GFP_ATOMIC);
if (!tunnel) {
spin_unlock_bh(&amt->lock);
return NULL;
}
In those cases a verified gateway still gets ICMP_HOST_UNREACH. This
includes an Update that arrives after __dev_close_many() clears
__LINK_STATE_START but before amt_dev_stop() releases the socket.
On the gateway side, amt_err_lookup() handles that ICMP like this:
case AMT_MSG_MEMBERSHIP_UPDATE:
if (READ_ONCE(amt->status) >= AMT_STATUS_RECEIVED_ADVERTISEMENT)
mod_delayed_work(amt_wq, &amt->req_wq, 0);
amt_event_send_request() then runs with req_cnt == 0, which clears
ready4/ready6 and picks a new nonce. Reports are dropped until a new
Query arrives.
Under sustained atomic allocation pressure, can this become a
Request/Query/Update/ICMP cycle paced by the round-trip time? The old
Request path sent ICMP only for the capacity case and dropped silently
when kzalloc() failed.
[ ... ]
> @@ -2764,60 +2922,40 @@ static bool amt_request_handler(struct amt_dev *amt, struct sk_buff *skb)
> if (amtrh->reserved1 || amtrh->reserved2 || amtrh->version)
> return true;
>
> - list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list)
> - if (tunnel->ip4 == iph->saddr)
> - goto send;
> -
> - spin_lock_bh(&amt->lock);
> - if (amt->nr_tunnels >= amt->max_tunnels) {
> - spin_unlock_bh(&amt->lock);
> - icmp_ndo_send(skb, ICMP_DEST_UNREACH, ICMP_HOST_UNREACH, 0);
> + if (!netif_running(amt->dev) || !netif_running(amt->stream_dev))
> return true;
> - }
>
> - tunnel = kzalloc(sizeof(*tunnel) +
> - (sizeof(struct hlist_head) * amt->hash_buckets),
> - GFP_ATOMIC);
> - if (!tunnel) {
> - spin_unlock_bh(&amt->lock);
> - return true;
> + /* Snapshot the secret and tunnel limits on the same locked walk. */
> + spin_lock_bh(&amt->lock);
> + key = amt->key[0];
> + full = amt->nr_tunnels >= amt->max_tunnels;
> + list_for_each_entry(tunnel, &amt->tunnel_list, list) {
> + if (tunnel->ip4 != iph->saddr)
> + continue;
> + per_src++;
> + if (tunnel->source_port == udph->source)
> + have = true;
> }
[ ... ]
> spin_unlock_bh(&amt->lock);
[Severity: Medium]
Every unauthenticated Request now takes the device-wide amt->lock with BH
disabled and walks the whole tunnel_list while holding it. Because the
walk counts per_src, it doesn't stop at a match.
Before this patch, an existing endpoint was found with a lockless
list_for_each_entry_rcu() walk, and the locked section for new sources
was O(1).
amt_update_handler() now also takes the same lock for every Update,
forged ones included, just to copy the key array before checking the
MAC:
spin_lock_bh(&amt->lock);
for (i = 0; i < ARRAY_SIZE(key); i++)
key[i] = amt->key[i];
spin_unlock_bh(&amt->lock);
Can a flood of spoofed Requests serialize all RX CPUs on amt->lock, with
the hold time growing with nr_tunnels? max_tunnels comes from
IFLA_AMT_MAX_TUNNELS with no upper bound. Legitimate tunnel creation,
Update key snapshots and amt_tunnel_expire() all contend on the same
lock.
amt_tunnel_get_or_create() already rechecks both caps under the lock.
Could the per_src/have scan here be a lockless RCU walk instead, and the
key read use a seqcount or an RCU-published key array?
[ ... ]
> diff --git a/include/net/amt.h b/include/net/amt.h
> index ffe231b87ec28..1b4283efa0fc8 100644
> --- a/include/net/amt.h
> +++ b/include/net/amt.h
> @@ -242,20 +242,16 @@ struct amt_tunnel_list {
[ ... ]
> - /* Set under amt->lock and tunnel->lock by whichever freer reaches the
> - * tunnel first; the other then leaves it alone, so it is freed once.
> + /* Set under amt->lock by whichever freer reaches the tunnel first;
> + * the other then leaves it alone, so the tunnel is freed exactly once.
> */
> bool dead;
[Severity: Low]
Should this comment still mention tunnel->lock? Both writers still set
the flag with tunnel->lock nested inside amt->lock. In
amt_tunnel_expire() and amt_dev_stop():
spin_lock_bh(&tunnel->lock);
WRITE_ONCE(tunnel->dead, true);
spin_unlock_bh(&tunnel->lock);
The new checks this patch adds to amt_update_handler() hold only
tunnel->lock:
spin_lock_bh(&tunnel->lock);
if (READ_ONCE(tunnel->dead)) {
These readers are serialized against teardown only because the writers
also take tunnel->lock, and the rewritten comment no longer says that.
If a later change follows the comment and drops the inner tunnel->lock,
won't the liveness recheck in amt_update_handler() quietly stop working?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261015220648.1545576-1-omar%40blockcast.net
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH net v4 0/5] amt: relay tunnel lifetime, validation and request-path hardening
2026-10-15 22:06 [PATCH net v4 0/5] amt: relay tunnel lifetime, validation and request-path hardening Omar Ramadan
` (4 preceding siblings ...)
2026-10-15 22:06 ` [PATCH net v4 5/5] amt: harden relay authentication and teardown Omar Ramadan
@ 2026-10-15 22:14 ` netdev-bot+sinfo
5 siblings, 0 replies; 11+ messages in thread
From: netdev-bot+sinfo @ 2026-10-15 22:14 UTC (permalink / raw)
To: Omar Ramadan
Cc: Taehee Yoo, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, netdev, linux-kernel, Simon Horman
Hi!
This is an automated message. This series looks like a fix, but its
commit messages seem to be missing some information:
- Whether the issue was actually triggered, or is only theoretical
(e.g. found by code inspection). If it was triggered please include
the symptoms, like the stack trace or error messages.
Please do not repost the series just to address the above. Instead,
reply to this email with the missing information, so that reviewers
can take it into account. If the series needs another revision for
other reasons, please include the information in the commit messages
then.
The evaluation is done by an LLM so it may be wrong, if you think
that is the case please reply and explain.
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH net v4 5/5] amt: harden relay authentication and teardown
2026-10-20 0:25 Omar Ramadan
@ 2026-10-20 0:25 ` Omar Ramadan
0 siblings, 0 replies; 11+ messages in thread
From: Omar Ramadan @ 2026-10-20 0:25 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni
Cc: netdev, linux-kernel, Simon Horman
An AMT Request has an unauthenticated, spoofable source address. Allocating
tunnel state from that path allowed spoofed Requests to consume tunnel
slots
or overwrite the nonce and MAC of an established gateway.
Keep Requests stateless by deriving the Query MAC from the source address,
source port, nonce, and current secret. Authenticate the Membership Update
before decapsulation or allocation, key verified state by the address/port
endpoint, enforce global and per-source limits, and silently reject an
endpoint that cannot be admitted. Retain four secret generations so a
gateway can renew its MAC across rotations.
Before a verified Update reaches the IGMP or MLD report handlers, recheck
tunnel liveness under tunnel->lock so it cannot add multicast state after
teardown has started.
Fixes: cbc21dc1cfe9 ("amt: add data plane of amt interface")
Assisted-by: LLM
---
drivers/net/amt.c | 300 +++++++++++++++++++++++++++++++++-------------
include/net/amt.h | 21 ++--
2 files changed, 228 insertions(+), 93 deletions(-)
Signed-off-by: Omar Ramadan <omar@blockcast.net>
diff --git a/drivers/net/amt.c b/drivers/net/amt.c
index f83afba..faf3297 100644
--- a/drivers/net/amt.c
+++ b/drivers/net/amt.c
@@ -860,11 +860,16 @@ out:
static bool amt_send_membership_query(struct amt_dev *amt,
struct sk_buff *skb,
- struct amt_tunnel_list *tunnel,
+ __be32 daddr, __be16 dport,
+ __be32 nonce, u64 mac,
bool v6);
-static void amt_send_igmp_gq(struct amt_dev *amt,
- struct amt_tunnel_list *tunnel)
+/* The destination is passed by value rather than as a tunnel: the requesting
+ * source is not authenticated yet and no tunnel state exists for it - see
+ * amt_request_handler().
+ */
+static void amt_send_igmp_gq(struct amt_dev *amt, __be32 daddr, __be16 dport,
+ __be32 nonce, u64 mac)
{
struct sk_buff *skb;
@@ -873,7 +878,8 @@ static void amt_send_igmp_gq(struct amt_dev *amt,
return;
skb_pull(skb, sizeof(struct ethhdr));
- if (amt_send_membership_query(amt, skb, tunnel, false)) {
+ if (amt_send_membership_query(amt, skb, daddr, dport, nonce, mac,
+ false)) {
DEV_STATS_INC(amt->dev, tx_dropped);
kfree_skb(skb);
}
@@ -952,7 +958,8 @@ static struct sk_buff *amt_build_mld_gq(struct amt_dev *amt)
return skb;
}
-static void amt_send_mld_gq(struct amt_dev *amt, struct amt_tunnel_list *tunnel)
+static void amt_send_mld_gq(struct amt_dev *amt, __be32 daddr, __be16 dport,
+ __be32 nonce, u64 mac)
{
struct sk_buff *skb;
@@ -961,13 +968,15 @@ static void amt_send_mld_gq(struct amt_dev *amt, struct amt_tunnel_list *tunnel)
return;
skb_pull(skb, sizeof(struct ethhdr));
- if (amt_send_membership_query(amt, skb, tunnel, true)) {
+ if (amt_send_membership_query(amt, skb, daddr, dport, nonce, mac,
+ true)) {
DEV_STATS_INC(amt->dev, tx_dropped);
kfree_skb(skb);
}
}
#else
-static void amt_send_mld_gq(struct amt_dev *amt, struct amt_tunnel_list *tunnel)
+static void amt_send_mld_gq(struct amt_dev *amt, __be32 daddr, __be16 dport,
+ __be32 nonce, u64 mac)
{
}
#endif
@@ -1001,7 +1010,14 @@ static void amt_secret_work(struct work_struct *work)
secret_wq);
spin_lock_bh(&amt->lock);
- get_random_bytes(&amt->key, sizeof(siphash_key_t));
+ /* Keep three previous generations for a 180-240s MAC lifetime.
+ * Its 180s floor exceeds the advertised QQIC (amt->qi, 125s),
+ * allowing the gateway to renew its MAC with a periodic Request.
+ */
+ amt->key[3] = amt->key[2];
+ amt->key[2] = amt->key[1];
+ amt->key[1] = amt->key[0];
+ get_random_bytes(&amt->key[0], sizeof(siphash_key_t));
spin_unlock_bh(&amt->lock);
mod_delayed_work(amt_wq, &amt->secret_wq,
msecs_to_jiffies(AMT_SECRET_TIMEOUT));
@@ -1191,9 +1207,16 @@ static void amt_send_multicast_data(struct amt_dev *amt,
0);
}
+/* Send a Membership Query to a source that has not been authenticated yet.
+ *
+ * No per-source state exists at this point and none is created: the nonce and
+ * the keyed MAC are carried in the Query and recomputed from the Membership
+ * Update when it comes back, so everything this needs is passed by value.
+ */
static bool amt_send_membership_query(struct amt_dev *amt,
struct sk_buff *skb,
- struct amt_tunnel_list *tunnel,
+ __be32 daddr, __be16 dport,
+ __be32 nonce, u64 mac,
bool v6)
{
struct amt_header_membership_query *amtmq;
@@ -1214,13 +1237,13 @@ static bool amt_send_membership_query(struct amt_dev *amt,
skb_reset_inner_headers(skb);
memset(&fl4, 0, sizeof(struct flowi4));
fl4.flowi4_oif = amt->stream_dev->ifindex;
- fl4.daddr = tunnel->ip4;
+ fl4.daddr = daddr;
fl4.saddr = amt->local_ip;
fl4.flowi4_dscp = inet_dsfield_to_dscp(AMT_TOS);
fl4.flowi4_proto = IPPROTO_UDP;
rt = ip_route_output_key(amt->net, &fl4);
if (IS_ERR(rt)) {
- netdev_dbg(amt->dev, "no route to %pI4\n", &tunnel->ip4);
+ netdev_dbg(amt->dev, "no route to %pI4\n", &daddr);
return true;
}
@@ -1230,8 +1253,8 @@ static bool amt_send_membership_query(struct amt_dev *amt,
amtmq->reserved = 0;
amtmq->l = 0;
amtmq->g = 0;
- amtmq->nonce = tunnel->nonce;
- amtmq->response_mac = tunnel->mac;
+ amtmq->nonce = nonce;
+ amtmq->response_mac = mac;
if (!v6)
skb_set_inner_protocol(skb, htons(ETH_P_IP));
@@ -1244,11 +1267,10 @@ static bool amt_send_membership_query(struct amt_dev *amt,
ip4_dst_hoplimit(&rt->dst),
0,
amt->relay_port,
- tunnel->source_port,
+ dport,
false,
false,
0);
- amt_update_relay_status(tunnel, AMT_STATUS_SENT_QUERY, true);
return false;
}
@@ -2576,16 +2598,106 @@ static bool amt_membership_query_handler(struct amt_dev *amt,
return false;
}
+/* The tunnel of the gateway endpoint @saddr:@sport, or NULL. */
+static struct amt_tunnel_list *amt_tunnel_lookup(struct amt_dev *amt,
+ __be32 saddr, __be16 sport)
+{
+ struct amt_tunnel_list *tunnel;
+
+ list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list)
+ if (tunnel->ip4 == saddr && tunnel->source_port == sport)
+ return tunnel;
+ return NULL;
+}
+
+/* Look up the tunnel for a source whose Membership Update has already been
+ * authenticated, creating it on first contact.
+ *
+ * This is now the only place tunnel state is allocated, so amt->max_tunnels
+ * bounds the number of *verified* gateways rather than the number of
+ * unverified Requests anyone can send. A fixed per-source cap additionally
+ * stops one verified source address from monopolising the table.
+ */
+static struct amt_tunnel_list *amt_tunnel_get_or_create(struct amt_dev *amt,
+ __be32 saddr,
+ __be16 sport)
+{
+ struct amt_tunnel_list *tunnel;
+ int per_src = 0;
+ int i;
+
+ tunnel = amt_tunnel_lookup(amt, saddr, sport);
+ if (tunnel)
+ return tunnel;
+
+ spin_lock_bh(&amt->lock);
+
+ /* Re-check under the lock in case a concurrent Update won the race, and
+ * count how many tunnels this source already holds on the same walk.
+ */
+ list_for_each_entry(tunnel, &amt->tunnel_list, list) {
+ if (tunnel->ip4 != saddr)
+ continue;
+ if (tunnel->source_port == sport) {
+ spin_unlock_bh(&amt->lock);
+ return tunnel;
+ }
+ per_src++;
+ }
+
+ if (amt->nr_tunnels >= amt->max_tunnels ||
+ per_src >= AMT_MAX_TUNNELS_PER_SRC) {
+ spin_unlock_bh(&amt->lock);
+ return NULL;
+ }
+
+ if (!netif_running(amt->dev)) {
+ spin_unlock_bh(&amt->lock);
+ return NULL;
+ }
+
+ tunnel = kzalloc(sizeof(*tunnel) +
+ (sizeof(struct hlist_head) * amt->hash_buckets),
+ GFP_ATOMIC);
+ if (!tunnel) {
+ spin_unlock_bh(&amt->lock);
+ return NULL;
+ }
+
+ tunnel->source_port = sport;
+ tunnel->ip4 = saddr;
+ tunnel->amt = amt;
+ spin_lock_init(&tunnel->lock);
+ for (i = 0; i < amt->hash_buckets; i++)
+ INIT_HLIST_HEAD(&tunnel->groups[i]);
+
+ tunnel->last_seen = jiffies;
+ INIT_DELAYED_WORK(&tunnel->gc_wq, amt_tunnel_expire);
+ __amt_update_relay_status(tunnel, AMT_STATUS_RECEIVED_UPDATE, false);
+
+ /* Publish only once the entry is fully initialised. */
+ list_add_tail_rcu(&tunnel->list, &amt->tunnel_list);
+ amt->nr_tunnels++;
+ mod_delayed_work(amt_wq, &tunnel->gc_wq,
+ msecs_to_jiffies(amt_gmi(amt)) * 3);
+ spin_unlock_bh(&amt->lock);
+
+ return tunnel;
+}
+
static bool amt_update_handler(struct amt_dev *amt, struct sk_buff *skb)
{
struct amt_header_membership_update *amtmu;
struct amt_tunnel_list *tunnel;
struct ethhdr *eth;
struct iphdr *iph;
- int len, hdr_size;
+ siphash_key_t key[4];
+ int len, hdr_size, i;
u64 response_mac;
+ bool verified = false;
__be32 saddr;
__be32 nonce;
+ __be16 sport;
saddr = ip_hdr(skb)->saddr;
@@ -2599,34 +2711,51 @@ static bool amt_update_handler(struct amt_dev *amt, struct sk_buff *skb)
nonce = amtmu->nonce;
response_mac = amtmu->response_mac;
+ /* Snapshot the tunnel endpoint port before the encap is stripped. */
+ sport = udp_hdr(skb)->source;
- if (iptunnel_pull_header(skb, hdr_size, skb->protocol, false))
- return true;
+ /* Recompute the MAC handed out in the Membership Query. Four live
+ * secrets cover 180-240s, exceeding the advertised QQIC (amt->qi,
+ * 125s), so a periodic Request can renew the MAC before it expires.
+ * Authenticate before decapsulation or allocating tunnel state.
+ */
+ spin_lock_bh(&amt->lock);
+ for (i = 0; i < ARRAY_SIZE(key); i++)
+ key[i] = amt->key[i];
+ spin_unlock_bh(&amt->lock);
- skb_reset_network_header(skb);
+ for (i = 0; i < ARRAY_SIZE(key); i++) {
+ u64 mac = siphash_3u32((__force u32)saddr,
+ (__force u32)sport,
+ (__force u32)nonce,
+ &key[i]) >> 16;
- list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list) {
- if (tunnel->ip4 == saddr) {
- if ((nonce == tunnel->nonce &&
- response_mac == tunnel->mac)) {
- /* Refresh liveness only. gc_wq is
- * re-armed by amt_tunnel_expire() itself
- * from last_seen, never from here, so this
- * cannot re-queue the work of a tunnel
- * that is being freed.
- */
- WRITE_ONCE(tunnel->last_seen, jiffies);
- goto report;
- } else {
- netdev_dbg(amt->dev, "Invalid MAC\n");
- return true;
- }
+ if (response_mac == mac) {
+ verified = true;
+ break;
}
}
- return true;
+ if (!verified) {
+ netdev_dbg(amt->dev, "Invalid MAC\n");
+ return true;
+ }
+
+ tunnel = amt_tunnel_get_or_create(amt, saddr, sport);
+ if (!tunnel)
+ return true;
+
+ /* Refresh liveness only. gc_wq is re-armed by amt_tunnel_expire()
+ * itself from last_seen, never from here, so this cannot re-queue the
+ * work of a tunnel that is being freed.
+ */
+ WRITE_ONCE(tunnel->last_seen, jiffies);
+
+ if (iptunnel_pull_header(skb, hdr_size, skb->protocol, false))
+ return true;
+
+ skb_reset_network_header(skb);
-report:
if (!pskb_may_pull(skb, sizeof(*iph)))
return true;
@@ -2811,15 +2940,29 @@ static bool amt_discovery_handler(struct amt_dev *amt, struct sk_buff *skb)
return false;
}
+/* Handle an AMT Relay Membership Request.
+ *
+ * The source of a Request is unauthenticated: it is a bare UDP datagram with a
+ * trivially spoofable source address. Allocating tunnel state here let an
+ * attacker fill the table from spoofed addresses, and let one spoofed packet
+ * overwrite the nonce/MAC of an established tunnel and cut that gateway off.
+ *
+ * Nothing needs to be remembered at this point. Every input to the keyed MAC
+ * is carried in the packet or is device state, so the MAC is generated here,
+ * echoed back by the gateway in its Membership Update, and recomputed and
+ * verified there - see amt_update_handler(). Tunnel state is created only once
+ * that verification succeeds.
+ */
static bool amt_request_handler(struct amt_dev *amt, struct sk_buff *skb)
{
struct amt_header_request *amtrh;
struct amt_tunnel_list *tunnel;
- unsigned long long key;
struct udphdr *udph;
struct iphdr *iph;
+ siphash_key_t key;
+ bool full, have = false;
+ int per_src = 0;
u64 mac;
- int i;
if (!pskb_may_pull(skb, sizeof(*udph) + sizeof(*amtrh)))
return true;
@@ -2831,59 +2974,41 @@ static bool amt_request_handler(struct amt_dev *amt, struct sk_buff *skb)
if (amtrh->reserved1 || amtrh->reserved2 || amtrh->version)
return true;
- list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list)
- if (tunnel->ip4 == iph->saddr)
- goto send;
-
- spin_lock_bh(&amt->lock);
- if (amt->nr_tunnels >= amt->max_tunnels) {
- spin_unlock_bh(&amt->lock);
- icmp_ndo_send(skb, ICMP_DEST_UNREACH, ICMP_HOST_UNREACH, 0);
- return true;
- }
-
- tunnel = kzalloc(sizeof(*tunnel) +
- (sizeof(struct hlist_head) * amt->hash_buckets),
- GFP_ATOMIC);
- if (!tunnel) {
- spin_unlock_bh(&amt->lock);
+ if (!netif_running(amt->dev) || !netif_running(amt->stream_dev))
return true;
- }
-
- tunnel->source_port = udph->source;
- tunnel->ip4 = iph->saddr;
-
- memcpy(&key, &tunnel->key, sizeof(unsigned long long));
- tunnel->amt = amt;
- spin_lock_init(&tunnel->lock);
- for (i = 0; i < amt->hash_buckets; i++)
- INIT_HLIST_HEAD(&tunnel->groups[i]);
-
- INIT_DELAYED_WORK(&tunnel->gc_wq, amt_tunnel_expire);
- list_add_tail_rcu(&tunnel->list, &amt->tunnel_list);
- tunnel->key = amt->key;
- __amt_update_relay_status(tunnel, AMT_STATUS_RECEIVED_REQUEST, true);
- amt->nr_tunnels++;
- mod_delayed_work(amt_wq, &tunnel->gc_wq,
- msecs_to_jiffies(amt_gmi(amt)));
+ /* Snapshot the secret and limits under the lock; keep the endpoint walk
+ * out of the lock so spoofed Requests cannot serialize relay updates.
+ */
+ spin_lock_bh(&amt->lock);
+ key = amt->key[0];
+ full = amt->nr_tunnels >= amt->max_tunnels;
spin_unlock_bh(&amt->lock);
-send:
- tunnel->nonce = amtrh->nonce;
- mac = siphash_3u32((__force u32)tunnel->ip4,
- (__force u32)tunnel->source_port,
- (__force u32)tunnel->nonce,
- &tunnel->key);
- tunnel->mac = mac >> 16;
+ rcu_read_lock();
+ list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list) {
+ if (tunnel->ip4 != iph->saddr)
+ continue;
+ per_src++;
+ if (tunnel->source_port == udph->source)
+ have = true;
+ }
+ rcu_read_unlock();
- if (!netif_running(amt->dev) || !netif_running(amt->stream_dev))
+ /* Withhold the Query when a new endpoint cannot get a tunnel. */
+ if (!have && (full || per_src >= AMT_MAX_TUNNELS_PER_SRC))
return true;
+ mac = siphash_3u32((__force u32)iph->saddr,
+ (__force u32)udph->source,
+ (__force u32)amtrh->nonce,
+ &key) >> 16;
if (!amtrh->p)
- amt_send_igmp_gq(amt, tunnel);
+ amt_send_igmp_gq(amt, iph->saddr, udph->source, amtrh->nonce,
+ mac);
else
- amt_send_mld_gq(amt, tunnel);
+ amt_send_mld_gq(amt, iph->saddr, udph->source, amtrh->nonce,
+ mac);
return false;
}
@@ -3151,6 +3276,14 @@ static int amt_dev_open(struct net_device *dev)
enable_delayed_work(&amt->discovery_wq);
enable_delayed_work(&amt->req_wq);
+ /* Seed the keys before amt_socket_create() enables the encap receive
+ * path, so a Request or Update that arrives immediately is never
+ * verified against an all-zero (predictable) secret.
+ */
+ spin_lock_bh(&amt->lock);
+ get_random_bytes(&amt->key, sizeof(amt->key));
+ spin_unlock_bh(&amt->lock);
+
err = amt_socket_create(amt);
if (err) {
disable_delayed_work(&amt->req_wq);
@@ -3161,7 +3294,6 @@ static int amt_dev_open(struct net_device *dev)
amt->req_cnt = 0;
WRITE_ONCE(amt->remote_ip, 0);
amt->nonce = 0;
- get_random_bytes(&amt->key, sizeof(siphash_key_t));
amt->status = AMT_STATUS_INIT;
if (amt->mode == AMT_MODE_GATEWAY) {
diff --git a/include/net/amt.h b/include/net/amt.h
index 123725c..4acfc3d 100644
--- a/include/net/amt.h
+++ b/include/net/amt.h
@@ -242,20 +242,17 @@ struct amt_tunnel_list {
struct delayed_work gc_wq;
/* Last time this tunnel was refreshed. gc_wq
* re-arms itself from this rather than being re-armed by the Update
- * path, so the Update path cannot re-queue the work of a tunnel that
- * amt_tunnel_expire() is freeing.
+ * path, so the Update path cannot re-queue work for a tunnel that
+ * amt_tunnel_expire() is freeing on the timeout path.
*/
unsigned long last_seen;
/* Set under amt->lock and tunnel->lock by whichever freer reaches the
- * tunnel first; the other then leaves it alone, so it is freed once.
+ * tunnel first;
+ * the other then leaves it alone, so the tunnel is freed exactly once.
*/
bool dead;
__be16 source_port;
__be32 ip4;
- __be32 nonce;
- siphash_key_t key;
- u64 mac:48,
- reserved:16;
struct rcu_head rcu;
struct hlist_head groups[];
};
@@ -341,8 +338,10 @@ struct amt_dev {
struct work_struct event_wq;
/* AMT status */
enum amt_status status;
- /* Generated key */
- siphash_key_t key;
+ /* Current key and three previous generations keep MACs valid for
+ * 180-240s, beyond the advertised QQIC (see amt_secret_work()).
+ */
+ siphash_key_t key[4];
struct sock __rcu *sk;
u32 max_groups;
u32 max_sources;
@@ -399,6 +398,10 @@ struct amt_dev {
#define AMT_SECRET_TIMEOUT 60000
#define IANA_AMT_UDP_PORT 2268
#define AMT_MAX_TUNNELS 128
+/* Fixed per-source cap stops one verified source address from monopolising
+ * the AMT_MAX_TUNNELS-slot table.
+ */
+#define AMT_MAX_TUNNELS_PER_SRC 16
#define AMT_MAX_REQS 128
#define AMT_GW_HLEN (sizeof(struct iphdr) + \
sizeof(struct udphdr) + \
^ permalink raw reply [flat|nested] 11+ messages in thread