inet_diag dumps execute attacker-controlled bytecode through inet_diag_bc_sk(). tcp_diag_dump() currently evaluates socket filters and runs that bytecode while holding the listener, bind and ehash bucket locks. A dump request can therefore force unbounded per-bucket lock hold by arranging for many sockets in the same bucket to fail the pre-bytecode filters, so the old 16-entry batching limit no longer bounds the locked walk itself. Under load this can trigger soft lockups and may escalate to a watchdog panic. Fix this by making each locked section collect only referenced sockets. Move all netns/family/port checks, inet_diag_bc_sk(), and fill work out of the bucket locks so the batch limit bounds raw bucket traversal rather than only filter hits. For listener, bind and ehash buckets, keep a referenced dump cursor so restarts resume after the previous socket instead of rescanning the bucket head under the same lock. Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Cc: stable@vger.kernel.org Reported-by: Vega Assisted-by: Codex:gpt-5.4 Signed-off-by: Zihan Xi --- include/linux/inet_diag.h | 15 ++ net/ipv4/inet_diag.c | 13 ++ net/ipv4/tcp_diag.c | 316 +++++++++++++++++++++++++++----------- 3 files changed, 257 insertions(+), 87 deletions(-) diff --git a/include/linux/inet_diag.h b/include/linux/inet_diag.h index 704fd415c2b4..4859e77a28c7 100644 --- a/include/linux/inet_diag.h +++ b/include/linux/inet_diag.h @@ -6,6 +6,7 @@ #include struct inet_hashinfo; +struct sock; struct inet_diag_handler { struct module *owner; @@ -32,12 +33,24 @@ struct inet_diag_handler { }; struct bpf_sk_storage_diag; + +enum inet_diag_dump_cursor_type { + INET_DIAG_DUMP_CURSOR_NONE, + INET_DIAG_DUMP_CURSOR_TCP_LISTEN, + INET_DIAG_DUMP_CURSOR_TCP_BIND, + INET_DIAG_DUMP_CURSOR_TCP_EHASH, + INET_DIAG_DUMP_CURSOR_MPTCP_LISTEN, +}; + struct inet_diag_dump_data { struct nlattr *req_nlas[__INET_DIAG_REQ_MAX]; #define inet_diag_nla_bc req_nlas[INET_DIAG_REQ_BYTECODE] #define inet_diag_nla_bpf_stgs req_nlas[INET_DIAG_REQ_SK_BPF_STORAGES] struct bpf_sk_storage_diag *bpf_stg_diag; + struct sock *dump_cursor; + unsigned int dump_cursor_slot; + u8 dump_cursor_type; bool mark_needed; /* INET_DIAG_BC_MARK_COND present. */ #ifdef CONFIG_SOCK_CGROUP_DATA bool cgroup_needed; /* INET_DIAG_BC_CGROUP_COND present. */ @@ -53,6 +66,8 @@ int inet_sk_diag_fill(struct sock *sk, struct inet_connection_sock *icsk, int inet_diag_bc_sk(const struct inet_diag_dump_data *cb_data, struct sock *sk); +void inet_diag_dump_clear_cursor(struct inet_diag_dump_data *cb_data); + void inet_diag_msg_common_fill(struct inet_diag_msg *r, struct sock *sk); static inline size_t inet_diag_msg_attrs_size(void) diff --git a/net/ipv4/inet_diag.c b/net/ipv4/inet_diag.c index 34b77aa87d0a..41148e880054 100644 --- a/net/ipv4/inet_diag.c +++ b/net/ipv4/inet_diag.c @@ -891,10 +891,23 @@ static int inet_diag_dump_start_compat(struct netlink_callback *cb) return __inet_diag_dump_start(cb, sizeof(struct inet_diag_req)); } +void inet_diag_dump_clear_cursor(struct inet_diag_dump_data *cb_data) +{ + if (!cb_data->dump_cursor) + return; + + sock_gen_put(cb_data->dump_cursor); + cb_data->dump_cursor = NULL; + cb_data->dump_cursor_slot = 0; + cb_data->dump_cursor_type = INET_DIAG_DUMP_CURSOR_NONE; +} +EXPORT_SYMBOL_GPL(inet_diag_dump_clear_cursor); + static int inet_diag_dump_done(struct netlink_callback *cb) { struct inet_diag_dump_data *cb_data = cb->data; + inet_diag_dump_clear_cursor(cb_data); bpf_sk_storage_diag_free(cb_data->bpf_stg_diag); kfree(cb->data); diff --git a/net/ipv4/tcp_diag.c b/net/ipv4/tcp_diag.c index ba1fdbe9807f..be0c22cc445b 100644 --- a/net/ipv4/tcp_diag.c +++ b/net/ipv4/tcp_diag.c @@ -285,6 +285,65 @@ static int sk_diag_fill(struct sock *sk, struct sk_buff *skb, net_admin); } +/* Process a maximum of SKARR_SZ sockets at a time when walking hash buckets + * with bh disabled. + */ +#define SKARR_SZ 16 + +static void tcp_diag_save_cursor(struct inet_diag_dump_data *cb_data, int type, + unsigned int slot, struct sock *sk) +{ + sock_hold(sk); + inet_diag_dump_clear_cursor(cb_data); + cb_data->dump_cursor = sk; + cb_data->dump_cursor_slot = slot; + cb_data->dump_cursor_type = type; +} + +static bool tcp_diag_bind_collect_sock(struct sock *sk, struct sock **sk_arr, + int *num_arr, int *accum, int num) +{ + sock_hold(sk); + num_arr[*accum] = num; + sk_arr[*accum] = sk; + + return ++*accum == SKARR_SZ; +} + +static bool tcp_diag_bind_collect_owners(struct hlist_head *owners, + struct sock **sk_arr, int *num_arr, + int *accum, int *num, int s_num) +{ + struct sock *sk; + + sk_for_each_bound(sk, owners) { + if (*num < s_num) { + (*num)++; + continue; + } + + if (tcp_diag_bind_collect_sock(sk, sk_arr, num_arr, accum, *num)) + return true; + (*num)++; + } + + return false; +} + +static bool tcp_diag_bind_collect_owners_continue(struct sock *sk, + struct sock **sk_arr, + int *num_arr, int *accum, + int *num) +{ + hlist_for_each_entry_continue(sk, sk_bind_node) { + if (tcp_diag_bind_collect_sock(sk, sk_arr, num_arr, accum, *num)) + return true; + (*num)++; + } + + return false; +} + static void twsk_build_assert(void) { BUILD_BUG_ON(offsetof(struct inet_timewait_sock, tw_family) != @@ -335,8 +394,15 @@ static void tcp_diag_dump(struct sk_buff *skb, struct netlink_callback *cb, for (i = s_i; i <= hashinfo->lhash2_mask; i++) { struct inet_listen_hashbucket *ilb; struct hlist_nulls_node *node; + struct sock *sk_arr[SKARR_SZ]; + struct sock *cursor; + int num_arr[SKARR_SZ]; + int idx, accum, res; + bool use_cursor; +resume_listen_walk: num = 0; + accum = 0; ilb = &hashinfo->lhash2[i]; if (hlist_nulls_empty(&ilb->nulls_head)) { @@ -344,52 +410,80 @@ static void tcp_diag_dump(struct sk_buff *skb, struct netlink_callback *cb, continue; } spin_lock(&ilb->lock); - sk_nulls_for_each(sk, node, &ilb->nulls_head) { - struct inet_sock *inet = inet_sk(sk); + cursor = cb_data->dump_cursor; + use_cursor = cursor && + cb_data->dump_cursor_type == + INET_DIAG_DUMP_CURSOR_TCP_LISTEN && + cb_data->dump_cursor_slot == i && + !hlist_nulls_unhashed(&cursor->sk_nulls_node) && + cursor->sk_nulls_node.pprev != LIST_POISON2; + node = use_cursor ? cursor->sk_nulls_node.next : + ilb->nulls_head.first; + hlist_nulls_for_each_entry_from(sk, node, sk_nulls_node) { + if (!use_cursor && num < s_num) + goto next_listen; - if (!net_eq(sock_net(sk), net)) - continue; + sock_hold(sk); + num_arr[accum] = num; + sk_arr[accum] = sk; + if (++accum == SKARR_SZ) + break; - if (num < s_num) { - num++; - continue; - } +next_listen: + ++num; + } + spin_unlock(&ilb->lock); + res = 0; + for (idx = 0; idx < accum; idx++) { + struct inet_sock *inet; + + sk = sk_arr[idx]; + if (!net_eq(sock_net(sk), net)) + goto processed_listen_sk; + + inet = inet_sk(sk); if (r->sdiag_family != AF_UNSPEC && sk->sk_family != r->sdiag_family) - goto next_listen; + goto processed_listen_sk; if (r->id.idiag_sport != inet->inet_sport && r->id.idiag_sport) - goto next_listen; - - if (!inet_diag_bc_sk(cb_data, sk)) - goto next_listen; + goto processed_listen_sk; - if (inet_sk_diag_fill(sk, inet_csk(sk), skb, - cb, r, NLM_F_MULTI, - net_admin) < 0) { - spin_unlock(&ilb->lock); - goto done; + if (res >= 0 && inet_diag_bc_sk(cb_data, sk)) { + res = inet_sk_diag_fill(sk, inet_csk(sk), + skb, cb, r, NLM_F_MULTI, + net_admin); + if (res < 0) + num = num_arr[idx]; } +processed_listen_sk: + if (res >= 0) + tcp_diag_save_cursor(cb_data, + INET_DIAG_DUMP_CURSOR_TCP_LISTEN, + i, sk); + sock_put(sk); + } + if (res < 0) + goto done; -next_listen: - ++num; + cond_resched(); + + if (accum == SKARR_SZ) { + s_num = 0; + goto resume_listen_walk; } - spin_unlock(&ilb->lock); + inet_diag_dump_clear_cursor(cb_data); s_num = 0; } skip_listen_ht: + inet_diag_dump_clear_cursor(cb_data); cb->args[0] = 1; s_i = num = s_num = 0; } -/* Process a maximum of SKARR_SZ sockets at a time when walking hash buckets - * with bh disabled. - */ -#define SKARR_SZ 16 - /* Dump bound but inactive (not listening, connecting, etc.) sockets */ if (cb->args[0] == 1) { if (!(idiag_states & TCPF_BOUND_INACTIVE)) @@ -399,8 +493,10 @@ static void tcp_diag_dump(struct sk_buff *skb, struct netlink_callback *cb, struct inet_bind_hashbucket *ibb; struct inet_bind2_bucket *tb2; struct sock *sk_arr[SKARR_SZ]; + struct sock *cursor; int num_arr[SKARR_SZ]; int idx, accum, res; + bool use_cursor; resume_bind_walk: num = 0; @@ -412,34 +508,38 @@ static void tcp_diag_dump(struct sk_buff *skb, struct netlink_callback *cb, continue; } spin_lock_bh(&ibb->lock); - inet_bind_bucket_for_each(tb2, &ibb->chain) { - if (!net_eq(ib2_net(tb2), net)) - continue; - - sk_for_each_bound(sk, &tb2->owners) { - struct inet_sock *inet = inet_sk(sk); - - if (num < s_num) - goto next_bind; - - if (sk->sk_state != TCP_CLOSE || - !inet->inet_num) - goto next_bind; - - if (r->sdiag_family != AF_UNSPEC && - r->sdiag_family != sk->sk_family) - goto next_bind; - - if (!inet_diag_bc_sk(cb_data, sk)) - goto next_bind; - - sock_hold(sk); - num_arr[accum] = num; - sk_arr[accum] = sk; - if (++accum == SKARR_SZ) + cursor = cb_data->dump_cursor; + use_cursor = cursor && + cb_data->dump_cursor_type == + INET_DIAG_DUMP_CURSOR_TCP_BIND && + cb_data->dump_cursor_slot == i && + !hlist_unhashed(&cursor->sk_bind_node) && + cursor->sk_bind_node.pprev != LIST_POISON2 && + inet_csk(cursor)->icsk_bind2_hash; + if (use_cursor) { + tb2 = inet_csk(cursor)->icsk_bind2_hash; + sk = cursor; + if (tcp_diag_bind_collect_owners_continue(sk, sk_arr, + num_arr, + &accum, + &num)) + goto pause_bind_walk; + hlist_for_each_entry_continue(tb2, node) { + if (tcp_diag_bind_collect_owners(&tb2->owners, + sk_arr, + num_arr, + &accum, + &num, 0)) + goto pause_bind_walk; + } + } else { + inet_bind_bucket_for_each(tb2, &ibb->chain) { + if (tcp_diag_bind_collect_owners(&tb2->owners, + sk_arr, + num_arr, + &accum, + &num, s_num)) goto pause_bind_walk; -next_bind: - num++; } } pause_bind_walk: @@ -447,15 +547,33 @@ static void tcp_diag_dump(struct sk_buff *skb, struct netlink_callback *cb, res = 0; for (idx = 0; idx < accum; idx++) { - if (res >= 0) { - res = inet_sk_diag_fill(sk_arr[idx], - NULL, skb, cb, + struct inet_sock *inet; + + sk = sk_arr[idx]; + if (!net_eq(sock_net(sk), net)) + goto put_bind_sk; + + inet = inet_sk(sk); + if (sk->sk_state != TCP_CLOSE || !inet->inet_num) + goto put_bind_sk; + + if (r->sdiag_family != AF_UNSPEC && + r->sdiag_family != sk->sk_family) + goto put_bind_sk; + + if (res >= 0 && inet_diag_bc_sk(cb_data, sk)) { + res = inet_sk_diag_fill(sk, NULL, skb, cb, r, NLM_F_MULTI, net_admin); if (res < 0) num = num_arr[idx]; } - sock_put(sk_arr[idx]); + if (res >= 0) + tcp_diag_save_cursor(cb_data, + INET_DIAG_DUMP_CURSOR_TCP_BIND, + i, sk); +put_bind_sk: + sock_put(sk); } if (res < 0) goto done; @@ -463,13 +581,15 @@ static void tcp_diag_dump(struct sk_buff *skb, struct netlink_callback *cb, cond_resched(); if (accum == SKARR_SZ) { - s_num = num + 1; + s_num = 0; goto resume_bind_walk; } + inet_diag_dump_clear_cursor(cb_data); s_num = 0; } skip_bind_ht: + inet_diag_dump_clear_cursor(cb_data); cb->args[0] = 2; s_i = num = s_num = 0; } @@ -482,42 +602,33 @@ static void tcp_diag_dump(struct sk_buff *skb, struct netlink_callback *cb, spinlock_t *lock = inet_ehash_lockp(hashinfo, i); struct hlist_nulls_node *node; struct sock *sk_arr[SKARR_SZ]; + struct sock *cursor; int num_arr[SKARR_SZ]; int idx, accum, res; + bool use_cursor; if (hlist_nulls_empty(&head->chain)) continue; - if (i > s_i) + if (i > s_i) { + inet_diag_dump_clear_cursor(cb_data); s_num = 0; + } next_chunk: num = 0; accum = 0; spin_lock_bh(lock); - sk_nulls_for_each(sk, node, &head->chain) { - int state; - - if (!net_eq(sock_net(sk), net)) - continue; - if (num < s_num) - goto next_normal; - state = (sk->sk_state == TCP_TIME_WAIT) ? - READ_ONCE(inet_twsk(sk)->tw_substate) : sk->sk_state; - if (!(idiag_states & (1 << state))) - goto next_normal; - if (r->sdiag_family != AF_UNSPEC && - sk->sk_family != r->sdiag_family) - goto next_normal; - if (r->id.idiag_sport != htons(READ_ONCE(sk->sk_num)) && - r->id.idiag_sport) - goto next_normal; - if (r->id.idiag_dport != sk->sk_dport && - r->id.idiag_dport) - goto next_normal; - twsk_build_assert(); - - if (!inet_diag_bc_sk(cb_data, sk)) + cursor = cb_data->dump_cursor; + use_cursor = cursor && + cb_data->dump_cursor_type == + INET_DIAG_DUMP_CURSOR_TCP_EHASH && + cb_data->dump_cursor_slot == i && + !hlist_nulls_unhashed(&cursor->sk_nulls_node) && + cursor->sk_nulls_node.pprev != LIST_POISON2; + node = use_cursor ? cursor->sk_nulls_node.next : head->chain.first; + hlist_nulls_for_each_entry_from(sk, node, sk_nulls_node) { + if (!use_cursor && num < s_num) goto next_normal; if (!refcount_inc_not_zero(&sk->sk_refcnt)) @@ -534,13 +645,42 @@ static void tcp_diag_dump(struct sk_buff *skb, struct netlink_callback *cb, res = 0; for (idx = 0; idx < accum; idx++) { - if (res >= 0) { - res = sk_diag_fill(sk_arr[idx], skb, cb, r, - NLM_F_MULTI, net_admin); + int state; + + sk = sk_arr[idx]; + if (!net_eq(sock_net(sk), net)) + goto put_estab_sk; + + state = (sk->sk_state == TCP_TIME_WAIT) ? + READ_ONCE(inet_twsk(sk)->tw_substate) : sk->sk_state; + if (!(idiag_states & (1 << state))) + goto put_estab_sk; + + if (r->sdiag_family != AF_UNSPEC && + sk->sk_family != r->sdiag_family) + goto put_estab_sk; + + if (r->id.idiag_sport != htons(READ_ONCE(sk->sk_num)) && + r->id.idiag_sport) + goto put_estab_sk; + + if (r->id.idiag_dport != sk->sk_dport && + r->id.idiag_dport) + goto put_estab_sk; + + twsk_build_assert(); + if (res >= 0 && inet_diag_bc_sk(cb_data, sk)) { + res = sk_diag_fill(sk, skb, cb, r, NLM_F_MULTI, + net_admin); if (res < 0) num = num_arr[idx]; } - sock_gen_put(sk_arr[idx]); + if (res >= 0) + tcp_diag_save_cursor(cb_data, + INET_DIAG_DUMP_CURSOR_TCP_EHASH, + i, sk); +put_estab_sk: + sock_gen_put(sk); } if (res < 0) break; @@ -548,9 +688,11 @@ static void tcp_diag_dump(struct sk_buff *skb, struct netlink_callback *cb, cond_resched(); if (accum == SKARR_SZ) { - s_num = num + 1; + s_num = 0; goto next_chunk; } + + inet_diag_dump_clear_cursor(cb_data); } done: -- 2.43.0