net/core/filter.c | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-)
A sockops prog reading skops->rtt_min never checks the sk type: on the
tcp_conn_request() path sock_ops->sk is a request_sock (non-full), and the
ctx rewrite casts it to a tcp_sock (full) and reads rtt_min past the end of
the request_sock, returning dirty adjacent memory.
SEC("sockops")
int prog(struct bpf_sock_ops *skops)
{
switch (skops->op) {
case BPF_SOCK_OPS_RWND_INIT:
leak = skops->rtt_min; /* reads the request_sock OOB */
...
}
}
For instance one such read returned rtt_min=0xffff8881, the high half of a
leaked kernel pointer.
Guarding that cast is exactly what SOCK_OPS_GET_FIELD() does -- it checks
is_locked_tcp_sock and returns 0 when sock_ops->sk is not a locked full
socket. Every other tcp_sock field in sock_ops goes through it; rtt_min is
the only one open-coded, so it skips the check.
Read rtt_min through SOCK_OPS_GET_FIELD() too. rtt_min is a bit special:
it is a struct minmax and we only want the current min, so pass
rtt_min.s[0].v. That is equivalent to the old hand-computed offset
offsetof(struct tcp_sock, rtt_min) + sizeof_field(struct minmax_sample, t)
(s[0] sits at rtt_min + 0 and .v at + sizeof(.t), i.e. what minmax_get()
returns), so the loaded field is unchanged and only the full-sock guard is
added. The two BUILD_BUG_ON()s that protected the hand-computed offset
are no longer needed.
Before patch:
0: r1 = *(u64 *)(r1 +0) ; r1 = skops->sk
1: r1 = *(u32 *)(r1 +2324) ; ((tcp_sock *)sk)->rtt_min.s[0].v
After patch:
0: *(u64 *)(r1 +56) = r9
1: r9 = *(u8 *)(r1 +50) ; is_locked_tcp_sock
2: if r9 == 0 goto pc+4 ; not a locked full sock -> 0
3: r9 = *(u64 *)(r1 +56)
4: r1 = *(u64 *)(r1 +0) ; r1 = skops->sk
5: r1 = *(u32 *)(r1 +2324) ; rtt_min.s[0].v
6: goto pc+2
7: r9 = *(u64 *)(r1 +56)
8: r1 = 0
Fixes: 44f0e43037d3 ("bpf: Add support for reading sk_state and more")
Reported-by: VEGA <vega@nebusec.ai>
Signed-off-by: Jiayuan Chen <jiayuan.chen@linux.dev>
---
net/core/filter.c | 13 +------------
1 file changed, 1 insertion(+), 12 deletions(-)
diff --git a/net/core/filter.c b/net/core/filter.c
index 61940e753552..a043d179ff1a 100644
--- a/net/core/filter.c
+++ b/net/core/filter.c
@@ -11105,18 +11105,7 @@ static u32 sock_ops_convert_ctx_access(enum bpf_access_type type,
break;
case offsetof(struct bpf_sock_ops, rtt_min):
- BUILD_BUG_ON(sizeof_field(struct tcp_sock, rtt_min) !=
- sizeof(struct minmax));
- BUILD_BUG_ON(sizeof(struct minmax) <
- sizeof(struct minmax_sample));
-
- *insn++ = BPF_LDX_MEM(BPF_FIELD_SIZEOF(
- struct bpf_sock_ops_kern, sk),
- si->dst_reg, si->src_reg,
- offsetof(struct bpf_sock_ops_kern, sk));
- *insn++ = BPF_LDX_MEM(BPF_W, si->dst_reg, si->dst_reg,
- offsetof(struct tcp_sock, rtt_min) +
- sizeof_field(struct minmax_sample, t));
+ SOCK_OPS_GET_FIELD(rtt_min, rtt_min.s[0].v, struct tcp_sock);
break;
case offsetof(struct bpf_sock_ops, bpf_sock_ops_cb_flags):
--
2.43.0
On Thu, Sep 3, 2026 at 6:10 AM Jiayuan Chen <jiayuan.chen@linux.dev> wrote:
>
> A sockops prog reading skops->rtt_min never checks the sk type: on the
> tcp_conn_request() path sock_ops->sk is a request_sock (non-full), and the
> ctx rewrite casts it to a tcp_sock (full) and reads rtt_min past the end of
> the request_sock, returning dirty adjacent memory.
>
> SEC("sockops")
> int prog(struct bpf_sock_ops *skops)
> {
> switch (skops->op) {
> case BPF_SOCK_OPS_RWND_INIT:
> leak = skops->rtt_min; /* reads the request_sock OOB */
> ...
> }
> }
>
> For instance one such read returned rtt_min=0xffff8881, the high half of a
> leaked kernel pointer.
>
> Guarding that cast is exactly what SOCK_OPS_GET_FIELD() does -- it checks
> is_locked_tcp_sock and returns 0 when sock_ops->sk is not a locked full
> socket. Every other tcp_sock field in sock_ops goes through it; rtt_min is
> the only one open-coded, so it skips the check.
>
> Read rtt_min through SOCK_OPS_GET_FIELD() too. rtt_min is a bit special:
> it is a struct minmax and we only want the current min, so pass
> rtt_min.s[0].v. That is equivalent to the old hand-computed offset
>
> offsetof(struct tcp_sock, rtt_min) + sizeof_field(struct minmax_sample, t)
>
> (s[0] sits at rtt_min + 0 and .v at + sizeof(.t), i.e. what minmax_get()
> returns), so the loaded field is unchanged and only the full-sock guard is
> added. The two BUILD_BUG_ON()s that protected the hand-computed offset
> are no longer needed.
>
> Before patch:
>
> 0: r1 = *(u64 *)(r1 +0) ; r1 = skops->sk
> 1: r1 = *(u32 *)(r1 +2324) ; ((tcp_sock *)sk)->rtt_min.s[0].v
>
> After patch:
>
> 0: *(u64 *)(r1 +56) = r9
> 1: r9 = *(u8 *)(r1 +50) ; is_locked_tcp_sock
> 2: if r9 == 0 goto pc+4 ; not a locked full sock -> 0
> 3: r9 = *(u64 *)(r1 +56)
> 4: r1 = *(u64 *)(r1 +0) ; r1 = skops->sk
> 5: r1 = *(u32 *)(r1 +2324) ; rtt_min.s[0].v
> 6: goto pc+2
> 7: r9 = *(u64 *)(r1 +56)
> 8: r1 = 0
>
> Fixes: 44f0e43037d3 ("bpf: Add support for reading sk_state and more")
> Reported-by: VEGA <vega@nebusec.ai>
> Signed-off-by: Jiayuan Chen <jiayuan.chen@linux.dev>
> ---
Reviewed-by: Emil Tsalapatis <emil@etsalapatis.com>
> net/core/filter.c | 13 +------------
> 1 file changed, 1 insertion(+), 12 deletions(-)
>
> diff --git a/net/core/filter.c b/net/core/filter.c
> index 61940e753552..a043d179ff1a 100644
> --- a/net/core/filter.c
> +++ b/net/core/filter.c
> @@ -11105,18 +11105,7 @@ static u32 sock_ops_convert_ctx_access(enum bpf_access_type type,
> break;
>
> case offsetof(struct bpf_sock_ops, rtt_min):
> - BUILD_BUG_ON(sizeof_field(struct tcp_sock, rtt_min) !=
> - sizeof(struct minmax));
> - BUILD_BUG_ON(sizeof(struct minmax) <
> - sizeof(struct minmax_sample));
> -
> - *insn++ = BPF_LDX_MEM(BPF_FIELD_SIZEOF(
> - struct bpf_sock_ops_kern, sk),
> - si->dst_reg, si->src_reg,
> - offsetof(struct bpf_sock_ops_kern, sk));
> - *insn++ = BPF_LDX_MEM(BPF_W, si->dst_reg, si->dst_reg,
> - offsetof(struct tcp_sock, rtt_min) +
> - sizeof_field(struct minmax_sample, t));
> + SOCK_OPS_GET_FIELD(rtt_min, rtt_min.s[0].v, struct tcp_sock);
> break;
>
> case offsetof(struct bpf_sock_ops, bpf_sock_ops_cb_flags):
> --
> 2.43.0
>
© 2016 - 2026 Red Hat, Inc.