From: Ibrahim Hashimov <security@auditcode.ai>
To: Marek Lindner <marek.lindner@mailbox.org>,
Simon Wunderlich <sw@simonwunderlich.de>,
Antonio Quartulli <antonio@mandelbit.com>,
Sven Eckelmann <sven@narfation.org>
Cc: b.a.t.m.a.n@lists.open-mesh.org, linux-kernel@vger.kernel.org,
stable@vger.kernel.org
Subject: [PATCH net] batman-adv: bound BLA claim and backbone gateway table growth
Date: Fri, 10 Jul 2026 18:52:24 +0200 [thread overview]
Message-ID: <20260710165224.39411-1-security@auditcode.ai> (raw)
batadv_bla_add_claim() and batadv_bla_get_backbone_gw() each kzalloc a
new hash entry (struct batadv_bla_claim / struct batadv_bla_backbone_gw)
for every distinct (mac, vid) / (orig, vid) pair carried in a
BLA-group-conforming CLAIM frame, and insert it into
bat_priv->bla.claim_hash / backbone_hash. There is no maximum-entry
cap on either table -- entries are only ever removed by the
timeout-driven periodic purge (batadv_bla_purge_claims() /
batadv_bla_purge_backbone_gw(), BATADV_BLA_CLAIM_TIMEOUT /
BATADV_BLA_BACKBONE_TIMEOUT, on the order of 100 s).
The BLA group a frame must carry is htons(crc16(primary_hardif_mac)),
which is trivially observable/derivable by any node already on the
mesh soft-interface (batadv_check_claim_group()). A single on-mesh
sender can therefore emit CLAIM frames with an incrementing source MAC
and force one kzalloc(GFP_ATOMIC) per frame on both hot paths, growing
kernel memory without bound for as long as the attacker keeps sending
-- uncontrolled resource allocation. Both allocations are
GFP_ATOMIC with a NULL check, so this is a graceful memory-pressure
DoS, not a crash: there is no OOB access.
batman-adv already has an established pattern for capping an
attacker-/peer-influenced, unbounded-growth per-mesh-interface
resource: the TP-meter session list bounds concurrent sessions with a
fixed ceiling and an atomic_add_unless() admission check that rejects
new allocations once the cap is hit, logging and freeing/decrementing
on the abort path (net/batman-adv/tp_meter.c, BATADV_TP_MAX_NUM,
bat_priv->tp_num, "Meter: too many ongoing sessions, aborting").
Apply the same pattern to BLA: add two atomic_t counters,
bat_priv->bla.num_claims and bat_priv->bla.num_backbone_gws, each
capped at a new BATADV_BLA_MAX_CLAIMS / BATADV_BLA_MAX_BACKBONE_GW
limit (4096 / 256 -- generous for any real bridged LAN/VLAN
population, several orders of magnitude below what would need to be
sprayed to threaten memory availability). batadv_bla_add_claim() and
batadv_bla_get_backbone_gw() reserve a slot with atomic_add_unless()
before allocating; on cap-hit the frame is dropped (matching existing
"drop silently, let the sender resync/backoff" BLA behaviour) instead
of allocating. The reservation is released on every existing early-out
(kzalloc failure, hash_add failure) and in the kref release paths
(batadv_claim_release(), batadv_backbone_gw_release()), where the
counters are decremented right before the objects are freed. No
locking changes are needed: the counters are only ever touched via
atomic ops, mirroring tp_num.
This does not change on-the-wire behaviour, hash table sizing, or
timeout-based purging; it only stops a single on-mesh peer from
growing the tables past a bounded ceiling.
Verified by code review rather than by driving either counter to its
cap at runtime: the atomic_add_unless()/atomic_dec() pairing was
checked against every existing early-out (kzalloc failure, hash_add
failure) and against both kref release callbacks, confirming exactly
one reservation and one release per entry, mirroring the same
tp_num accounting in tp_meter.c. A loopback CLAIM-frame reproducer
was used earlier to confirm the pre-fix unbounded growth itself
(distinct claim_hash/backbone_hash entries scale linearly with the
number of distinct (mac, vid) pairs sent), but reaching the new
4096 / 256 caps with that same reproducer is impractical: entries
age out via the existing timeout-driven purge faster than a
single-host reproducer can accumulate enough distinct pairs to hit
the ceiling, so the cap-hit and slot-release paths were exercised
by inspection, not by a live saturation run.
Fixes: 23721387c409 ("batman-adv: add basic bridge loop avoidance code")
Cc: stable@vger.kernel.org
Signed-off-by: Ibrahim Hashimov <security@auditcode.ai>
Assisted-by: AuditCode-AI:2026.07
---
net/batman-adv/bridge_loop_avoidance.c | 38 ++++++++++++++++++++++++--
net/batman-adv/main.h | 3 ++
net/batman-adv/types.h | 6 ++++
3 files changed, 45 insertions(+), 2 deletions(-)
diff --git a/net/batman-adv/bridge_loop_avoidance.c b/net/batman-adv/bridge_loop_avoidance.c
index 5c73f6ba16cf..1b6e9bee5c6c 100644
--- a/net/batman-adv/bridge_loop_avoidance.c
+++ b/net/batman-adv/bridge_loop_avoidance.c
@@ -154,6 +154,9 @@ static void batadv_backbone_gw_release(struct kref *ref)
backbone_gw = container_of(ref, struct batadv_bla_backbone_gw,
refcount);
+ /* release the slot reserved in batadv_bla_get_backbone_gw() */
+ atomic_dec(&backbone_gw->bat_priv->bla.num_backbone_gws);
+
kfree_rcu(backbone_gw, rcu);
}
@@ -179,6 +182,7 @@ static void batadv_claim_release(struct kref *ref)
{
struct batadv_bla_claim *claim;
struct batadv_bla_backbone_gw *old_backbone_gw;
+ struct batadv_priv *bat_priv;
claim = container_of(ref, struct batadv_bla_claim, refcount);
@@ -187,12 +191,18 @@ static void batadv_claim_release(struct kref *ref)
claim->backbone_gw = NULL;
spin_unlock_bh(&claim->backbone_lock);
+ /* stash bat_priv before dropping our reference on old_backbone_gw */
+ bat_priv = old_backbone_gw->bat_priv;
+
spin_lock_bh(&old_backbone_gw->crc_lock);
old_backbone_gw->crc ^= crc16(0, claim->addr, ETH_ALEN);
spin_unlock_bh(&old_backbone_gw->crc_lock);
batadv_backbone_gw_put(old_backbone_gw);
+ /* release the slot reserved in batadv_bla_add_claim() */
+ atomic_dec(&bat_priv->bla.num_claims);
+
kfree_rcu(claim, rcu);
}
@@ -508,9 +518,20 @@ batadv_bla_get_backbone_gw(struct batadv_priv *bat_priv, const u8 *orig,
"%s(): not found (%pM, %d), creating new entry\n", __func__,
orig, batadv_print_vid(vid));
+ if (!atomic_add_unless(&bat_priv->bla.num_backbone_gws, 1,
+ BATADV_BLA_MAX_BACKBONE_GW)) {
+ batadv_dbg(BATADV_DBG_BLA, bat_priv,
+ "%s(): too many backbone gateways (limit %d), dropping (%pM, %d)\n",
+ __func__, BATADV_BLA_MAX_BACKBONE_GW, orig,
+ batadv_print_vid(vid));
+ return NULL;
+ }
+
entry = kzalloc_obj(*entry, GFP_ATOMIC);
- if (!entry)
+ if (!entry) {
+ atomic_dec(&bat_priv->bla.num_backbone_gws);
return NULL;
+ }
entry->vid = vid;
WRITE_ONCE(entry->lasttime, jiffies);
@@ -531,6 +552,7 @@ batadv_bla_get_backbone_gw(struct batadv_priv *bat_priv, const u8 *orig,
if (unlikely(hash_added != 0)) {
/* hash failed, free the structure */
+ atomic_dec(&bat_priv->bla.num_backbone_gws);
kfree(entry);
return NULL;
}
@@ -708,9 +730,20 @@ static void batadv_bla_add_claim(struct batadv_priv *bat_priv,
/* create a new claim entry if it does not exist yet. */
if (!claim) {
+ if (!atomic_add_unless(&bat_priv->bla.num_claims, 1,
+ BATADV_BLA_MAX_CLAIMS)) {
+ batadv_dbg(BATADV_DBG_BLA, bat_priv,
+ "%s(): too many claims (limit %d), dropping %pM, vid %d\n",
+ __func__, BATADV_BLA_MAX_CLAIMS, mac,
+ batadv_print_vid(vid));
+ return;
+ }
+
claim = kzalloc_obj(*claim, GFP_ATOMIC);
- if (!claim)
+ if (!claim) {
+ atomic_dec(&bat_priv->bla.num_claims);
return;
+ }
ether_addr_copy(claim->addr, mac);
spin_lock_init(&claim->backbone_lock);
@@ -732,6 +765,7 @@ static void batadv_bla_add_claim(struct batadv_priv *bat_priv,
if (unlikely(hash_added != 0)) {
/* only local changes happened. */
+ atomic_dec(&bat_priv->bla.num_claims);
batadv_backbone_gw_put(backbone_gw);
kfree(claim);
return;
diff --git a/net/batman-adv/main.h b/net/batman-adv/main.h
index f68fc8b7239c..f7bb3991dbf1 100644
--- a/net/batman-adv/main.h
+++ b/net/batman-adv/main.h
@@ -110,6 +110,9 @@
#define BATADV_BLA_WAIT_PERIODS 3
#define BATADV_BLA_LOOPDETECT_PERIODS 6
#define BATADV_BLA_LOOPDETECT_TIMEOUT 3000 /* 3 seconds */
+/* upper bound on claim_hash / backbone_hash entries per mesh interface */
+#define BATADV_BLA_MAX_CLAIMS 4096
+#define BATADV_BLA_MAX_BACKBONE_GW 256
#define BATADV_DUPLIST_SIZE 16
#define BATADV_DUPLIST_TIMEOUT 500 /* 500 ms */
diff --git a/net/batman-adv/types.h b/net/batman-adv/types.h
index b1f9f8964c3f..e427318eb6d9 100644
--- a/net/batman-adv/types.h
+++ b/net/batman-adv/types.h
@@ -1077,6 +1077,12 @@ struct batadv_priv_bla {
*/
spinlock_t num_requests_lock;
+ /** @num_claims: number of entries currently in @claim_hash */
+ atomic_t num_claims;
+
+ /** @num_backbone_gws: number of entries currently in @backbone_hash */
+ atomic_t num_backbone_gws;
+
/**
* @claim_hash: hash table containing mesh nodes this host has claimed
*/
--
2.50.1 (Apple Git-155)
next reply other threads:[~2026-07-10 16:52 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-10 16:52 Ibrahim Hashimov [this message]
2026-07-10 17:56 ` [PATCH net] batman-adv: bound BLA claim and backbone gateway table growth Sven Eckelmann
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260710165224.39411-1-security@auditcode.ai \
--to=security@auditcode.ai \
--cc=antonio@mandelbit.com \
--cc=b.a.t.m.a.n@lists.open-mesh.org \
--cc=linux-kernel@vger.kernel.org \
--cc=marek.lindner@mailbox.org \
--cc=stable@vger.kernel.org \
--cc=sven@narfation.org \
--cc=sw@simonwunderlich.de \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.