blob: 2ba73dd6c52cce9ff49a10de5fde954805463447 [file] [edit]
// 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 <linux/filter.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 void parse_hdr_stub(struct sock *sk, struct sk_buff *skb)
{
}
static void hdr_opt_len_stub(struct sock *sk, struct sk_buff *skb__nullable,
struct request_sock *req__nullable,
struct sk_buff *syn_skb__nullable,
enum tcp_synack_type synack_type,
unsigned int *remaining)
{
}
static void write_hdr_opt_stub(struct sock *sk, struct sk_buff *skb,
struct request_sock *req__nullable,
struct sk_buff *syn_skb__nullable,
enum tcp_synack_type synack_type,
u32 opt_off)
{
}
static void enqueue_rcvq_stub(struct sock *sk, struct sk_buff *skb)
{
}
static void dequeue_rcvq_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,
.parse_hdr = parse_hdr_stub,
.hdr_opt_len = hdr_opt_len_stub,
.write_hdr_opt = write_hdr_opt_stub,
.enqueue_rcvq = enqueue_rcvq_stub,
.dequeue_rcvq = dequeue_rcvq_stub,
};
BPF_CALL_4(bpf_tcp_ops_store_hdr_opt, void *, ctx, const void *, from,
u32, len, u64, flags)
{
u64 *args = ctx;
struct sk_buff *skb = (void *)(unsigned long)args[1];
struct bpf_sock_ops_kern sock_ops = {};
u32 opt_off = args[5];
u8 *op, *opend;
/*
* bpf_tcp_ops does not keep track of the end of the written TCP header
* options, so search for it every time the helper is called. The free
* space is NOP-filled, so a TCPOPT_NOP ends the search rather than being
* skipped as in a normal option walk in sockops.
*/
op = skb->data + opt_off;
opend = skb->data + tcp_hdrlen(skb);
while (op < opend && *op != TCPOPT_NOP) {
if (*op == TCPOPT_EOL || op + 1 >= opend || op[1] < 2)
break;
op += op[1];
}
sock_ops.skb = skb;
sock_ops.skb_data_end = op;
sock_ops.remaining_opt_len = opend - op;
return __bpf_sock_ops_store_hdr_opt(&sock_ops, from, len, flags);
}
static const struct bpf_func_proto bpf_tcp_ops_store_hdr_opt_proto = {
.func = bpf_tcp_ops_store_hdr_opt,
.gpl_only = false,
.ret_type = RET_INTEGER,
.arg1_type = ARG_PTR_TO_CTX,
.arg2_type = ARG_PTR_TO_MEM | MEM_RDONLY,
.arg3_type = ARG_MEM_SIZE,
.arg4_type = ARG_ANYTHING,
};
BPF_CALL_4(bpf_tcp_ops_load_hdr_opt, void *, ctx, void *, search_res,
u32, len, u64, flags)
{
u64 *args = ctx;
struct sk_buff *skb = (void *)(unsigned long)args[1];
struct bpf_sock_ops_kern sock_ops = {};
/*
* No flags supported. In particular BPF_LOAD_HDR_OPT_TCP_SYN, which
* loads from the saved SYN, is not available because bpf_tcp_ops has no
* carrier to track the SYN source across the hooks.
*/
if (flags)
return -EINVAL;
sock_ops.skb = skb;
sock_ops.skb_data_end = skb->data + tcp_hdrlen(skb);
return __bpf_sock_ops_load_hdr_opt(&sock_ops, search_res, len, flags);
}
static const struct bpf_func_proto bpf_tcp_ops_load_hdr_opt_proto = {
.func = bpf_tcp_ops_load_hdr_opt,
.gpl_only = false,
.ret_type = RET_INTEGER,
.arg1_type = ARG_PTR_TO_CTX,
.arg2_type = ARG_PTR_TO_MEM | MEM_WRITE,
.arg3_type = ARG_MEM_SIZE,
.arg4_type = ARG_ANYTHING,
};
BPF_CALL_3(bpf_tcp_ops_reserve_hdr_opt, void *, ctx, u32, len, u64, flags)
{
u64 *args = ctx;
unsigned int *remaining = (void *)(unsigned long)args[5];
if (flags || len < 2)
return -EINVAL;
if (len > *remaining)
return -ENOSPC;
*remaining -= len;
return 0;
}
static const struct bpf_func_proto bpf_tcp_ops_reserve_hdr_opt_proto = {
.func = bpf_tcp_ops_reserve_hdr_opt,
.gpl_only = false,
.ret_type = RET_INTEGER,
.arg1_type = ARG_PTR_TO_CTX,
.arg2_type = ARG_ANYTHING,
.arg3_type = ARG_ANYTHING,
};
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 bool is_sockopt_supported(u32 moff)
{
switch (moff) {
case offsetof(struct bpf_tcp_ops, active_established):
case offsetof(struct bpf_tcp_ops, passive_established):
case offsetof(struct bpf_tcp_ops, rto):
case offsetof(struct bpf_tcp_ops, rtt):
case offsetof(struct bpf_tcp_ops, set_state):
case offsetof(struct bpf_tcp_ops, retrans):
case offsetof(struct bpf_tcp_ops, connect):
case offsetof(struct bpf_tcp_ops, listen):
case offsetof(struct bpf_tcp_ops, parse_hdr):
return true;
}
return false;
}
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:
if (is_sockopt_supported(moff))
return &bpf_sk_setsockopt_proto;
return NULL;
case BPF_FUNC_getsockopt:
if (is_sockopt_supported(moff))
return &bpf_sk_getsockopt_proto;
return NULL;
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;
case BPF_FUNC_reserve_hdr_opt:
if (moff == offsetof(struct bpf_tcp_ops, hdr_opt_len))
return &bpf_tcp_ops_reserve_hdr_opt_proto;
return NULL;
case BPF_FUNC_load_hdr_opt:
if (moff == offsetof(struct bpf_tcp_ops, parse_hdr) ||
moff == offsetof(struct bpf_tcp_ops, write_hdr_opt))
return &bpf_tcp_ops_load_hdr_opt_proto;
return NULL;
case BPF_FUNC_store_hdr_opt:
if (moff == offsetof(struct bpf_tcp_ops, write_hdr_opt))
return &bpf_tcp_ops_store_hdr_opt_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,
};
__bpf_kfunc_start_defs();
__bpf_kfunc int bpf_tcp_ops_set_flags(struct tcp_sock *tp, u32 enable, u32 disable)
{
u32 old, new;
if ((enable & disable) || (enable | disable) & ~BPF_TCP_OPS_FLAG_ALL)
return -EINVAL;
if (sk_is_mptcp((struct sock *)tp) && (enable & BPF_TCP_OPS_FLAG_RCVQ))
return -EOPNOTSUPP;
old = READ_ONCE(tp->bpf_tcp_ops_flags);
do {
new = (old | enable) & ~disable;
if (new == old)
break;
} while (!try_cmpxchg(&tp->bpf_tcp_ops_flags, &old, new));
return 0;
}
__bpf_kfunc int bpf_tcp_ops_set_rcvlowat(struct sock *sk, int rcvlowat,
const struct bpf_prog_aux *aux)
{
u32 moff = aux->attach_st_ops_member_off;
bool wakeup = false;
if (moff == offsetof(struct bpf_tcp_ops, dequeue_rcvq))
wakeup = true;
if (rcvlowat < 0)
rcvlowat = INT_MAX;
return __tcp_set_rcvlowat(sk, rcvlowat, wakeup);
}
__bpf_kfunc_end_defs();
BTF_KFUNCS_START(bpf_tcp_ops_set_flags_kfunc_set)
BTF_ID_FLAGS(func, bpf_tcp_ops_set_flags)
BTF_KFUNCS_END(bpf_tcp_ops_set_flags_kfunc_set)
BTF_KFUNCS_START(bpf_tcp_ops_set_rcvlowat_kfunc_set)
BTF_ID_FLAGS(func, bpf_tcp_ops_set_rcvlowat, KF_IMPLICIT_ARGS)
BTF_KFUNCS_END(bpf_tcp_ops_set_rcvlowat_kfunc_set)
static int bpf_tcp_ops_set_flags_kfunc_filter(const struct bpf_prog *prog,
u32 kfunc_id)
{
if (!btf_id_set8_contains(&bpf_tcp_ops_set_flags_kfunc_set, kfunc_id))
return 0;
if (prog->type == BPF_PROG_TYPE_STRUCT_OPS &&
prog->aux->st_ops == &bpf_tcp_ops)
return 0;
if (prog->type == BPF_PROG_TYPE_CGROUP_SOCKOPT ||
prog->type == BPF_PROG_TYPE_CGROUP_SKB)
return 0;
return -EACCES;
}
static const struct btf_kfunc_id_set bpf_tcp_ops_set_flags_kfunc_id_set = {
.owner = THIS_MODULE,
.set = &bpf_tcp_ops_set_flags_kfunc_set,
.filter = bpf_tcp_ops_set_flags_kfunc_filter,
};
static int bpf_tcp_ops_set_rcvlowat_kfunc_filter(const struct bpf_prog *prog,
u32 kfunc_id)
{
u32 moff;
if (!btf_id_set8_contains(&bpf_tcp_ops_set_rcvlowat_kfunc_set, kfunc_id))
return 0;
if (prog->aux->st_ops != &bpf_tcp_ops)
return -EACCES;
moff = prog->aux->attach_st_ops_member_off;
if (moff != offsetof(struct bpf_tcp_ops, enqueue_rcvq) &&
moff != offsetof(struct bpf_tcp_ops, dequeue_rcvq))
return -EACCES;
return 0;
}
static const struct btf_kfunc_id_set bpf_tcp_ops_set_rcvlowat_kfunc_id_set = {
.owner = THIS_MODULE,
.set = &bpf_tcp_ops_set_rcvlowat_kfunc_set,
.filter = bpf_tcp_ops_set_rcvlowat_kfunc_filter,
};
static int __init __bpf_tcp_ops_init(void)
{
int ret;
ret = register_btf_kfunc_id_set(BPF_PROG_TYPE_STRUCT_OPS,
&bpf_tcp_ops_set_flags_kfunc_id_set);
/* BPF_PROG_TYPE_CGROUP_{SOCKOPT,SKB} share BTF_KFUNC_HOOK_CGROUP. */
ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_CGROUP_SOCKOPT,
&bpf_tcp_ops_set_flags_kfunc_id_set);
ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_STRUCT_OPS,
&bpf_tcp_ops_set_rcvlowat_kfunc_id_set);
ret = ret ?: register_bpf_struct_ops(&bpf_tcp_ops, bpf_tcp_ops);
return ret;
}
late_initcall(__bpf_tcp_ops_init);