// SPDX-License-Identifier: GPL-2.0 /* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */ #include #include #include #include #include #include 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);