-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnft-catchall-fix-livepatch.c
More file actions
256 lines (234 loc) · 7.36 KB
/
Copy pathnft-catchall-fix-livepatch.c
File metadata and controls
256 lines (234 loc) · 7.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
// SPDX-License-Identifier: GPL-2.0
/*
* Livepatch for CVE-2026-23111: use-after-free in
* nft_map_catchall_activate() via inverted genmask check.
*
* Upstream fix:
* commit f41c5d151078c5348271ffaf8e7410d96f2d82f8
* net/netfilter/nf_tables_api.c
*
* Bug
* ---
* The pre-fix nft_map_catchall_activate() walked set->catchall_list
* with an inverted active-bit check:
*
* if (!nft_set_elem_active(ext, genmask))
* continue;
*
* On the DELSET abort path this skips the catchall element that needs
* re-activation and instead falls through on already-active (or no)
* elements. For map elements carrying NFT_GOTO verdicts, the missed
* call to nft_setelem_data_activate() means chain->use is never
* restored. Repeating the create/abort cycle drives chain->use to
* zero; a subsequent DELCHAIN then frees the chain while a catchall
* element still references it -> UAF on struct nft_chain.
*
* Fix: drop the '!'.
*
* if (nft_set_elem_active(ext, genmask))
* continue;
*
* Reconstruction caveat
* ---------------------
* The verbatim body of nft_map_catchall_activate() was not available
* at write time (kernel.org cgit blocked, and the GitHub mirror
* summarised rather than quoted). The non-conditional lines below
* are reconstructed by symmetry from nft_map_catchall_deactivate(),
* which *was* available verbatim, and from standard nf_tables
* activate/deactivate symmetry (nft_clear + nft_setelem_data_activate
* mirrors nft_set_elem_change_active + nft_setelem_data_deactivate).
*
* BEFORE DEPLOYING: diff this against your kernel's actual
* pre-fix function body. If any non-conditional line differs,
* update accordingly or use kpatch-build against commit
* f41c5d151078 -- that produces a bit-exact replacement.
*
* Static helpers
* --------------
* nft_setelem_data_activate() is non-static and declared in
* <net/netfilter/nf_tables.h>, but on many builds it is not
* EXPORT_SYMBOL_GPL'd, so this module resolves it via
* kallsyms-through-kprobes (same pattern as ptrace-fix-livepatch.c)
* rather than relying on the linker.
*
* Target object
* -------------
* nf_tables_api.c builds into the nf_tables module on most distros
* (CONFIG_NF_TABLES=m). If your kernel has CONFIG_NF_TABLES=y, change
* klp_object.name to NULL so the patch attaches to vmlinux.
*
* Compatibility
* -------------
* - CONFIG_LIVEPATCH=y, CONFIG_KPROBES=y
* - Modern (>=5.1) and legacy livepatch APIs both handled.
* - nft_map_catchall_activate must be present in kallsyms; verify
* with: grep nft_map_catchall_activate /proc/kallsyms
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/livepatch.h>
#include <linux/list.h>
#include <linux/rcupdate.h>
#include <linux/kallsyms.h>
#include <linux/version.h>
#include <linux/errno.h>
#include <net/netfilter/nf_tables.h>
/*
* struct nft_set_elem_catchall is declared *inside* net/netfilter/
* nf_tables_api.c, not in any public header, so we redeclare it here
* with the same field layout the target kernel uses. The list-head
* macros need exact offsets to land container_of() in the right place.
*
* v6.7 commit 0e1ea651c9d9 ("netfilter: nf_tables: shrink memory
* consumption of set elements") replaced the old `void *elem;` field
* with `struct nft_elem_priv *elem;` and re-typed the helpers that
* consume it (nft_setelem_data_activate, nft_set_elem_ext) to match.
* The on-wire layout (pointer-sized field at the same offset) is
* identical, so list-iteration arithmetic is the same on both sides,
* but we keep the typed declaration so calls type-check cleanly.
*
* Covers 6.x kernels including the 6.8 and 6.12 targets in this repo.
*/
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 7, 0)
struct nft_set_elem_catchall {
struct list_head list;
struct rcu_head rcu;
struct nft_elem_priv *elem;
};
typedef struct nft_elem_priv *nft_elem_t;
#else
struct nft_set_elem_catchall {
struct list_head list;
struct rcu_head rcu;
void *elem;
};
typedef void *nft_elem_t;
#endif
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 7, 0)
# include <linux/kprobes.h>
# define NEED_KPROBE_FALLBACK 1
#endif
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Marian Marinov");
MODULE_DESCRIPTION("Livepatch for CVE-2026-23111 (nf_tables catchall UAF)");
MODULE_INFO(livepatch, "Y");
static void (*nft_setelem_data_activate_fn)(const struct net *net,
const struct nft_set *set,
nft_elem_t elem_priv);
typedef unsigned long (*kln_t)(const char *);
static kln_t resolve_kln(void)
{
#ifdef NEED_KPROBE_FALLBACK
struct kprobe kp = { .symbol_name = "kallsyms_lookup_name" };
kln_t fn;
int ret;
ret = register_kprobe(&kp);
if (ret < 0) {
pr_err("nft-cve-2026-23111: register_kprobe(kallsyms_lookup_name) failed: %d\n",
ret);
pr_err("nft-cve-2026-23111: pass activate_addr= as a fallback\n");
return NULL;
}
fn = (kln_t)kp.addr;
unregister_kprobe(&kp);
return fn;
#else
return (kln_t)kallsyms_lookup_name;
#endif
}
static unsigned long activate_addr;
module_param(activate_addr, ulong, 0);
MODULE_PARM_DESC(activate_addr,
"Address of nft_setelem_data_activate (override; 0=auto via kallsyms)");
static void patched_nft_map_catchall_activate(const struct nft_ctx *ctx,
struct nft_set *set)
{
u8 genmask = nft_genmask_next(ctx->net);
struct nft_set_elem_catchall *catchall;
struct nft_set_ext *ext;
list_for_each_entry(catchall, &set->catchall_list, list) {
ext = nft_set_elem_ext(set, catchall->elem);
/*
* CVE-2026-23111 fix: drop the '!'. Skip already-active
* elements; process the inactive catchall that the abort
* path needs to re-activate.
*/
if (nft_set_elem_active(ext, genmask))
continue;
nft_clear(ctx->net, ext);
nft_setelem_data_activate_fn(ctx->net, set, catchall->elem);
break;
}
}
static struct klp_func funcs[] = {
{
.old_name = "nft_map_catchall_activate",
.new_func = patched_nft_map_catchall_activate,
},
{ }
};
static struct klp_object objs[] = {
{
// CONFIG_NF_TABLES=y systems: set .name = NULL for vmlinux
.name = "nf_tables",
.funcs = funcs,
},
{ }
};
static struct klp_patch patch = {
.mod = THIS_MODULE,
.objs = objs,
};
static int __init nft_lp_init(void)
{
unsigned long addr = activate_addr;
int ret;
if (!addr) {
kln_t kln = resolve_kln();
if (!kln)
return -ENOENT;
addr = kln("nft_setelem_data_activate");
if (!addr) {
pr_err("nft-cve-2026-23111: nft_setelem_data_activate not in kallsyms\n");
return -ENOENT;
}
}
nft_setelem_data_activate_fn =
(typeof(nft_setelem_data_activate_fn))addr;
pr_info("nft-cve-2026-23111: nft_setelem_data_activate at %px\n",
(void *)addr);
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 1, 0)
ret = klp_enable_patch(&patch);
#else
ret = klp_register_patch(&patch);
if (ret)
return ret;
ret = klp_enable_patch(&patch);
if (ret) {
klp_unregister_patch(&patch);
return ret;
}
#endif
if (ret) {
pr_err("nft-cve-2026-23111: klp_enable_patch failed: %d\n",
ret);
return ret;
}
pr_info("nft-cve-2026-23111: nft_map_catchall_activate patched\n");
return 0;
}
static void __exit nft_lp_exit(void)
{
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 1, 0)
/*
* Modern livepatch: disable via sysfs *before* rmmod:
* echo 0 > /sys/kernel/livepatch/nft_catchall_fix_livepatch/enabled
* Wait for .../transition to read 0, then rmmod.
*/
#else
klp_disable_patch(&patch);
klp_unregister_patch(&patch);
#endif
}
module_init(nft_lp_init);
module_exit(nft_lp_exit);