bpf: tcp: Support selected sock_ops callbacks as struct_ops

In LSFMMBPF 2025, I have talked about moving the BPF_PROG_TYPE_SOCK_OPS
to a struct_ops interface [1].

The BPF_SOCK_OPS_*_CB enum interface has grown over time as new TCP
callback points were added. A BPF_PROG_TYPE_SOCK_OPS program now
commonly needs a large switch on sock_ops->op, and the shared
bpf_sock_ops_kern context has become harder to extend because different
callbacks have different locking, argument, skb, and helper
requirements. The existing 'union { u32 args[4]; u32 replylong[4]; }' is
also not reliable in passing args to bpf prog when there are multiple
progs attached to a cgroup.

The above has already been solved in struct_ops. Add a TCP-specific
struct_ops type, bpf_tcp_ops, and support attaching it to cgroups.
This allows each callback have its own func signature and allows
the verifier to select kfuncs/helpers based on the specific
struct_ops member being implemented.

This patch wires up the following existing sock_ops callbacks:
- BPF_SOCK_OPS_TIMEOUT_INIT
- BPF_SOCK_OPS_RWND_INIT
- BPF_SOCK_OPS_RTT_CB
- BPF_SOCK_OPS_STATE_CB
- BPF_SOCK_OPS_RETRANS_CB
- BPF_SOCK_OPS_TCP_CONNECT_CB
- BPF_SOCK_OPS_TCP_LISTEN_CB
- BPF_SOCK_OPS_RTO_CB
- BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB
- BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB

BASE_RTT is ignored as it is not particularly useful. NEEDS_ECN should
be done in bpf-tcp-cc instead. The tstamp ones should be a separate
struct_ops (e.g. "bpf_sock_ops") that can work in both TCP and UDP.

timeout_init and rwnd_init could have a request_sock pointer. This patch
tries a different API and directly passes the request_sock pointer as
an arg.

Two other approaches were considered before settling on having
bpf_get_retval() read the dispatcher's run_ctx via saved_run_ctx. The
first was to inherit the retval in the trampoline itself: add a helper
in the four __bpf_prog_enter*() paths that, for struct_ops programs,
copies the chained value from the caller's run_ctx (now saved_run_ctx)
into the program's own run_ctx. It works but puts a per-enter
program-type check on the generic trampoline fast path, taxing all
fentry/fexit/lsm callers for a cgroup-struct_ops-only feature. The
second was to do that same inherit only for the int-returning members
via a gen_prologue that emits a hidden kfunc at the start of
timeout_init/rwnd_init; this keeps the cost off the generic path and
scoped to bpf_tcp_ops, but needs a kfunc + BTF_ID + prologue-emission
machinery. The chosen approach avoids both: it touches neither the
trampoline nor the program, since saved_run_ctx already points at the
dispatcher's run_ctx that carries the value.

[1], page 13: https://drive.google.com/file/d/1wjKZth6T0llLJ_ONPAL_6Q_jbxbAjByp/view?usp=sharing

Signed-off-by: Martin KaFai Lau <martin.lau@kernel.org>
Signed-off-by: Amery Hung <ameryhung@gmail.com>
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
Reviewed-by: Emil Tsalapatis <emil@etsalapatis.com>
Link: https://patch.msgid.link/20260917200542.3689605-12-ameryhung@gmail.com
This commit is contained in:
Amery Hung 2026-09-17 13:05:37 -07:00 committed by Alexei Starovoitov
parent 6d4ce90766
commit 5ac77ae329
No known key found for this signature in database
9 changed files with 310 additions and 2 deletions

View File

@ -2634,6 +2634,7 @@ struct bpf_trace_run_ctx {
struct bpf_tramp_run_ctx {
struct bpf_run_ctx run_ctx;
u64 bpf_cookie;
int retval;
struct bpf_run_ctx *saved_run_ctx;
};

View File

@ -2959,12 +2959,120 @@ static inline void tcp_clear_sock_ops_cb_flags(struct sock *sk)
#endif
#if defined(CONFIG_BPF_JIT) && defined(CONFIG_CGROUP_BPF)
struct bpf_tcp_ops {
/* Should return the initial SYN (active open) or SYN-ACK (passive open)
* retransmission timeout. Return the timeout in jiffies, or <= 0 for
* the kernel default.
*
* @req: request_sock on the passive (synack) path; NULL otherwise.
*/
int (*timeout_init)(struct sock *sk, struct request_sock *req);
/* Should return the initial advertised receive window, in packets,
* or < 0 for the kernel default. @req as in timeout_init().
*/
int (*rwnd_init)(struct sock *sk, struct request_sock *req);
/* Called when an active connection becomes established.
* @skb is the SYNACK that completed the 3WHS, or NULL for a
* TCP_REPAIR socket (tcp_finish_connect() with no skb).
*/
void (*active_established)(struct sock *sk, struct sk_buff *skb__nullable);
/* Called when a passive connection becomes established.
* @skb is the ACK that completed the 3WHS.
*/
void (*passive_established)(struct sock *sk, struct sk_buff *skb);
/* Called when the retransmission timer fires. */
void (*rto)(struct sock *sk);
/* Called on every RTT sample.
* @mrtt: the measured RTT, in microseconds.
* @srtt: the updated smoothed RTT.
*/
void (*rtt)(struct sock *sk, long mrtt, u32 srtt);
/* Called when the connection changes TCP state.
* @state: the new state (one of the TCP_* states).
*/
void (*set_state)(struct sock *sk, int state);
/* Called when an skb is retransmitted.
* @skb: the retransmitted skb.
* @err: tcp_transmit_skb() return value (0 on success).
*/
void (*retrans)(struct sock *sk, struct sk_buff *skb, int err);
/* Called right before an active connection is initialized. */
void (*connect)(struct sock *sk);
/* Called on listen(2), right after the socket enters TCP_LISTEN. */
void (*listen)(struct sock *sk);
};
#define bpf_tcp_ops_call(op, sk, ...) \
do { \
if (cgroup_bpf_enabled(CGROUP_TCP_SOCK_OPS)) { \
const struct bpf_prog_array_item *item; \
const struct bpf_tcp_ops *tcp_ops; \
struct cgroup *cgrp; \
\
cgrp = sock_cgroup_ptr(&sk->sk_cgrp_data); \
rcu_read_lock_dont_migrate(); \
bpf_cgroup_struct_ops_foreach(tcp_ops, item, cgrp, \
CGROUP_TCP_SOCK_OPS) { \
if (tcp_ops->op) \
tcp_ops->op(sk, ##__VA_ARGS__); \
} \
rcu_read_unlock_migrate(); \
} \
} while (0)
#define bpf_tcp_ops_call_int(op, init_retval, sk, ...) \
({ \
int __retval = (init_retval); \
if (cgroup_bpf_enabled(CGROUP_TCP_SOCK_OPS)) { \
const struct bpf_prog_array_item *item; \
const struct bpf_tcp_ops *tcp_ops; \
struct bpf_tramp_run_ctx run_ctx; \
struct bpf_run_ctx *old_run_ctx; \
struct sock *__sk = sk_to_full_sk(sk); \
struct request_sock *req = NULL; \
struct cgroup *cgrp; \
\
if (__sk) { \
run_ctx.retval = (init_retval); \
cgrp = sock_cgroup_ptr(&__sk->sk_cgrp_data); \
if (!sk_fullsock(sk)) \
req = (struct request_sock *)sk; \
rcu_read_lock_dont_migrate(); \
old_run_ctx = bpf_set_run_ctx(&run_ctx.run_ctx);\
bpf_cgroup_struct_ops_foreach(tcp_ops, item, cgrp, \
CGROUP_TCP_SOCK_OPS) { \
if (tcp_ops->op) \
run_ctx.retval = tcp_ops->op(__sk, req, ##__VA_ARGS__); \
} \
bpf_reset_run_ctx(old_run_ctx); \
rcu_read_unlock_migrate(); \
__retval = run_ctx.retval; \
} \
} \
__retval; \
})
#else
#define bpf_tcp_ops_call(op, sk, ...) do { } while (0)
#define bpf_tcp_ops_call_int(op, init_retval, sk, ...) (init_retval)
#endif
static inline u32 tcp_timeout_init(struct sock *sk)
{
int timeout;
timeout = tcp_call_bpf(sk, BPF_SOCK_OPS_TIMEOUT_INIT, 0, NULL);
timeout = bpf_tcp_ops_call_int(timeout_init, timeout, sk);
if (timeout <= 0)
timeout = TCP_TIMEOUT_INIT;
return min_t(int, timeout, TCP_RTO_MAX);
@ -2975,7 +3083,7 @@ static inline u32 tcp_rwnd_init_bpf(struct sock *sk)
int rwnd;
rwnd = tcp_call_bpf(sk, BPF_SOCK_OPS_RWND_INIT, 0, NULL);
rwnd = bpf_tcp_ops_call_int(rwnd_init, rwnd, sk);
if (rwnd < 0)
rwnd = 0;
return rwnd;
@ -2990,6 +3098,7 @@ static inline void tcp_bpf_rtt(struct sock *sk, long mrtt, u32 srtt)
{
if (BPF_SOCK_OPS_TEST_FLAG(tcp_sk(sk), BPF_SOCK_OPS_RTT_CB_FLAG))
tcp_call_bpf_2arg(sk, BPF_SOCK_OPS_RTT_CB, mrtt, srtt);
bpf_tcp_ops_call(rtt, sk, mrtt, srtt);
}
#if IS_ENABLED(CONFIG_SMC)

View File

@ -70,6 +70,7 @@ obj-$(CONFIG_TCP_AO) += tcp_ao.o
ifeq ($(CONFIG_BPF_JIT),y)
obj-$(CONFIG_BPF_SYSCALL) += bpf_tcp_ca.o
obj-$(CONFIG_CGROUP_BPF) += bpf_tcp_ops.o
endif
ifdef CONFIG_GCOV_PROFILE_NETFILTER

View File

@ -227,6 +227,7 @@ int __inet_listen_sk(struct sock *sk, int backlog)
return err;
tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_LISTEN_CB, 0, NULL);
bpf_tcp_ops_call(listen, sk);
}
return 0;
}

188
net/ipv4/bpf_tcp_ops.c Normal file
View File

@ -0,0 +1,188 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
#include <linux/bpf.h>
#include <linux/btf_ids.h>
#include <linux/bpf_verifier.h>
#include <net/bpf_sk_storage.h>
#include <net/tcp.h>
static int timeout_init_stub(struct sock *sk, struct request_sock *req__nullable)
{
struct bpf_tramp_run_ctx *ctx =
container_of(current->bpf_ctx, struct bpf_tramp_run_ctx, run_ctx);
return ctx->retval;
}
static int rwnd_init_stub(struct sock *sk, struct request_sock *req__nullable)
{
struct bpf_tramp_run_ctx *ctx =
container_of(current->bpf_ctx, struct bpf_tramp_run_ctx, run_ctx);
return ctx->retval;
}
static void active_established_stub(struct sock *sk, struct sk_buff *skb__nullable)
{
}
static void passive_established_stub(struct sock *sk, struct sk_buff *skb)
{
}
static void rto_stub(struct sock *sk)
{
}
static void rtt_stub(struct sock *sk, long mrtt, u32 srtt)
{
}
static void set_state_stub(struct sock *sk, int state)
{
}
static void retrans_stub(struct sock *sk, struct sk_buff *skb, int err)
{
}
static void connect_stub(struct sock *sk)
{
}
static void listen_stub(struct sock *sk)
{
}
static struct bpf_tcp_ops __bpf_tcp_ops = {
.timeout_init = timeout_init_stub,
.rwnd_init = rwnd_init_stub,
.active_established = active_established_stub,
.passive_established = passive_established_stub,
.rto = rto_stub,
.rtt = rtt_stub,
.set_state = set_state_stub,
.retrans = retrans_stub,
.connect = connect_stub,
.listen = listen_stub,
};
BPF_CALL_0(bpf_tcp_ops_get_retval)
{
struct bpf_tramp_run_ctx *ctx =
container_of(current->bpf_ctx, struct bpf_tramp_run_ctx, run_ctx);
/* bpf_get_retval() is only exposed to timeout_init/rwnd_init, which
* always run via bpf_tcp_ops_call_int(). Its run_ctx carries the int
* return value chained across the bpf_tcp_ops attached to the cgroup
* and is this program's saved_run_ctx.
*/
if (WARN_ON_ONCE(!ctx->saved_run_ctx))
return 0;
return container_of(ctx->saved_run_ctx, struct bpf_tramp_run_ctx,
run_ctx)->retval;
}
const struct bpf_func_proto bpf_tcp_ops_get_retval_proto = {
.func = bpf_tcp_ops_get_retval,
.gpl_only = false,
.ret_type = RET_INTEGER,
};
static const struct bpf_func_proto *
get_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
{
u32 moff = prog->aux->attach_st_ops_member_off;
switch (func_id) {
case BPF_FUNC_sk_storage_get:
return &bpf_sk_storage_get_proto;
case BPF_FUNC_sk_storage_delete:
return &bpf_sk_storage_delete_proto;
case BPF_FUNC_setsockopt:
/* The listener is not locked. */
if (moff == offsetof(struct bpf_tcp_ops, rwnd_init) ||
moff == offsetof(struct bpf_tcp_ops, timeout_init))
return NULL;
return &bpf_sk_setsockopt_proto;
case BPF_FUNC_getsockopt:
if (moff == offsetof(struct bpf_tcp_ops, rwnd_init) ||
moff == offsetof(struct bpf_tcp_ops, timeout_init))
return NULL;
return &bpf_sk_getsockopt_proto;
case BPF_FUNC_get_retval:
if (moff == offsetof(struct bpf_tcp_ops, timeout_init) ||
moff == offsetof(struct bpf_tcp_ops, rwnd_init))
return &bpf_tcp_ops_get_retval_proto;
return NULL;
default:
return bpf_base_func_proto(func_id, prog);
}
}
static bool is_valid_access(int off, int size, enum bpf_access_type type,
const struct bpf_prog *prog, struct bpf_insn_access_aux *info)
{
if (!bpf_tracing_btf_ctx_access(off, size, type, prog, info))
return false;
if (base_type(info->reg_type) == PTR_TO_BTF_ID &&
!bpf_type_has_unsafe_modifiers(info->reg_type) &&
info->btf_id == btf_sock_ids[BTF_SOCK_TYPE_SOCK])
/* promote it to tcp_sock */
info->btf_id = btf_sock_ids[BTF_SOCK_TYPE_TCP];
return true;
}
static int bpf_tcp_ops_init_member(const struct btf_type *t,
const struct btf_member *member,
void *kdata, const void *udata)
{
return 0;
}
static int bpf_tcp_ops_check_member(const struct btf_type *t,
const struct btf_member *member,
const struct bpf_prog *prog)
{
if (prog->sleepable)
return -EINVAL;
return 0;
}
static int bpf_tcp_ops_init(struct btf *btf)
{
return 0;
}
static int bpf_tcp_ops_validate(void *kdata)
{
return 0;
}
static const struct bpf_verifier_ops bpf_tcp_ops_verifier = {
.get_func_proto = get_func_proto,
.is_valid_access = is_valid_access,
};
static struct bpf_struct_ops bpf_tcp_ops = {
.verifier_ops = &bpf_tcp_ops_verifier,
.init_member = bpf_tcp_ops_init_member,
.check_member = bpf_tcp_ops_check_member,
.init = bpf_tcp_ops_init,
.validate = bpf_tcp_ops_validate,
.name = "bpf_tcp_ops",
.cgroup_atype = CGROUP_TCP_SOCK_OPS,
.cfi_stubs = &__bpf_tcp_ops,
.owner = THIS_MODULE,
};
static int __init __bpf_tcp_ops_init(void)
{
return register_bpf_struct_ops(&bpf_tcp_ops, bpf_tcp_ops);
}
late_initcall(__bpf_tcp_ops_init);

View File

@ -2997,6 +2997,7 @@ void tcp_set_state(struct sock *sk, int state)
if (BPF_SOCK_OPS_TEST_FLAG(tcp_sk(sk), BPF_SOCK_OPS_STATE_CB_FLAG))
tcp_call_bpf_2arg(sk, BPF_SOCK_OPS_STATE_CB, oldstate, state);
bpf_tcp_ops_call(set_state, sk, state);
switch (state) {
case TCP_ESTABLISHED:

View File

@ -6724,6 +6724,10 @@ void tcp_init_transfer(struct sock *sk, int bpf_op, struct sk_buff *skb)
tp->snd_cwnd_stamp = tcp_jiffies32;
bpf_skops_established(sk, bpf_op, skb);
if (bpf_op == BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB)
bpf_tcp_ops_call(active_established, sk, skb);
else
bpf_tcp_ops_call(passive_established, sk, skb);
/* Initialize congestion control unless BPF initialized it already: */
if (!icsk->icsk_ca_initialized)
tcp_init_congestion_control(sk);

View File

@ -3678,6 +3678,7 @@ int __tcp_retransmit_skb(struct sock *sk, struct sk_buff *skb, int segs)
if (BPF_SOCK_OPS_TEST_FLAG(tp, BPF_SOCK_OPS_RETRANS_CB_FLAG))
tcp_call_bpf_3arg(sk, BPF_SOCK_OPS_RETRANS_CB,
TCP_SKB_CB(skb)->seq, segs, err);
bpf_tcp_ops_call(retrans, sk, skb, err);
if (unlikely(err) && err != -EBUSY)
NET_ADD_STATS(sock_net(sk), LINUX_MIB_TCPRETRANSFAIL, segs);
@ -4302,6 +4303,7 @@ int tcp_connect(struct sock *sk)
int err;
tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_CONNECT_CB, 0, NULL);
bpf_tcp_ops_call(connect, sk);
#if defined(CONFIG_TCP_MD5SIG) && defined(CONFIG_TCP_AO)
/* Has to be checked late, after setting daddr/saddr/ops.

View File

@ -290,6 +290,7 @@ static int tcp_write_timeout(struct sock *sk)
tcp_call_bpf_3arg(sk, BPF_SOCK_OPS_RTO_CB,
icsk->icsk_retransmits,
icsk->icsk_rto, (int)expired);
bpf_tcp_ops_call(rto, sk);
if (expired) {
/* Has it gone just too far? */