:p
atchew
Login
Two BPF MPTCP packet-scheduler kfuncs accept a generic "struct sock *" but internally reinterpret it as a specific role (the MPTCP-level socket, or a subflow's TCP socket). The verifier only enforces that the argument is a trusted struct sock, so a scheduler struct_ops program can pass the wrong kind of socket; the kfunc then upcasts and dereferences it, causing wild pointer use. Both are reachable from a scheduler hook with no privilege beyond loading the scheduler. Patch 1: mptcp_set_timeout() expects the msk. A subflow socket passed instead is cast via mptcp_sk() and walked as msk->conn_list, causing a GPF. Found by an MPTCP protocol-flow harness extending BRF (arXiv:2305.08782). Fixed by narrowing the kfunc arg to struct mptcp_sock *, so the verifier rejects a non-msk socket at load. Patch 2: mptcp_pm_subflow_chk_stale()'s ssk arg is a subflow TCP socket; a non-subflow socket passed in is reinterpreted via mptcp_subflow_ctx() and both read and written through. This kfunc legitimately takes a generic socket, so it is fixed with a runtime role check in a __bpf_kfunc wrapper, like bpf_mptcp_subflow_ctx(). Patch 3: adds a negative selftest: a scheduler that passes a subflow socket to the narrowed bpf_mptcp_set_timeout() must be rejected by the verifier; the test asserts the specific load-time type-mismatch message. Patch 4: extends that selftest into a small suite guarding both socket type-confusion directions across the narrow-typed scheduler kfunc surface (mptcp_wnd_end and mptcp_subflow_set_scheduled), so the contract cannot silently regress. Patches 1 and 2 are squash-to "bpf: Export mptcp packet scheduler helpers" and update the in-tree burst scheduler selftest to the new kfunc names. Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com> --- Shardul Bankar (4): Squash to "bpf: Export mptcp packet scheduler helpers" Squash to "bpf: Export mptcp packet scheduler helpers" selftests/bpf: mptcp: verify scheduler rejects non-msk socket to set_timeout selftests/bpf: mptcp: extend bad scheduler test to the kfunc type contract net/mptcp/bpf.c | 17 +++- tools/testing/selftests/bpf/prog_tests/mptcp.c | 57 ++++++++++++ .../selftests/bpf/progs/mptcp_bpf_bad_sched.c | 100 +++++++++++++++++++++ .../testing/selftests/bpf/progs/mptcp_bpf_burst.c | 8 +- 4 files changed, 176 insertions(+), 6 deletions(-) --- base-commit: ba8940c77ff7e7f3081e7e3d8a9146000a3ff2aa change-id: 20260629-mptcp_bpf_kfunc_fixes-7ab60edc2902 Best regards, -- Shardul Bankar <shardul.b@mpiricsoftware.com>
mptcp_set_timeout() is exposed to BPF MPTCP packet schedulers as a kfunc taking a generic "struct sock *". The verifier only checks that the argument is a trusted struct sock; it cannot distinguish an MPTCP-level socket (msk) from a subflow's TCP socket. A scheduler get_send() program can therefore pass a subflow socket (e.g. msk->first, or the result of bpf_mptcp_subflow_tcp_sock()), which mptcp_set_timeout() upcasts via mptcp_sk() and iterates as msk->conn_list. On a subflow socket those bytes are live TCP state, so the walk yields a wild mptcp_subflow_context and the subsequent subflow->tcp_sock dereference faults (GPF / KASAN user-memory-access). Narrow the kfunc-facing type: register a bpf_mptcp_set_timeout() wrapper taking "struct mptcp_sock *" instead of the raw mptcp_set_timeout() symbol, so the verifier's BTF-id check rejects a non-msk socket at program load time. A scheduler that passes its msk is unaffected; update the in-tree burst scheduler selftest accordingly. Found by an MPTCP protocol-flow harness extending BRF (arXiv:2305.08782). Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com> --- net/mptcp/bpf.c | 7 ++++++- tools/testing/selftests/bpf/progs/mptcp_bpf_burst.c | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/net/mptcp/bpf.c b/net/mptcp/bpf.c index XXXXXXX..XXXXXXX 100644 --- a/net/mptcp/bpf.c +++ b/net/mptcp/bpf.c @@ -XXX,XX +XXX,XX @@ bpf_sk_stream_memory_free(const struct mptcp_subflow_context *subflow) return false; } +__bpf_kfunc static void bpf_mptcp_set_timeout(struct mptcp_sock *msk) +{ + mptcp_set_timeout((struct sock *)msk); +} + __bpf_kfunc_end_defs(); BTF_KFUNCS_START(bpf_mptcp_iter_kfunc_ids) @@ -XXX,XX +XXX,XX @@ BTF_ID_FLAGS(func, bpf_mptcp_subflow_ctx, KF_RET_NULL) BTF_ID_FLAGS(func, bpf_mptcp_subflow_tcp_sock, KF_RET_NULL) BTF_ID_FLAGS(func, mptcp_subflow_set_scheduled) BTF_ID_FLAGS(func, mptcp_subflow_active) -BTF_ID_FLAGS(func, mptcp_set_timeout) +BTF_ID_FLAGS(func, bpf_mptcp_set_timeout) BTF_ID_FLAGS(func, mptcp_wnd_end) BTF_ID_FLAGS(func, bpf_sk_stream_memory_free) BTF_ID_FLAGS(func, mptcp_pm_subflow_chk_stale, KF_SLEEPABLE) diff --git a/tools/testing/selftests/bpf/progs/mptcp_bpf_burst.c b/tools/testing/selftests/bpf/progs/mptcp_bpf_burst.c index XXXXXXX..XXXXXXX 100644 --- a/tools/testing/selftests/bpf/progs/mptcp_bpf_burst.c +++ b/tools/testing/selftests/bpf/progs/mptcp_bpf_burst.c @@ -XXX,XX +XXX,XX @@ struct bpf_subflow_send_info { #define RB_EMPTY_ROOT(root) (READ_ONCE((root)->rb_node) == NULL) extern bool mptcp_subflow_active(struct mptcp_subflow_context *subflow) __ksym; -extern void mptcp_set_timeout(struct sock *sk) __ksym; +extern void bpf_mptcp_set_timeout(struct mptcp_sock *msk) __ksym; extern __u64 mptcp_wnd_end(const struct mptcp_sock *msk) __ksym; extern bool bpf_sk_stream_memory_free(const struct mptcp_subflow_context *subflow) __ksym; extern void mptcp_pm_subflow_chk_stale(const struct mptcp_sock *msk, struct sock *ssk) __ksym; @@ -XXX,XX +XXX,XX @@ int BPF_PROG(bpf_burst_get_send, struct mptcp_sock *msk) send_info[backup].linger_time = linger_time; } } - mptcp_set_timeout(sk); + bpf_mptcp_set_timeout(msk); /* pick the best backup if no other subflow is active */ if (!nr_active) -- 2.34.1
mptcp_pm_subflow_chk_stale() is exposed to BPF MPTCP packet schedulers as a kfunc whose second argument is a generic "struct sock *ssk". The verifier only checks that the argument is a trusted struct sock; it cannot tell a subflow's TCP socket from any other socket. A scheduler get_retrans() program can therefore pass a non-subflow socket (e.g. (struct sock *)msk, or any other full socket), and mptcp_pm_subflow_chk_stale() reinterprets it via mptcp_subflow_ctx(), which is an unchecked cast of inet_csk(ssk)->icsk_ulp_data. On a non-subflow socket the derived subflow is bogus, and the function reads tcp_sk(ssk) state and writes through that bogus pointer (subflow->stale_rcv_tstamp, subflow->stale_count). This kfunc legitimately operates on a subflow TCP socket, so the fix is a runtime role check rather than a narrower arg type: register a bpf_mptcp_pm_subflow_chk_stale() wrapper that validates ssk is a full MPTCP subflow TCP socket before calling the internal helper, like bpf_mptcp_subflow_ctx(). A scheduler passing a real subflow socket is unaffected; update the in-tree burst scheduler selftest accordingly. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com> --- net/mptcp/bpf.c | 10 +++++++++- tools/testing/selftests/bpf/progs/mptcp_bpf_burst.c | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/net/mptcp/bpf.c b/net/mptcp/bpf.c index XXXXXXX..XXXXXXX 100644 --- a/net/mptcp/bpf.c +++ b/net/mptcp/bpf.c @@ -XXX,XX +XXX,XX @@ __bpf_kfunc static void bpf_mptcp_set_timeout(struct mptcp_sock *msk) mptcp_set_timeout((struct sock *)msk); } +__bpf_kfunc static void +bpf_mptcp_pm_subflow_chk_stale(const struct mptcp_sock *msk, struct sock *ssk) +{ + if (ssk && sk_fullsock(ssk) && ssk->sk_type == SOCK_STREAM && + ssk->sk_protocol == IPPROTO_TCP && sk_is_mptcp(ssk)) + mptcp_pm_subflow_chk_stale(msk, ssk); +} + __bpf_kfunc_end_defs(); BTF_KFUNCS_START(bpf_mptcp_iter_kfunc_ids) @@ -XXX,XX +XXX,XX @@ BTF_ID_FLAGS(func, mptcp_subflow_active) BTF_ID_FLAGS(func, bpf_mptcp_set_timeout) BTF_ID_FLAGS(func, mptcp_wnd_end) BTF_ID_FLAGS(func, bpf_sk_stream_memory_free) -BTF_ID_FLAGS(func, mptcp_pm_subflow_chk_stale, KF_SLEEPABLE) +BTF_ID_FLAGS(func, bpf_mptcp_pm_subflow_chk_stale, KF_SLEEPABLE) BTF_KFUNCS_END(bpf_mptcp_common_kfunc_ids) static int bpf_mptcp_common_kfunc_filter(const struct bpf_prog *prog, u32 kfunc_id) diff --git a/tools/testing/selftests/bpf/progs/mptcp_bpf_burst.c b/tools/testing/selftests/bpf/progs/mptcp_bpf_burst.c index XXXXXXX..XXXXXXX 100644 --- a/tools/testing/selftests/bpf/progs/mptcp_bpf_burst.c +++ b/tools/testing/selftests/bpf/progs/mptcp_bpf_burst.c @@ -XXX,XX +XXX,XX @@ extern bool mptcp_subflow_active(struct mptcp_subflow_context *subflow) __ksym; extern void bpf_mptcp_set_timeout(struct mptcp_sock *msk) __ksym; extern __u64 mptcp_wnd_end(const struct mptcp_sock *msk) __ksym; extern bool bpf_sk_stream_memory_free(const struct mptcp_subflow_context *subflow) __ksym; -extern void mptcp_pm_subflow_chk_stale(const struct mptcp_sock *msk, struct sock *ssk) __ksym; +extern void bpf_mptcp_pm_subflow_chk_stale(const struct mptcp_sock *msk, struct sock *ssk) __ksym; static __always_inline __u64 div_u64(__u64 dividend, __u32 divisor) { @@ -XXX,XX +XXX,XX @@ int BPF_PROG(bpf_burst_get_retrans, struct mptcp_sock *msk) /* still data outstanding at TCP level? skip this */ if (!tcp_rtx_and_write_queues_empty(ssk)) { - mptcp_pm_subflow_chk_stale(msk, ssk); + bpf_mptcp_pm_subflow_chk_stale(msk, ssk); min_stale_count = min(min_stale_count, subflow->stale_count); continue; } -- 2.34.1
Add a negative test for the bpf_mptcp_set_timeout() kfunc: a BPF MPTCP scheduler whose get_send() passes a subflow TCP socket to it, where the kfunc takes a struct mptcp_sock *, must be rejected by the verifier at program load time. This guards against widening the kfunc argument back to a generic struct sock *, which would reintroduce the socket type confusion between an MPTCP-level socket and a subflow TCP socket. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com> --- tools/testing/selftests/bpf/prog_tests/mptcp.c | 31 +++++++++++++ .../selftests/bpf/progs/mptcp_bpf_bad_sched.c | 54 ++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/tools/testing/selftests/bpf/prog_tests/mptcp.c b/tools/testing/selftests/bpf/prog_tests/mptcp.c index XXXXXXX..XXXXXXX 100644 --- a/tools/testing/selftests/bpf/prog_tests/mptcp.c +++ b/tools/testing/selftests/bpf/prog_tests/mptcp.c @@ -XXX,XX +XXX,XX @@ #include "mptcp_bpf_rr.skel.h" #include "mptcp_bpf_red.skel.h" #include "mptcp_bpf_burst.skel.h" +#include "mptcp_bpf_bad_sched.skel.h" #define NS_TEST "mptcp_ns" #define ADDR_1 "10.0.1.1" @@ -XXX,XX +XXX,XX @@ static void test_burst(void) mptcp_bpf_burst__destroy(skel); } +static void test_bad_sched(void) +{ + struct mptcp_bpf_bad_sched *skel; + char *log = NULL; + int err; + + /* bad_sched_get_send() passes a subflow TCP socket to + * bpf_mptcp_set_timeout(), which takes a struct mptcp_sock *. The + * verifier must reject this socket type confusion at load time, and + * for the right reason -- assert the specific verifier message. + */ + skel = mptcp_bpf_bad_sched__open(); + if (!ASSERT_OK_PTR(skel, "open: bad_sched")) + return; + + if (start_libbpf_log_capture()) + goto destroy; + + err = mptcp_bpf_bad_sched__load(skel); + log = stop_libbpf_log_capture(); + ASSERT_ERR(err, "load: bad_sched must be rejected"); + ASSERT_HAS_SUBSTR(log, "expected pointer to STRUCT mptcp_sock", + "verifier rejects subflow sock to bpf_mptcp_set_timeout"); + free(log); +destroy: + mptcp_bpf_bad_sched__destroy(skel); +} + void test_mptcp(void) { if (test__start_subtest("base")) @@ -XXX,XX +XXX,XX @@ void test_mptcp(void) test_red(); if (test__start_subtest("burst")) test_burst(); + if (test__start_subtest("bad_sched")) + test_bad_sched(); } diff --git a/tools/testing/selftests/bpf/progs/mptcp_bpf_bad_sched.c b/tools/testing/selftests/bpf/progs/mptcp_bpf_bad_sched.c new file mode 100644 index XXXXXXX..XXXXXXX --- /dev/null +++ b/tools/testing/selftests/bpf/progs/mptcp_bpf_bad_sched.c @@ -XXX,XX +XXX,XX @@ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright (c) 2026, Mpiric Software. */ + +/* A scheduler that passes a subflow TCP socket to bpf_mptcp_set_timeout(), + * which takes a struct mptcp_sock *. The verifier must reject this at load + * time; see the bad_sched subtest in prog_tests/mptcp.c. + */ +#include "mptcp_bpf.h" +#include <bpf/bpf_tracing.h> + +char _license[] SEC("license") = "GPL"; + +extern void bpf_mptcp_set_timeout(struct mptcp_sock *msk) __ksym; + +SEC("struct_ops") +void BPF_PROG(bad_sched_init, struct mptcp_sock *msk) +{ +} + +SEC("struct_ops") +void BPF_PROG(bad_sched_release, struct mptcp_sock *msk) +{ +} + +SEC("struct_ops") +int BPF_PROG(bad_sched_get_send, struct mptcp_sock *msk) +{ + struct mptcp_subflow_context *subflow; + struct sock *ssk; + + bpf_for_each(mptcp_subflow, subflow, (struct sock *)msk) { + ssk = bpf_mptcp_subflow_tcp_sock(subflow); + if (!ssk) + return -1; + /* ssk is a subflow TCP socket (struct sock *), not an msk. + * Passing it to bpf_mptcp_set_timeout(), which takes a + * struct mptcp_sock *, is a socket type confusion that the + * verifier must reject at load time ("expected pointer to + * STRUCT mptcp_sock"). + */ + bpf_mptcp_set_timeout((struct mptcp_sock *)ssk); + mptcp_subflow_set_scheduled(subflow, true); + return 0; + } + return -1; +} + +SEC(".struct_ops.link") +struct mptcp_sched_ops bad_sched = { + .init = (void *)bad_sched_init, + .release = (void *)bad_sched_release, + .get_send = (void *)bad_sched_get_send, + .name = "bpf_bad_sched", +}; -- 2.34.1
The bad_sched test covers one socket type confusion: a subflow TCP socket passed to bpf_mptcp_set_timeout(), which takes a struct mptcp_sock *. Extend it into a small suite that guards the narrow-typed scheduler kfunc surface against that bug class, covering both confusion directions: - a subflow sock passed to mptcp_wnd_end() (struct mptcp_sock *), the same direction as the set_timeout case; - the msk passed to mptcp_subflow_set_scheduled() (struct mptcp_subflow_context *), the inverse direction. Each malicious scheduler is its own struct_ops map. They are loaded one at a time via bpf_map__set_autocreate(), so each load failure is checked against its own verifier type-mismatch message. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com> --- tools/testing/selftests/bpf/prog_tests/mptcp.c | 56 ++++++++++++++++------ .../selftests/bpf/progs/mptcp_bpf_bad_sched.c | 46 ++++++++++++++++++ 2 files changed, 87 insertions(+), 15 deletions(-) diff --git a/tools/testing/selftests/bpf/prog_tests/mptcp.c b/tools/testing/selftests/bpf/prog_tests/mptcp.c index XXXXXXX..XXXXXXX 100644 --- a/tools/testing/selftests/bpf/prog_tests/mptcp.c +++ b/tools/testing/selftests/bpf/prog_tests/mptcp.c @@ -XXX,XX +XXX,XX @@ static void test_burst(void) mptcp_bpf_burst__destroy(skel); } -static void test_bad_sched(void) +static void load_bad_sched(struct mptcp_bpf_bad_sched *skel, const char *msg) { - struct mptcp_bpf_bad_sched *skel; char *log = NULL; int err; - /* bad_sched_get_send() passes a subflow TCP socket to - * bpf_mptcp_set_timeout(), which takes a struct mptcp_sock *. The - * verifier must reject this socket type confusion at load time, and - * for the right reason -- assert the specific verifier message. + if (start_libbpf_log_capture()) + return; + + err = mptcp_bpf_bad_sched__load(skel); + log = stop_libbpf_log_capture(); + ASSERT_ERR(err, "load: bad scheduler must be rejected"); + ASSERT_HAS_SUBSTR(log, msg, "verifier type-mismatch message"); + free(log); +} + +static void test_bad_sched(void) +{ + struct mptcp_bpf_bad_sched *skel; + + /* Each scheduler in mptcp_bpf_bad_sched passes a wrong-subtype socket + * to a narrow-typed scheduler kfunc; the verifier must reject each at + * load time with the specific type-mismatch message. The skel holds + * several such schedulers, so enable one struct_ops map at a time -- + * loading them together would fail atomically. */ + + /* subflow sock -> bpf_mptcp_set_timeout(struct mptcp_sock *) */ skel = mptcp_bpf_bad_sched__open(); if (!ASSERT_OK_PTR(skel, "open: bad_sched")) return; + bpf_map__set_autocreate(skel->maps.bad_wnd_end, false); + bpf_map__set_autocreate(skel->maps.bad_set_sched, false); + load_bad_sched(skel, "expected pointer to STRUCT mptcp_sock"); + mptcp_bpf_bad_sched__destroy(skel); - if (start_libbpf_log_capture()) - goto destroy; + /* subflow sock -> mptcp_wnd_end(struct mptcp_sock *) */ + skel = mptcp_bpf_bad_sched__open(); + if (!ASSERT_OK_PTR(skel, "open: bad_wnd_end")) + return; + bpf_map__set_autocreate(skel->maps.bad_sched, false); + bpf_map__set_autocreate(skel->maps.bad_set_sched, false); + load_bad_sched(skel, "expected pointer to STRUCT mptcp_sock"); + mptcp_bpf_bad_sched__destroy(skel); - err = mptcp_bpf_bad_sched__load(skel); - log = stop_libbpf_log_capture(); - ASSERT_ERR(err, "load: bad_sched must be rejected"); - ASSERT_HAS_SUBSTR(log, "expected pointer to STRUCT mptcp_sock", - "verifier rejects subflow sock to bpf_mptcp_set_timeout"); - free(log); -destroy: + /* msk -> mptcp_subflow_set_scheduled(struct mptcp_subflow_context *) */ + skel = mptcp_bpf_bad_sched__open(); + if (!ASSERT_OK_PTR(skel, "open: bad_set_sched")) + return; + bpf_map__set_autocreate(skel->maps.bad_sched, false); + bpf_map__set_autocreate(skel->maps.bad_wnd_end, false); + load_bad_sched(skel, "expected pointer to STRUCT mptcp_subflow_context"); mptcp_bpf_bad_sched__destroy(skel); } diff --git a/tools/testing/selftests/bpf/progs/mptcp_bpf_bad_sched.c b/tools/testing/selftests/bpf/progs/mptcp_bpf_bad_sched.c index XXXXXXX..XXXXXXX 100644 --- a/tools/testing/selftests/bpf/progs/mptcp_bpf_bad_sched.c +++ b/tools/testing/selftests/bpf/progs/mptcp_bpf_bad_sched.c @@ -XXX,XX +XXX,XX @@ char _license[] SEC("license") = "GPL"; extern void bpf_mptcp_set_timeout(struct mptcp_sock *msk) __ksym; +extern __u64 mptcp_wnd_end(const struct mptcp_sock *msk) __ksym; SEC("struct_ops") void BPF_PROG(bad_sched_init, struct mptcp_sock *msk) @@ -XXX,XX +XXX,XX @@ struct mptcp_sched_ops bad_sched = { .get_send = (void *)bad_sched_get_send, .name = "bpf_bad_sched", }; + +/* Same confusion class as bad_sched, on another struct mptcp_sock * kfunc: + * feed a subflow TCP socket to mptcp_wnd_end(). The verifier must reject it + * ("expected pointer to STRUCT mptcp_sock"). get_send is the only required + * scheduler op, so the rest are omitted. + */ +SEC("struct_ops") +int BPF_PROG(bad_wnd_end_get_send, struct mptcp_sock *msk) +{ + struct mptcp_subflow_context *subflow; + struct sock *ssk; + + bpf_for_each(mptcp_subflow, subflow, (struct sock *)msk) { + ssk = bpf_mptcp_subflow_tcp_sock(subflow); + if (!ssk) + return -1; + if (mptcp_wnd_end((struct mptcp_sock *)ssk)) + return 0; + return -1; + } + return -1; +} + +SEC(".struct_ops.link") +struct mptcp_sched_ops bad_wnd_end = { + .get_send = (void *)bad_wnd_end_get_send, + .name = "bpf_bad_wnd_end", +}; + +/* Inverse confusion: feed the msk to mptcp_subflow_set_scheduled(), which + * takes a struct mptcp_subflow_context *. The verifier must reject it + * ("expected pointer to STRUCT mptcp_subflow_context"). + */ +SEC("struct_ops") +int BPF_PROG(bad_set_sched_get_send, struct mptcp_sock *msk) +{ + mptcp_subflow_set_scheduled((struct mptcp_subflow_context *)msk, true); + return 0; +} + +SEC(".struct_ops.link") +struct mptcp_sched_ops bad_set_sched = { + .get_send = (void *)bad_set_sched_get_send, + .name = "bpf_bad_set_sch", +}; -- 2.34.1
Two BPF MPTCP packet-scheduler kfuncs accept a generic "struct sock *" but internally reinterpret it as a specific role (the MPTCP-level socket, or a subflow's TCP socket). The verifier only enforces that the argument is a trusted struct sock, so a scheduler struct_ops program can pass the wrong kind of socket; the kfunc then upcasts and dereferences it, causing wild pointer use. Both are reachable from a scheduler hook with no privilege beyond loading the scheduler. Patch 1 (squash-to "bpf: Export mptcp packet scheduler helpers"): mptcp_set_timeout() expects the msk. A subflow socket passed instead is cast via mptcp_sk() and walked as msk->conn_list, causing a GPF. Found by an MPTCP protocol-flow harness extending BRF (arXiv:2305.08782). Fixed by narrowing the kfunc arg to struct mptcp_sock *, so the verifier rejects a non-msk socket at load. Patch 2 (squash-to "bpf: Export mptcp packet scheduler helpers"): mptcp_pm_subflow_chk_stale()'s ssk arg is a subflow TCP socket; a non-subflow socket passed in is reinterpreted via mptcp_subflow_ctx() and both read and written through. This kfunc legitimately takes a generic socket, so it is fixed with a runtime role check in a __bpf_kfunc wrapper, like bpf_mptcp_subflow_ctx(). The wrapper also confirms ssk belongs to the passed msk; this is hardening for a currently-unreachable surface, kept correct if it widens. Patch 3 (squash-to "selftests/bpf: Add bpf_burst scheduler & test"): update the in-tree burst scheduler selftest to the new wrapper kfunc names so it keeps building and loading. Split from patches 1 and 2 so each squash-to lands on the commit it fixes. Patches 4 and 5 (DO-NOT-MERGE): negative selftests validating the verifier rejects the type confusion. Not intended for propagation to net-next; like the other DO-NOT-MERGE checks they can live in the MPTCP tree so the CI keeps exercising the kfunc type contract (per the v1 review). Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com> --- Changes in v2: - Split the burst-scheduler selftest kfunc rename out of the two net/mptcp/bpf.c squash-tos into its own squash-to targeting "selftests/bpf: Add bpf_burst scheduler & test" (Geliang). - Patch 2: the wrapper now also verifies the subflow belongs to the msk (subflow->conn == msk) and NULL-checks the context (the Sashiko/Geliang ownership point). - Marked the two negative selftests DO-NOT-MERGE so they are not propagated to net-next (Geliang: the dedicated bad scheduler is not for integration). - Link to v1: https://patch.msgid.link/20260629-mptcp_bpf_kfunc_fixes-v1-0-8cc875f36f53@mpiricsoftware.com --- Shardul Bankar (5): Squash to "bpf: Export mptcp packet scheduler helpers" Squash to "bpf: Export mptcp packet scheduler helpers" Squash to "selftests/bpf: Add bpf_burst scheduler & test" DO-NOT-MERGE: selftests/bpf: mptcp: verify scheduler rejects non-msk socket to set_timeout DO-NOT-MERGE: selftests/bpf: mptcp: extend bad scheduler test to the kfunc type contract net/mptcp/bpf.c | 21 ++++- tools/testing/selftests/bpf/prog_tests/mptcp.c | 58 ++++++++++++ .../selftests/bpf/progs/mptcp_bpf_bad_sched.c | 104 +++++++++++++++++++++ .../testing/selftests/bpf/progs/mptcp_bpf_burst.c | 8 +- 4 files changed, 185 insertions(+), 6 deletions(-) --- base-commit: 0d7f76b2394b7a20a1ec100557cd7905ee0ab40e change-id: 20260629-mptcp_bpf_kfunc_fixes-7ab60edc2902 Best regards, -- Shardul Bankar <shardul.b@mpiricsoftware.com>
mptcp_set_timeout() is exposed to BPF MPTCP packet schedulers as a kfunc taking a generic "struct sock *". The verifier only checks that the argument is a trusted struct sock; it cannot distinguish an MPTCP-level socket (msk) from a subflow's TCP socket. A scheduler get_send() program can therefore pass a subflow socket (e.g. msk->first, or the result of bpf_mptcp_subflow_tcp_sock()), which mptcp_set_timeout() upcasts via mptcp_sk() and iterates as msk->conn_list. On a subflow socket those bytes are live TCP state, so the walk yields a wild mptcp_subflow_context and the subsequent subflow->tcp_sock dereference faults (GPF / KASAN user-memory-access). Narrow the kfunc-facing type: register a bpf_mptcp_set_timeout() wrapper taking "struct mptcp_sock *" instead of the raw mptcp_set_timeout() symbol, so the verifier's BTF-id check rejects a non-msk socket at program load time. A scheduler that passes its msk is unaffected. The in-tree burst scheduler selftest is updated to the wrapper name in a separate squash-to. Found by an MPTCP protocol-flow harness extending BRF (arXiv:2305.08782). Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com> --- net/mptcp/bpf.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/net/mptcp/bpf.c b/net/mptcp/bpf.c index XXXXXXX..XXXXXXX 100644 --- a/net/mptcp/bpf.c +++ b/net/mptcp/bpf.c @@ -XXX,XX +XXX,XX @@ bpf_sk_stream_memory_free(const struct mptcp_subflow_context *subflow) return false; } +__bpf_kfunc static void bpf_mptcp_set_timeout(struct mptcp_sock *msk) +{ + mptcp_set_timeout((struct sock *)msk); +} + __bpf_kfunc_end_defs(); BTF_KFUNCS_START(bpf_mptcp_iter_kfunc_ids) @@ -XXX,XX +XXX,XX @@ BTF_ID_FLAGS(func, bpf_mptcp_subflow_ctx, KF_RET_NULL) BTF_ID_FLAGS(func, bpf_mptcp_subflow_tcp_sock, KF_RET_NULL) BTF_ID_FLAGS(func, mptcp_subflow_set_scheduled) BTF_ID_FLAGS(func, mptcp_subflow_active) -BTF_ID_FLAGS(func, mptcp_set_timeout) +BTF_ID_FLAGS(func, bpf_mptcp_set_timeout) BTF_ID_FLAGS(func, mptcp_wnd_end) BTF_ID_FLAGS(func, bpf_sk_stream_memory_free) BTF_ID_FLAGS(func, mptcp_pm_subflow_chk_stale, KF_SLEEPABLE) -- 2.34.1
mptcp_pm_subflow_chk_stale() is exposed to BPF MPTCP packet schedulers as a kfunc taking a generic "struct sock *ssk", but it treats ssk as a subflow TCP socket: it derives the subflow context with mptcp_subflow_ctx(), an unchecked cast of inet_csk(ssk)->icsk_ulp_data, then reads and writes through it. The verifier only proves ssk is a trusted struct sock, not that it is one of msk's subflows, so a mistyped or foreign socket would make the helper operate on a bogus context. Register a bpf_mptcp_pm_subflow_chk_stale() wrapper that validates ssk is a full MPTCP subflow TCP socket belonging to the passed msk before calling the helper, which assumes both but checks neither. This mirrors bpf_mptcp_subflow_ctx(). A scheduler passing one of its own subflows is unaffected; the in-tree burst scheduler selftest is updated to the wrapper name in a separate squash-to. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com> --- net/mptcp/bpf.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/net/mptcp/bpf.c b/net/mptcp/bpf.c index XXXXXXX..XXXXXXX 100644 --- a/net/mptcp/bpf.c +++ b/net/mptcp/bpf.c @@ -XXX,XX +XXX,XX @@ __bpf_kfunc static void bpf_mptcp_set_timeout(struct mptcp_sock *msk) mptcp_set_timeout((struct sock *)msk); } +__bpf_kfunc static void +bpf_mptcp_pm_subflow_chk_stale(const struct mptcp_sock *msk, struct sock *ssk) +{ + if (ssk && sk_fullsock(ssk) && ssk->sk_type == SOCK_STREAM && + ssk->sk_protocol == IPPROTO_TCP && sk_is_mptcp(ssk)) { + struct mptcp_subflow_context *subflow = mptcp_subflow_ctx(ssk); + + if (subflow && subflow->conn == (const struct sock *)msk) + mptcp_pm_subflow_chk_stale(msk, ssk); + } +} + __bpf_kfunc_end_defs(); BTF_KFUNCS_START(bpf_mptcp_iter_kfunc_ids) @@ -XXX,XX +XXX,XX @@ BTF_ID_FLAGS(func, mptcp_subflow_active) BTF_ID_FLAGS(func, bpf_mptcp_set_timeout) BTF_ID_FLAGS(func, mptcp_wnd_end) BTF_ID_FLAGS(func, bpf_sk_stream_memory_free) -BTF_ID_FLAGS(func, mptcp_pm_subflow_chk_stale, KF_SLEEPABLE) +BTF_ID_FLAGS(func, bpf_mptcp_pm_subflow_chk_stale, KF_SLEEPABLE) BTF_KFUNCS_END(bpf_mptcp_common_kfunc_ids) static int bpf_mptcp_common_kfunc_filter(const struct bpf_prog *prog, u32 kfunc_id) -- 2.34.1
The bpf_burst scheduler selftest calls the mptcp_set_timeout() and mptcp_pm_subflow_chk_stale() kfuncs by their raw names. The companion squash-to patches to "bpf: Export mptcp packet scheduler helpers" narrow those kfuncs behind bpf_mptcp_set_timeout() and bpf_mptcp_pm_subflow_chk_stale() wrappers. Update the burst scheduler's extern declarations and call sites to the wrapper names so it keeps building and loading. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com> --- tools/testing/selftests/bpf/progs/mptcp_bpf_burst.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/testing/selftests/bpf/progs/mptcp_bpf_burst.c b/tools/testing/selftests/bpf/progs/mptcp_bpf_burst.c index XXXXXXX..XXXXXXX 100644 --- a/tools/testing/selftests/bpf/progs/mptcp_bpf_burst.c +++ b/tools/testing/selftests/bpf/progs/mptcp_bpf_burst.c @@ -XXX,XX +XXX,XX @@ struct bpf_subflow_send_info { #define RB_EMPTY_ROOT(root) (READ_ONCE((root)->rb_node) == NULL) extern bool mptcp_subflow_active(struct mptcp_subflow_context *subflow) __ksym; -extern void mptcp_set_timeout(struct sock *sk) __ksym; +extern void bpf_mptcp_set_timeout(struct mptcp_sock *msk) __ksym; extern __u64 mptcp_wnd_end(const struct mptcp_sock *msk) __ksym; extern bool bpf_sk_stream_memory_free(const struct mptcp_subflow_context *subflow) __ksym; -extern void mptcp_pm_subflow_chk_stale(const struct mptcp_sock *msk, struct sock *ssk) __ksym; +extern void bpf_mptcp_pm_subflow_chk_stale(const struct mptcp_sock *msk, struct sock *ssk) __ksym; static __always_inline __u64 div_u64(__u64 dividend, __u32 divisor) { @@ -XXX,XX +XXX,XX @@ int BPF_PROG(bpf_burst_get_send, struct mptcp_sock *msk) send_info[backup].linger_time = linger_time; } } - mptcp_set_timeout(sk); + bpf_mptcp_set_timeout(msk); /* pick the best backup if no other subflow is active */ if (!nr_active) @@ -XXX,XX +XXX,XX @@ int BPF_PROG(bpf_burst_get_retrans, struct mptcp_sock *msk) /* still data outstanding at TCP level? skip this */ if (!tcp_rtx_and_write_queues_empty(ssk)) { - mptcp_pm_subflow_chk_stale(msk, ssk); + bpf_mptcp_pm_subflow_chk_stale(msk, ssk); min_stale_count = min(min_stale_count, subflow->stale_count); continue; } -- 2.34.1
Add a negative test for the bpf_mptcp_set_timeout() kfunc: a BPF MPTCP scheduler whose get_send() passes a subflow TCP socket to it, where the kfunc takes a struct mptcp_sock *, must be rejected by the verifier at program load time. This guards against widening the kfunc argument back to a generic struct sock *, which would reintroduce the socket type confusion between an MPTCP-level socket and a subflow TCP socket. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com> --- tools/testing/selftests/bpf/prog_tests/mptcp.c | 32 +++++++++++++ .../selftests/bpf/progs/mptcp_bpf_bad_sched.c | 56 ++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/tools/testing/selftests/bpf/prog_tests/mptcp.c b/tools/testing/selftests/bpf/prog_tests/mptcp.c index XXXXXXX..XXXXXXX 100644 --- a/tools/testing/selftests/bpf/prog_tests/mptcp.c +++ b/tools/testing/selftests/bpf/prog_tests/mptcp.c @@ -XXX,XX +XXX,XX @@ #include "mptcp_bpf_rr.skel.h" #include "mptcp_bpf_red.skel.h" #include "mptcp_bpf_burst.skel.h" +#include "mptcp_bpf_bad_sched.skel.h" #define NS_TEST "mptcp_ns" #define ADDR_1 "10.0.1.1" @@ -XXX,XX +XXX,XX @@ static void test_burst(void) mptcp_bpf_burst__destroy(skel); } +static void test_bad_sched(void) +{ + struct mptcp_bpf_bad_sched *skel; + char *log = NULL; + int err; + + /* + * bad_sched_get_send() passes a subflow TCP socket to + * bpf_mptcp_set_timeout(), which takes a struct mptcp_sock *. The + * verifier must reject this socket type confusion at load time, and + * for the right reason: assert the specific verifier message. + */ + skel = mptcp_bpf_bad_sched__open(); + if (!ASSERT_OK_PTR(skel, "open: bad_sched")) + return; + + if (start_libbpf_log_capture()) + goto destroy; + + err = mptcp_bpf_bad_sched__load(skel); + log = stop_libbpf_log_capture(); + ASSERT_ERR(err, "load: bad_sched must be rejected"); + ASSERT_HAS_SUBSTR(log, "expected pointer to STRUCT mptcp_sock", + "verifier rejects subflow sock to bpf_mptcp_set_timeout"); + free(log); +destroy: + mptcp_bpf_bad_sched__destroy(skel); +} + void test_mptcp(void) { if (test__start_subtest("base")) @@ -XXX,XX +XXX,XX @@ void test_mptcp(void) test_red(); if (test__start_subtest("burst")) test_burst(); + if (test__start_subtest("bad_sched")) + test_bad_sched(); } diff --git a/tools/testing/selftests/bpf/progs/mptcp_bpf_bad_sched.c b/tools/testing/selftests/bpf/progs/mptcp_bpf_bad_sched.c new file mode 100644 index XXXXXXX..XXXXXXX --- /dev/null +++ b/tools/testing/selftests/bpf/progs/mptcp_bpf_bad_sched.c @@ -XXX,XX +XXX,XX @@ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright (c) 2026, Mpiric Software. */ + +/* + * A scheduler that passes a subflow TCP socket to bpf_mptcp_set_timeout(), + * which takes a struct mptcp_sock *. The verifier must reject this at load + * time; see the bad_sched subtest in prog_tests/mptcp.c. + */ +#include "mptcp_bpf.h" +#include <bpf/bpf_tracing.h> + +char _license[] SEC("license") = "GPL"; + +extern void bpf_mptcp_set_timeout(struct mptcp_sock *msk) __ksym; + +SEC("struct_ops") +void BPF_PROG(bad_sched_init, struct mptcp_sock *msk) +{ +} + +SEC("struct_ops") +void BPF_PROG(bad_sched_release, struct mptcp_sock *msk) +{ +} + +SEC("struct_ops") +int BPF_PROG(bad_sched_get_send, struct mptcp_sock *msk) +{ + struct mptcp_subflow_context *subflow; + struct sock *ssk; + + bpf_for_each(mptcp_subflow, subflow, (struct sock *)msk) { + ssk = bpf_mptcp_subflow_tcp_sock(subflow); + if (!ssk) + return -1; + /* + * ssk is a subflow TCP socket (struct sock *), not an msk. + * Passing it to bpf_mptcp_set_timeout(), which takes a + * struct mptcp_sock *, is a socket type confusion that the + * verifier must reject at load time ("expected pointer to + * STRUCT mptcp_sock"). + */ + bpf_mptcp_set_timeout((struct mptcp_sock *)ssk); + mptcp_subflow_set_scheduled(subflow, true); + return 0; + } + return -1; +} + +SEC(".struct_ops.link") +struct mptcp_sched_ops bad_sched = { + .init = (void *)bad_sched_init, + .release = (void *)bad_sched_release, + .get_send = (void *)bad_sched_get_send, + .name = "bpf_bad_sched", +}; -- 2.34.1
The bad_sched test covers one socket type confusion: a subflow TCP socket passed to bpf_mptcp_set_timeout(), which takes a struct mptcp_sock *. Extend it into a small suite that guards the narrow-typed scheduler kfunc surface against that bug class, covering both confusion directions: - a subflow sock passed to mptcp_wnd_end() (struct mptcp_sock *), the same direction as the set_timeout case; - the msk passed to mptcp_subflow_set_scheduled() (struct mptcp_subflow_context *), the inverse direction. Each malicious scheduler is its own struct_ops map. They are loaded one at a time via bpf_map__set_autocreate(), so each load failure is checked against its own verifier type-mismatch message. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com> --- tools/testing/selftests/bpf/prog_tests/mptcp.c | 56 ++++++++++++++++------ .../selftests/bpf/progs/mptcp_bpf_bad_sched.c | 48 +++++++++++++++++++ 2 files changed, 89 insertions(+), 15 deletions(-) diff --git a/tools/testing/selftests/bpf/prog_tests/mptcp.c b/tools/testing/selftests/bpf/prog_tests/mptcp.c index XXXXXXX..XXXXXXX 100644 --- a/tools/testing/selftests/bpf/prog_tests/mptcp.c +++ b/tools/testing/selftests/bpf/prog_tests/mptcp.c @@ -XXX,XX +XXX,XX @@ static void test_burst(void) mptcp_bpf_burst__destroy(skel); } -static void test_bad_sched(void) +static void load_bad_sched(struct mptcp_bpf_bad_sched *skel, const char *msg) { - struct mptcp_bpf_bad_sched *skel; char *log = NULL; int err; + if (start_libbpf_log_capture()) + return; + + err = mptcp_bpf_bad_sched__load(skel); + log = stop_libbpf_log_capture(); + ASSERT_ERR(err, "load: bad scheduler must be rejected"); + ASSERT_HAS_SUBSTR(log, msg, "verifier type-mismatch message"); + free(log); +} + +static void test_bad_sched(void) +{ + struct mptcp_bpf_bad_sched *skel; + /* - * bad_sched_get_send() passes a subflow TCP socket to - * bpf_mptcp_set_timeout(), which takes a struct mptcp_sock *. The - * verifier must reject this socket type confusion at load time, and - * for the right reason: assert the specific verifier message. + * Each scheduler in mptcp_bpf_bad_sched passes a wrong-subtype socket + * to a narrow-typed scheduler kfunc; the verifier must reject each at + * load time with the specific type-mismatch message. The skel holds + * several such schedulers, so enable one struct_ops map at a time; + * loading them together would fail atomically. */ + + /* subflow sock -> bpf_mptcp_set_timeout(struct mptcp_sock *) */ skel = mptcp_bpf_bad_sched__open(); if (!ASSERT_OK_PTR(skel, "open: bad_sched")) return; + bpf_map__set_autocreate(skel->maps.bad_wnd_end, false); + bpf_map__set_autocreate(skel->maps.bad_set_sched, false); + load_bad_sched(skel, "expected pointer to STRUCT mptcp_sock"); + mptcp_bpf_bad_sched__destroy(skel); - if (start_libbpf_log_capture()) - goto destroy; + /* subflow sock -> mptcp_wnd_end(struct mptcp_sock *) */ + skel = mptcp_bpf_bad_sched__open(); + if (!ASSERT_OK_PTR(skel, "open: bad_wnd_end")) + return; + bpf_map__set_autocreate(skel->maps.bad_sched, false); + bpf_map__set_autocreate(skel->maps.bad_set_sched, false); + load_bad_sched(skel, "expected pointer to STRUCT mptcp_sock"); + mptcp_bpf_bad_sched__destroy(skel); - err = mptcp_bpf_bad_sched__load(skel); - log = stop_libbpf_log_capture(); - ASSERT_ERR(err, "load: bad_sched must be rejected"); - ASSERT_HAS_SUBSTR(log, "expected pointer to STRUCT mptcp_sock", - "verifier rejects subflow sock to bpf_mptcp_set_timeout"); - free(log); -destroy: + /* msk -> mptcp_subflow_set_scheduled(struct mptcp_subflow_context *) */ + skel = mptcp_bpf_bad_sched__open(); + if (!ASSERT_OK_PTR(skel, "open: bad_set_sched")) + return; + bpf_map__set_autocreate(skel->maps.bad_sched, false); + bpf_map__set_autocreate(skel->maps.bad_wnd_end, false); + load_bad_sched(skel, "expected pointer to STRUCT mptcp_subflow_context"); mptcp_bpf_bad_sched__destroy(skel); } diff --git a/tools/testing/selftests/bpf/progs/mptcp_bpf_bad_sched.c b/tools/testing/selftests/bpf/progs/mptcp_bpf_bad_sched.c index XXXXXXX..XXXXXXX 100644 --- a/tools/testing/selftests/bpf/progs/mptcp_bpf_bad_sched.c +++ b/tools/testing/selftests/bpf/progs/mptcp_bpf_bad_sched.c @@ -XXX,XX +XXX,XX @@ char _license[] SEC("license") = "GPL"; extern void bpf_mptcp_set_timeout(struct mptcp_sock *msk) __ksym; +extern __u64 mptcp_wnd_end(const struct mptcp_sock *msk) __ksym; SEC("struct_ops") void BPF_PROG(bad_sched_init, struct mptcp_sock *msk) @@ -XXX,XX +XXX,XX @@ struct mptcp_sched_ops bad_sched = { .get_send = (void *)bad_sched_get_send, .name = "bpf_bad_sched", }; + +/* + * Same confusion class as bad_sched, on another struct mptcp_sock * kfunc: + * feed a subflow TCP socket to mptcp_wnd_end(). The verifier must reject it + * ("expected pointer to STRUCT mptcp_sock"). get_send is the only required + * scheduler op, so the rest are omitted. + */ +SEC("struct_ops") +int BPF_PROG(bad_wnd_end_get_send, struct mptcp_sock *msk) +{ + struct mptcp_subflow_context *subflow; + struct sock *ssk; + + bpf_for_each(mptcp_subflow, subflow, (struct sock *)msk) { + ssk = bpf_mptcp_subflow_tcp_sock(subflow); + if (!ssk) + return -1; + if (mptcp_wnd_end((struct mptcp_sock *)ssk)) + return 0; + return -1; + } + return -1; +} + +SEC(".struct_ops.link") +struct mptcp_sched_ops bad_wnd_end = { + .get_send = (void *)bad_wnd_end_get_send, + .name = "bpf_bad_wnd_end", +}; + +/* + * Inverse confusion: feed the msk to mptcp_subflow_set_scheduled(), which + * takes a struct mptcp_subflow_context *. The verifier must reject it + * ("expected pointer to STRUCT mptcp_subflow_context"). + */ +SEC("struct_ops") +int BPF_PROG(bad_set_sched_get_send, struct mptcp_sock *msk) +{ + mptcp_subflow_set_scheduled((struct mptcp_subflow_context *)msk, true); + return 0; +} + +SEC(".struct_ops.link") +struct mptcp_sched_ops bad_set_sched = { + .get_send = (void *)bad_set_sched_get_send, + .name = "bpf_bad_set_sch", +}; -- 2.34.1