netfilter: nf_conntrack_gre: fix gre keymap list corruption

Quoting reporter:
  A race between GRE keymap insertion and destruction can corrupt the
  kernel list or use a freed object. `nf_ct_gre_keymap_add()` publishes a
  new keymap pointer before the embedded `list_head` is linked, while
  `nf_ct_gre_keymap_destroy()` can concurrently delete and free that
  same object. An unprivileged user can reach this through the PPTP
  conntrack helper by racing PPTP control messages or helper teardown,
  leading to KASAN-detectable list corruption/UAF in kernel context.

 ## Root Cause Analysis
 `exp_gre()` installs GRE expectations for a PPTP control flow and then
  adds two GRE keymap entries [..]

 The add path publishes `ct_pptp_info->keymap[dir]` before linking the
 embedded list node [..]
 Concurrent teardown deletes that partially initialized object.

Make add/destroy symmetric: install both, destroy both while under lock.

Furthermore, we should refuse to publish a new mapping in case ct is going
away, else we may leak the allocation.

The "retrans" detection is strange:  existing mapping is checked for key
equality with the new mapping, then for "is on the list" via list walk.

But I can't see how an existing keymap entry can be NOT on list.

Change this to only check if we're asked to map same tuple again -- if so,
   skip re-install, else signal failure.

Last, add a bug trap for the keymap list; it has to be empty when namespace
is going away.

Reported-by: Leo Lin <leo@depthfirst.com>
Signed-off-by: Florian Westphal <fw@strlen.de>
This commit is contained in:
Florian Westphal 2026-05-14 14:21:57 +02:00
parent 92170e6afe
commit 47980b6dbf
4 changed files with 95 additions and 34 deletions

View File

@ -18,9 +18,10 @@ struct nf_ct_gre_keymap {
struct rcu_head rcu;
};
/* add new tuple->key_reply pair to keymap */
int nf_ct_gre_keymap_add(struct nf_conn *ct, enum ip_conntrack_dir dir,
struct nf_conntrack_tuple *t);
/* add tuple->key_reply pairs to keymap */
bool nf_ct_gre_keymap_add(struct nf_conn *ct,
const struct nf_conntrack_tuple *orig,
const struct nf_conntrack_tuple *repl);
/* delete keymap entries */
void nf_ct_gre_keymap_destroy(struct nf_conn *ct);

View File

@ -568,6 +568,13 @@ static void destroy_gre_conntrack(struct nf_conn *ct)
#endif
}
static void warn_on_keymap_list_leak(const struct net *net)
{
#ifdef CONFIG_NF_CT_PROTO_GRE
WARN_ON_ONCE(!list_empty(&net->ct.nf_ct_proto.gre.keymap_list));
#endif
}
void nf_ct_destroy(struct nf_conntrack *nfct)
{
struct nf_conn *ct = (struct nf_conn *)nfct;
@ -2510,6 +2517,7 @@ void nf_conntrack_cleanup_net_list(struct list_head *net_exit_list)
}
list_for_each_entry(net, net_exit_list, exit_list) {
warn_on_keymap_list_leak(net);
nf_conntrack_ecache_pernet_fini(net);
nf_conntrack_expect_pernet_fini(net);
free_percpu(net->ct.stat);

View File

@ -225,13 +225,9 @@ static int exp_gre(struct nf_conn *ct, __be16 callid, __be16 peer_callid)
if (nf_ct_expect_related(exp_reply, 0) != 0)
goto out_unexpect_orig;
/* Add GRE keymap entries */
if (nf_ct_gre_keymap_add(ct, IP_CT_DIR_ORIGINAL, &exp_orig->tuple) != 0)
if (!nf_ct_gre_keymap_add(ct, &exp_orig->tuple,
&exp_reply->tuple))
goto out_unexpect_both;
if (nf_ct_gre_keymap_add(ct, IP_CT_DIR_REPLY, &exp_reply->tuple) != 0) {
nf_ct_gre_keymap_destroy(ct);
goto out_unexpect_both;
}
ret = 0;
out_put_both:

View File

@ -87,41 +87,97 @@ static __be16 gre_keymap_lookup(struct net *net, struct nf_conntrack_tuple *t)
return key;
}
/* add a single keymap entry, associate with specified master ct */
int nf_ct_gre_keymap_add(struct nf_conn *ct, enum ip_conntrack_dir dir,
struct nf_conntrack_tuple *t)
enum nf_ct_gre_km_act {
NF_CT_GRE_KM_NEW,
NF_CT_GRE_KM_BAD,
NF_CT_GRE_KM_DUP
};
static enum nf_ct_gre_km_act
nf_ct_gre_km_acceptable(const struct nf_ct_pptp_master *ct_pptp_info,
const struct nf_conntrack_tuple *orig,
const struct nf_conntrack_tuple *repl)
{
struct nf_ct_gre_keymap *km_orig, *km_repl;
lockdep_assert_held(&keymap_lock);
km_orig = ct_pptp_info->keymap[IP_CT_DIR_ORIGINAL];
km_repl = ct_pptp_info->keymap[IP_CT_DIR_REPLY];
if (km_orig && km_repl) {
if (!gre_key_cmpfn(km_orig, orig))
return NF_CT_GRE_KM_BAD;
if (!gre_key_cmpfn(km_repl, repl))
return NF_CT_GRE_KM_BAD;
return NF_CT_GRE_KM_DUP;
}
DEBUG_NET_WARN_ON_ONCE(km_orig);
DEBUG_NET_WARN_ON_ONCE(km_repl);
return NF_CT_GRE_KM_NEW;
}
/* add keymap entries, associate with specified master ct */
bool nf_ct_gre_keymap_add(struct nf_conn *ct,
const struct nf_conntrack_tuple *orig,
const struct nf_conntrack_tuple *repl)
{
struct net *net = nf_ct_net(ct);
struct nf_gre_net *net_gre = gre_pernet(net);
struct nf_ct_pptp_master *ct_pptp_info = nfct_help_data(ct);
struct nf_ct_gre_keymap **kmp, *km;
struct nf_ct_gre_keymap *km_orig, *km_repl;
bool ret = false;
kmp = &ct_pptp_info->keymap[dir];
if (*kmp) {
/* check whether it's a retransmission */
list_for_each_entry_rcu(km, &net_gre->keymap_list, list) {
if (gre_key_cmpfn(km, t) && km == *kmp)
return 0;
}
pr_debug("trying to override keymap_%s for ct %p\n",
dir == IP_CT_DIR_REPLY ? "reply" : "orig", ct);
return -EEXIST;
}
km_orig = kmalloc_obj(*km_orig, GFP_ATOMIC);
if (!km_orig)
return false;
km_repl = kmalloc_obj(*km_repl, GFP_ATOMIC);
if (!km_repl)
goto km_free;
km = kmalloc_obj(*km, GFP_ATOMIC);
if (!km)
return -ENOMEM;
memcpy(&km->tuple, t, sizeof(*t));
*kmp = km;
pr_debug("adding new entry %p: ", km);
nf_ct_dump_tuple(&km->tuple);
memcpy(&km_orig->tuple, orig, sizeof(*orig));
memcpy(&km_repl->tuple, repl, sizeof(*repl));
spin_lock_bh(&keymap_lock);
list_add_tail(&km->list, &net_gre->keymap_list);
if (nf_ct_is_dying(ct))
goto unlock_free;
switch (nf_ct_gre_km_acceptable(ct_pptp_info, orig, repl)) {
case NF_CT_GRE_KM_NEW:
break;
case NF_CT_GRE_KM_DUP:
ret = true;
goto unlock_free;
case NF_CT_GRE_KM_BAD:
pr_debug("trying to override keymap for ct %p\n", ct);
goto unlock_free;
}
if (ct_pptp_info->keymap[IP_CT_DIR_ORIGINAL] ||
ct_pptp_info->keymap[IP_CT_DIR_REPLY])
goto unlock_free;
pr_debug("adding new entries %p,%p: ", km_orig, km_repl);
nf_ct_dump_tuple(&km_orig->tuple);
nf_ct_dump_tuple(&km_repl->tuple);
list_add_tail_rcu(&km_orig->list, &net_gre->keymap_list);
list_add_tail_rcu(&km_repl->list, &net_gre->keymap_list);
ct_pptp_info->keymap[IP_CT_DIR_ORIGINAL] = km_orig;
ct_pptp_info->keymap[IP_CT_DIR_REPLY] = km_repl;
spin_unlock_bh(&keymap_lock);
return 0;
return true;
unlock_free:
spin_unlock_bh(&keymap_lock);
km_free:
kfree(km_orig);
kfree(km_repl);
return ret;
}
EXPORT_SYMBOL_GPL(nf_ct_gre_keymap_add);