diff mbox series

[nf-next,2/3] netfilter: nf_tables: validate register loads never access unitialised registers

Message ID 20230505111656.32238-3-fw@strlen.de
State Changes Requested, archived
Delegated to: Pablo Neira
Headers show
Series netfilter: nf_tables: reject loads from uninitialized registers | expand

Commit Message

Florian Westphal May 5, 2023, 11:16 a.m. UTC
Reject rules where a load occurs from a register that has not seen a
store early in the same rule.

commit 4c905f6740a3 ("netfilter: nf_tables: initialize registers in nft_do_chain()")
had to add a unconditional memset to the nftables register space to
avoid leaking stack information to userspace.

This memset shows up in benchmarks.  After this change, this commit
can be reverted again.

Note that this breaks userspace compatibility, because theoretically
you can do

    rule 1: reg2 := meta load iif, reg2  == 1 jump ...
    rule 2: reg2 == 2 jump ...   // read access with no store in this rule

... after this change this is rejected.

Neither nftables nor iptables-nft generate such rules, each rule is
always standalone.

Signed-off-by: Florian Westphal <fw@strlen.de>
---
 include/net/netfilter/nf_tables.h |  1 +
 net/netfilter/nf_tables_api.c     | 37 ++++++++++++++++++++++++++++---
 2 files changed, 35 insertions(+), 3 deletions(-)

Comments

Pablo Neira Ayuso May 30, 2023, 11:49 p.m. UTC | #1
Hi Florian,

On Fri, May 05, 2023 at 01:16:55PM +0200, Florian Westphal wrote:
> Reject rules where a load occurs from a register that has not seen a
> store early in the same rule.
> 
> commit 4c905f6740a3 ("netfilter: nf_tables: initialize registers in nft_do_chain()")
> had to add a unconditional memset to the nftables register space to
> avoid leaking stack information to userspace.
> 
> This memset shows up in benchmarks.  After this change, this commit
> can be reverted again.
> 
> Note that this breaks userspace compatibility, because theoretically
> you can do
> 
>     rule 1: reg2 := meta load iif, reg2  == 1 jump ...
>     rule 2: reg2 == 2 jump ...   // read access with no store in this rule
> 
> ... after this change this is rejected.

We can probably achieve the same effect by recovering the runtime
register tracking patches I posted. It should be possible to add
unlikely() to the branch that checks for uninitialized data in source
register, that is missing in this patch:

https://patchwork.ozlabs.org/project/netfilter-devel/patch/20230505153130.2385-6-pablo@netfilter.org/

such patch also equires these patches to add the helpers to load and
store:

https://patchwork.ozlabs.org/project/netfilter-devel/patch/20230505153130.2385-3-pablo@netfilter.org/
https://patchwork.ozlabs.org/project/netfilter-devel/patch/20230505153130.2385-4-pablo@netfilter.org/
https://patchwork.ozlabs.org/project/netfilter-devel/patch/20230505153130.2385-5-pablo@netfilter.org/

I think those helpers to load and store on registers should have been
there since the beginning instead of opencoding operations on the
registers. I am going to collect some numbers with these patches
including unlikely() to the _load() checks. I fear I might have
introduced some subtle bug, I remember these patches are passing
selftests fine, but I am not sure we have enough of these selftests.

As you suggested, I also considered using the new track infrastructure
(the one I posted to achieve the combo expressions) to detect a read
to uninitialized registers from control plane, but it gets complicated
again because:

1) what level should register tracking happen at? rule, chain or from
   basechain to leaf chains (to ensure that we retain the freedom to
   make more transformation from userspace, eg. static flag for ruleset
   that never change to omit redundant operations in the generated
   bytecode). Your patch selects rule level. Chain level will lose
   context when jumping/going to another chain. Inspecting from
   basechain to leaf chains will be expensive in dynamic rulesets.

2) combo expressions omit the register store operation, the tracking
   infrastructure would need to distinguish between two situations:
   register data has been omitted or register data is missing because
   userspace provides bytecode that tries to read uninitialized
   registers.

While I agree control plane is ideal for this, because it allows to
reject a ruleset that reads on uninitialized register data, checking
at rule/chain level cripples expressiveness in a way that it will not
be easy to revert in the future if we want to change direction.

> Neither nftables nor iptables-nft generate such rules, each rule is
> always standalone.

That is true these days indeed. I like your approach because it is
simple. But my concern is that this limits expressiveness.

Thanks.
Florian Westphal May 31, 2023, 9:51 a.m. UTC | #2
Pablo Neira Ayuso <pablo@netfilter.org> wrote:
> >     rule 1: reg2 := meta load iif, reg2  == 1 jump ...
> >     rule 2: reg2 == 2 jump ...   // read access with no store in this rule
> > 
> > ... after this change this is rejected.
> 
> We can probably achieve the same effect by recovering the runtime
> register tracking patches I posted. It should be possible to add
> unlikely() to the branch that checks for uninitialized data in source
> register, that is missing in this patch:
> 
> https://patchwork.ozlabs.org/project/netfilter-devel/patch/20230505153130.2385-6-pablo@netfilter.org/
> 
> such patch also equires these patches to add the helpers to load and
> store:
> 
> https://patchwork.ozlabs.org/project/netfilter-devel/patch/20230505153130.2385-3-pablo@netfilter.org/
> https://patchwork.ozlabs.org/project/netfilter-devel/patch/20230505153130.2385-4-pablo@netfilter.org/
> https://patchwork.ozlabs.org/project/netfilter-devel/patch/20230505153130.2385-5-pablo@netfilter.org/
> 
> I think those helpers to load and store on registers should have been
> there since the beginning instead of opencoding operations on the
> registers. I am going to collect some numbers with these patches
> including unlikely() to the _load() checks. I fear I might have
> introduced some subtle bug, I remember these patches are passing
> selftests fine, but I am not sure we have enough of these selftests.

Thanks, I agree that we will need runtime checking because control plane
is too complex due to stores becoming unreliable (NFT_BREAK
encountered) if we want to do this load suppression.

I get the impression that we're overthinking this, we really
should not bother too much with speeding up iptables-style linear
rulesets.

I'm only concerned with speeding up compact rulesets that use
sets/maps wherever possible.

I think for those getting rid of the memset() will help more
than eliding a couple of reloads.

> 1) what level should register tracking happen at? rule, chain or from
>    basechain to leaf chains (to ensure that we retain the freedom to
>    make more transformation from userspace, eg. static flag for ruleset
>    that never change to omit redundant operations in the generated
>    bytecode). Your patch selects rule level. Chain level will lose
>    context when jumping/going to another chain. Inspecting from
>    basechain to leaf chains will be expensive in dynamic rulesets.

Right.  I used rule level because its easy to do but as you say,
it prevents userspace from ever making a "clever" ruleset that
performs any sort of preload (without kernel update).

> 2) combo expressions omit the register store operation, the tracking
>    infrastructure would need to distinguish between two situations:
>    register data has been omitted or register data is missing because
>    userspace provides bytecode that tries to read uninitialized
>    registers.
> 
> While I agree control plane is ideal for this, because it allows to
> reject a ruleset that reads on uninitialized register data, checking
> at rule/chain level cripples expressiveness in a way that it will not
> be easy to revert in the future if we want to change direction.
> 
> > Neither nftables nor iptables-nft generate such rules, each rule is
> > always standalone.
> 
> That is true these days indeed. I like your approach because it is
> simple. But my concern is that this limits expressiveness.

Lets wait for your test with runtime checking, so we have some data
on how much of a slowdown that gives.

But I'd like to see some proof that a *good* ruleset has many
redundant loads where such cross-rule load elimination can add a
benefit in the first place.  Else doing runtime validation makes no
sense.

What I can think of is to allow this patch in,
i.e. rule level enforcement, and then follow a approach similar to what
you already proposed earlier: A per-chain prefetch stage.

This would mean instead of

chain [ rule [ expr, expr, expr ]] [ rule [ expr , expr ]] ..

we'd have
chain prefetch [ expr, expr ] [ rule ... ]

The prefetch is limited to selected meta and payload operations.
Failure means entire chain is bypassed.

Any sort of jump invalidates the prefetch.

So, this patch would be later relaxed to pre-init
the "prefetch" registers as "initialised", so following is legal:

prefetch: reg3: meta l4roto, reg4: meta load iif
rule 1: reg2 := ip saddr, lookup(reg2 . reg3 .reg 4) accept
rule 2: reg3 == icmp accept ...  // no longer rejected as uninitialized
rule 3: reg4 == "eth*" jump ...
rule 4: reg3 == icmp accept ...  // fails --> prefetch lost
rule 4: reg2 := ip saddr ...  // fails --> prefetch overwritten

Yet another idea: Introduce internal shadow registers:
Prefetch to reg1, reg2, reg3 will auto-store those *also*
to *pfr1*, *pfr2, and so on.

This would allow register content recovery after each jump:
reg1 = pfr1
reg2 = pfr2

and so on.

But again, I think this is the wrong approach and we should not
bother with load elimination or anything else that will complicate
the data path.

Combo match approach is good because it doesn't have that issue.
diff mbox series

Patch

diff --git a/include/net/netfilter/nf_tables.h b/include/net/netfilter/nf_tables.h
index 246c4a4620ae..8c6068569fcf 100644
--- a/include/net/netfilter/nf_tables.h
+++ b/include/net/netfilter/nf_tables.h
@@ -220,6 +220,7 @@  struct nft_ctx {
 	u8				family;
 	u8				level;
 	bool				report;
+	DECLARE_BITMAP(reg_inited, NFT_REG32_COUNT);
 };
 
 enum nft_data_desc_flags {
diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
index 4fd6bbb88cd2..cd9deeccda0f 100644
--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -139,6 +139,8 @@  static void nft_ctx_init(struct nft_ctx *ctx,
 	ctx->report	= nlmsg_report(nlh);
 	ctx->flags	= nlh->nlmsg_flags;
 	ctx->seq	= nlh->nlmsg_seq;
+
+	memset(ctx->reg_inited, 0, sizeof(ctx->reg_inited));
 }
 
 static struct nft_trans *nft_trans_alloc_gfp(const struct nft_ctx *ctx,
@@ -10039,7 +10041,7 @@  static int nft_validate_register_load(enum nft_registers reg, unsigned int len)
 int nft_parse_register_load(const struct nft_ctx *ctx,
 			    const struct nlattr *attr, u8 *sreg, u32 len)
 {
-	u32 reg;
+	u32 reg, bit;
 	int err;
 
 	err = nft_parse_register(attr, &reg);
@@ -10050,11 +10052,37 @@  int nft_parse_register_load(const struct nft_ctx *ctx,
 	if (err < 0)
 		return err;
 
+	for (bit = reg; len > 0; bit++) {
+		if (!test_bit(bit, ctx->reg_inited))
+			return -ENODATA;
+		if (len <= NFT_REG32_SIZE)
+			break;
+		len -= NFT_REG32_SIZE;
+	}
+
 	*sreg = reg;
 	return 0;
 }
 EXPORT_SYMBOL_GPL(nft_parse_register_load);
 
+static void nft_saw_register_store(const struct nft_ctx *__ctx,
+				   int reg, unsigned int len)
+{
+	unsigned int last_reg = reg + (len - 1) / NFT_REG32_SIZE;
+	struct nft_ctx *ctx = (struct nft_ctx *)__ctx;
+	int bit;
+
+	if (WARN_ON_ONCE(len == 0 || reg < 0))
+		return;
+
+	for (bit = reg; bit <= last_reg; bit++) {
+		if (WARN_ON_ONCE(bit >= NFT_REG32_COUNT))
+			return;
+
+		set_bit(bit, ctx->reg_inited);
+	}
+}
+
 static int nft_validate_register_store(const struct nft_ctx *ctx,
 				       enum nft_registers reg,
 				       const struct nft_data *data,
@@ -10076,7 +10104,7 @@  static int nft_validate_register_store(const struct nft_ctx *ctx,
 				return err;
 		}
 
-		return 0;
+		break;
 	default:
 		if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE)
 			return -EINVAL;
@@ -10088,8 +10116,11 @@  static int nft_validate_register_store(const struct nft_ctx *ctx,
 
 		if (data != NULL && type != NFT_DATA_VALUE)
 			return -EINVAL;
-		return 0;
+		break;
 	}
+
+	nft_saw_register_store(ctx, reg, len);
+	return 0;
 }
 
 int nft_parse_register_store(const struct nft_ctx *ctx,