1. 簡介

在 Linux 做 LPE 時,攻擊者有很多方式可以做 heap spraying,但如果情境為 RCE,可能就沒有這麼容易,因此想透過該篇文章了解網卡裝置處理封包請求的方式,也順便探討是否能透過網路服務來 spray 一些與網路相關的結構。

接下來會以 Linux kernel 版本 6.6.32 來分析,VM 使用的 NIC driver 為 e1000e。此外,下方提到的程式碼片段都會刪減一些相較不重要的部分,因此並非完整的執行邏輯。

2. Receive

E1000 在初始化時會註冊 napi (New API) interface 的 polling callback,將硬體裝置的 receive buffer 包裝成物件並清空 (clean)。其中在 MTU 小於 1500 以下時,clean callback 為 function e1000_clean()

static int e1000_clean(struct napi_struct *napi, int budget)
{
    struct e1000_adapter *adapter = container_of(napi, struct e1000_adapter,
                             napi);
    int tx_clean_complete = 0, work_done = 0;
    tx_clean_complete = e1000_clean_tx_irq(adapter, &adapter->tx_ring[0]);
    adapter->clean_rx(adapter, &adapter->rx_ring[0], &work_done, budget);
    // [...]
}

e1000_clean() 會呼叫 e1000_clean_rx_irq(),把資料包成 struvt sk_buff (後續以 skb 代稱) 並送到上層的網路協議。當封包 payload 大小小於 256 時由 e1000_copybreak() [1] 分配 skb,否則由 napi_build_skb() 分配 [2]。當取得 skb 後,會呼叫 e1000_receive_skb() 將 skb 複製到 receive queue。

static bool e1000_clean_rx_irq(struct e1000_adapter *adapter,
                   struct e1000_rx_ring *rx_ring,
                   int *work_done, int work_to_do)
{
    // [...]
    length = le16_to_cpu(rx_desc->length);
    data = buffer_info->rxbuf.data;

    skb = e1000_copybreak(adapter, buffer_info, length, data); // [1]
    if (!skb) {
       unsigned int frag_len = e1000_frag_len(adapter);

       skb = napi_build_skb(data - E1000_HEADROOM, frag_len); // [2]
       skb_reserve(skb, E1000_HEADROOM);
       buffer_info->dma = 0;
       buffer_info->rxbuf.data = NULL;
    }
  
    // [...]
    if (buffer_info->rxbuf.data == NULL)
        skb_put(skb, length);
    else /* copybreak skb */
        skb_trim(skb, length); // 移除超過 length 的資料
    e1000_receive_skb(adapter, status, rx_desc->special, skb);
    // [...]
}

e1000_copybreak() 會呼叫 e1000_alloc_rx_skb() 分配 skb [3],並呼叫 skb_put_data() 將 receive buffer 的資料複製到 skb。

static struct sk_buff *e1000_copybreak(struct e1000_adapter *adapter,
                       struct e1000_rx_buffer *buffer_info,
                       u32 length, const void *data)
{
    struct sk_buff *skb;

    if (length > copybreak) // 256
        return NULL;

    skb = e1000_alloc_rx_skb(adapter, length); // [3]
    skb_put_data(skb, data, length);
    return skb;
}

e1000_alloc_rx_skb() 底層會呼叫到 __napi_alloc_skb()。該 function 會先分配一個 1k (fragment) 的 data buffer 存資料 [4],之後呼叫 __napi_build_skb() 分配且初始化 skb。

struct sk_buff *__napi_alloc_skb(struct napi_struct *napi, unsigned int len,
                 gfp_t gfp_mask)
{
    // [...]
    if (/*...*/) {
        len = SZ_1K;
        data = page_frag_alloc_1k(&nc->page_small, gfp_mask); // [4]
    } // [...]
    skb = __napi_build_skb(data, len);
    // [...]
    skb->head_frag = 1;
}

正常情況 __napi_build_skb() 會從 cache skbuff_head_cache 回傳一個 object 出來,之後呼叫 __build_skb_around() 來初始化 skb 內的成員,包含 struct skb_shared_info [5]。

static struct sk_buff *__napi_build_skb(void *data, unsigned int frag_size)
{
    struct sk_buff *skb;

    skb = napi_skb_cache_get();
    memset(skb, 0, offsetof(struct sk_buff, tail));
    __build_skb_around(skb, data, frag_size); // [5]

    return skb;
}

napi_build_skb() 處理大於 256 的 payload,最後也是呼叫 __napi_build_skb() [6] 來處理。

struct sk_buff *napi_build_skb(void *data, unsigned int frag_size)
{
    struct sk_buff *skb = __napi_build_skb(data, frag_size); // [6]
    skb->head_frag = 1;
    return skb;
}

兩者的差別在於,如果發現 payload 小於 256 時,會使用 page_frag_alloc_1k() 分配的 buffer 來放資料;反之,當大於 256 時,則是直接用 receive buffer 的記憶體區塊 buffer_info->rxbuf.data




e1000_receive_skb() 底層呼叫到 gro_normal_one(),將 skb 新增到 rx list [1],並且以一個 batch 為單位發送出去,不過預設 gro_normal_batch 會是 0。

static inline void gro_normal_one(struct napi_struct *napi, struct sk_buff *skb, int segs)
{
    list_add_tail(&skb->list, &napi->rx_list); // [1]
    napi->rx_count += segs;
    if (napi->rx_count >= READ_ONCE(gro_normal_batch))
        gro_normal_list(napi);
}

gro_normal_list() 會先呼叫 netif_receive_skb_list_internal(),一路執行到 __netif_receive_skb_list_ptype()。因為並呼叫 packet type 是 IPv4,因此最後會呼叫 ip_packet_type 的 list callback function ip_list_rcv() [2]。

static inline void __netif_receive_skb_list_ptype(struct list_head *head,
                          struct packet_type *pt_prev,
                          struct net_device *orig_dev)
{
    struct sk_buff *skb, *next;

    if (!pt_prev)
        return;
    if (list_empty(head))
        return;
    if (pt_prev->list_func != NULL)
        INDIRECT_CALL_INET(pt_prev->list_func, ipv6_list_rcv,
                   ip_list_rcv, head, pt_prev, orig_dev); // [2]
    // [...]
}

ip_list_rcv() 接收一個 skb list,並對每個 skb 做 IP header 的檢查,最後呼叫 ip_sublist_rcv_finish()。該 function 一樣接收一個 skb list,unlink 後呼叫 deliver callback [3]。如果 destination 是 local 的話,就會呼叫 ip_local_deliver() 把封包送到更高的 protocol layers。

static void ip_sublist_rcv_finish(struct list_head *head)
{
    struct sk_buff *skb, *next;

    list_for_each_entry_safe(skb, next, head, list) {
        skb_list_del_init(skb);
        dst_input(skb); // [3]
    }
}

ip_local_deliver() 會在上 RCU lock 後呼叫 ip_protocol_deliver_rcu(),該 function 會在呼叫對應 protocol 的 handler。舉例來說,如果請求為 TCP,就會由 tcp_protocol->handler(),也就是 tcp_v4_rcv() 來處理 [4]。

void ip_protocol_deliver_rcu(struct net *net, struct sk_buff *skb, int protocol)
{
    const struct net_protocol *ipprot;
    int raw, ret;

    ipprot = rcu_dereference(inet_protos[protocol]);
    if (ipprot) {
        // [...]
        ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv,
                      skb); // [4]
    // [...]
  }
}

tcp_v4_rcv() 則是會做 TCP 的檢查,並執行一些 hook 處理,像是 filter handler [5]。做完檢查後,最後會呼叫 tcp_v4_do_rcv() 做處理。

int tcp_v4_rcv(struct sk_buff *skb)
{
    // [...]
    if (tcp_filter(sk, skb)) { // [5]
        drop_reason = SKB_DROP_REASON_SOCKET_FILTER;
        goto discard_and_relse;
    }
    // [...]
    if (sk->sk_state == TCP_LISTEN) {
        ret = tcp_v4_do_rcv(sk, skb);
        goto put_and_return;
    }
    // [...]
    if (!sock_owned_by_user(sk)) {
        ret = tcp_v4_do_rcv(sk, skb);
    } 
}

tcp_v4_do_rcv() 發現 socket state 為 TCP_ESTABLISHED,代表連線已經建立,呼叫 tcp_rcv_established()

tcp_rcv_established() 會再執行到 tcp_ack() [6] 處理 ACK 請求,之後呼叫 tcp_data_queue() enqueue skb 到 socket 的 receive queue [7]。

void tcp_rcv_established(struct sock *sk, struct sk_buff *skb)
{
    // [...]
    if (len <= tcp_header_len) {
        // [...]
    } else {
        // [...]
        if ((int)skb->truesize > sk->sk_forward_alloc)
                goto step5;
        // [...]
    }
step5:
    reason = tcp_ack(sk, skb, FLAG_SLOWPATH | FLAG_UPDATE_TS_RECENT); // [6]
    if ((int)reason < 0) {
        reason = -reason;
        goto discard;
    }
    // [...]
    tcp_data_queue(sk, skb); // [7]
}

tcp_data_queue() 會呼叫 tcp_queue_rcv(),而 tcp_queue_rcv() 在 enqueue 之前會嘗試與之前的 skb merge [8]。若判斷不需要 merge,就會 enqueue 到 socket object 的 sk_receive_queue 當中。

static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb,
                      bool *fragstolen)
{
    int eaten;
    struct sk_buff *tail = skb_peek_tail(&sk->sk_receive_queue);

    eaten = (tail &&
         tcp_try_coalesce(sk, tail,
                  skb, fragstolen)) ? 1 : 0; // [8]
    tcp_rcv_nxt_update(tcp_sk(sk), TCP_SKB_CB(skb)->end_seq);
    if (!eaten) {
        __skb_queue_tail(&sk->sk_receive_queue, skb); // [9]
        skb_set_owner_r(skb, sk);
    }
    return eaten;
}

3. Release

每個 packet 都會對應到一個 skb 並且執行到 tcp_queue_rcv(),但如果 tcp_queue_rcv() 認為當前的 skb 可以與前一個合併 [1],就會在複製資料與更新結構後,將這次的 skb 釋放掉 [2]。

static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
    // [...]
    eaten = tcp_queue_rcv(sk, skb, &fragstolen); // [1]
    // [...]
    if (eaten > 0)
        kfree_skb_partial(skb, fragstolen); // [2]
    // [...]
}

kfree_skb_partial() 會根據傳入的參數來決定釋放的方式,如果目標 skb 原本是 list head 就執行額外的處理 [3]。

void kfree_skb_partial(struct sk_buff *skb, bool head_stolen)
{
    if (head_stolen) {
        skb_release_head_state(skb); // [3]
        kmem_cache_free(skbuff_cache, skb);
    } else {
        __kfree_skb(skb);
    }
}

由此可知能否 spraying 成功的關鍵在於 tcp_try_coalesce()。當 TCP sequence number 不同時 [4] 或硬體為 TLS 裝置 [5] 能馬上知道不需要做 coalesce,最後會再由 skb_try_coalesce() 做檢查 [6]。

static bool tcp_try_coalesce(struct sock *sk,
                 struct sk_buff *to,
                 struct sk_buff *from,
                 bool *fragstolen)
{
    int delta;

    *fragstolen = false;

    if (TCP_SKB_CB(from)->seq != TCP_SKB_CB(to)->end_seq) // [4]
        return false;

    if (from->decrypted != to->decrypted) // [5]
        return false;

    if (!skb_try_coalesce(to, from, fragstolen, &delta)) // [6]
        return false;
  
    // [...]
    return true;
}

skb_try_coalesce() 在下面幾個情況會認為不用合併 skb,像是 skb 透過 skb_clone() 分配 [7]、要被 merge 的兩個 skb 其中一個使用 zero copy [8]、已經有太多 fragment [9]、 以及其他等等更複雜的情況。不過在檢查合併的條件之前,如果 kernel 發現前一個 skb 還有空間 [10],就會直接 coalesce。

bool skb_try_coalesce(struct sk_buff *to, struct sk_buff *from,
              bool *fragstolen, int *delta_truesize)
{
    struct skb_shared_info *to_shinfo, *from_shinfo;
    int i, delta, len = from->len;

    *fragstolen = false;

    if (skb_cloned(to)) // [7]
        return false;
  
    if (len <= skb_tailroom(to)) { // [10]
        skb_copy_bits(from, 0, skb_put(to, len), len);
        *delta_truesize = 0;
        return true;
    }

    // [...]
  
    if (skb_zcopy(to) || skb_zcopy(from)) // [8]
        return false;

    if (skb_headlen(from) != 0) {
        // [...]
        if (to_shinfo->nr_frags +
            from_shinfo->nr_frags >= MAX_SKB_FRAGS) // [9]
            return false;
    
        // [...]
    } else {
        if (to_shinfo->nr_frags +
            from_shinfo->nr_frags > MAX_SKB_FRAGS) // [9]
            return false;

        // [...]
    }

    // [...]
    return true;
}

4. 猜想

我們能從 tcp_try_coalesce() 的實作得知,如果可以透過發送與預期不同 TCP sequence 的 packet,就會在 tcp_try_coalesce() 一開始的檢查被判斷成不做 coalesce,這樣就可以透過遠端連線來 spraying struct sk_buff 與 data buffer。

5. 實驗

將 server 執行在 VM 當中,並透過 host network formwarding 的方式與 server 互動。下方為執行 VM 的腳本:

qemu-system-x86_64 \
    -m 1G \
    -smp 1 \
    -nographic \
    -monitor /dev/null \
    -kernel ./arch/x86_64/boot/bzImage \
    -append "console=ttyS0 nokaslr pti=on quiet" \
    -cpu qemu64,+smep,+smap \
    -initrd ./rootfs.cpio \
    -netdev user,id=mynet0,hostfwd=tcp::1337-:1337 \
    -device e1000,netdev=mynet0 \
    -s

嘗試 1 - BPF

安裝 bcc

sudo apt-get install bpfcc-tools linux-headers-$(uname -r)

寫 BPF program 來 hook TCP request 並改變 sequence number,最後修好 checksum 後送出。下方的 bpf program 假設 server port 聽在 1337。

#!/usr/bin/python3
from bcc import BPF

bpf_program = '''
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>

int modify_tcp_seq(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    
    struct ethhdr *eth = data;
    if ((void *)&eth[1] > data_end)
        return XDP_PASS;
    if (eth->h_proto != htons(ETH_P_IP))
        return XDP_PASS;

    struct iphdr *iph = data + sizeof(*eth);
    if ((void *)&iph[1] > data_end)
        return XDP_PASS;
    if (iph->protocol != IPPROTO_TCP)
        return XDP_PASS;

    struct tcphdr *tcph = data + sizeof(*eth) + sizeof(*iph);
    if ((void *)&tcph[1] > data_end)
        return XDP_PASS;

    if (ntohs(tcph->dest) != 1337)
        return XDP_PASS;

    __u32 new_seq = bpf_htonl(bpf_ntohl(tcph->seq) - (bpf_get_prandom_u32() % 0x1000));
    __u32 new_check;
    {
        uint32_t sum = ~ntohs(tcph->check) & 0xffff;
        sum -= (ntohs(tcph->seq >> 16) & 0xffff);
        sum -= (ntohs(tcph->seq & 0xffff) & 0xffff);
        sum += (ntohs(new_seq >> 16) & 0xffff);
        sum += (ntohs(new_seq & 0xffff) & 0xffff);
        sum = (sum & 0xffff) + (sum >> 16);
        new_check = htons(~sum);
    }

    tcph->check = new_check;
    tcph->seq = new_seq;

    return XDP_PASS;
}
'''

bpf = BPF(text=bpf_program)
fn = bpf.load_func("modify_tcp_seq", BPF.XDP)
bpf.attach_xdp("lo", fn, 0)

try:
    bpf.trace_print()
except KeyboardInterrupt:
    pass

bpf.remove_xdp("lo", 0)

不過使用這個方式會因為直接 reuse kernel 的 TCP stack 的關係,讓 kernel 收不到 server 回傳的 ACK 而不斷重送,導致剩下的 TCP packet 都會一直卡在 queue 送不出去。

嘗試 2 - Scapy

透過 Scapy 我們可以控制整個 TCP 互動的流程,這樣就不會被 kernel 影響。不過在建置執行環境時會踩到一些雷,下方提供問題以及相對應的解法:

  1. Loopback device 沒有辦法正常的 handshake
    • https://stackoverflow.com/a/41449990
    • https://github.com/secdev/scapy/blob/master/doc/scapy/troubleshooting.rst#i-cant-ping-127001-or-1-scapy-does-not-work-with-127001-or-1-on-the-loopback-interface
  2. Kernel 會因為發現 server 並沒有開啟 packet 指定的 source port 而自動回傳 RST
    • https://stackoverflow.com/a/9154940
    • https://github.com/secdev/scapy/blob/master/doc/scapy/troubleshooting.rst#my-tcp-connections-are-reset-by-scapy-or-by-my-kernel
    • iptables command
       sudo iptables -A OUTPUT -d 127.0.0.1 -p tcp --dport 1337 --tcp-flags RST RST -j DROP
      

成功建置環境後,接下來就可以直接透過 scapy 發送 raw pakcet 跟 server 互動。假設在 handshake 後 client 會先跟 server 交換一次資料,然後 server 就會 hang 住不呼叫 recv() 相關的 function 收封包。首先發送 SYN [1] 與 ACK [2] 做三項交握,之後發送 8 個 “A” [3] 並接收 server 回傳的資料 [4]。最後在發送一個正常的封包後 [5] 不斷送 sequence 不相同的封包 [6]。

from scapy.all import *
import random

conf.L3socket = L3RawSocket

target_ip = "127.0.0.1"
target_port = 1337
src_port = random.randint(1338, 65535)

# [1]
syn_packet = IP(dst=target_ip)/TCP(sport=src_port, dport=target_port, flags="S")
syn_ack_response = sr1(syn_packet)

if syn_ack_response and syn_ack_response.haslayer(TCP) and syn_ack_response[TCP].flags == "SA":
    # [2]
    ack_packet = IP(dst=target_ip)/TCP(sport=src_port, dport=target_port, flags="A", seq=syn_ack_response.ack, ack=syn_ack_response.seq+1)
    send(ack_packet)
    print("Handshake successful")
else:
    print("Handshake failed")
    exit(1)

seq = syn_ack_response.ack
ack = syn_ack_response.seq + 1

# [3] 送資料
data_packet = IP(dst=target_ip)/TCP(sport=src_port, dport=target_port, flags="PA", seq=seq, ack=ack)/Raw(load=b"AAAAAAAA")
data_response = sr1(data_packet)
seq += len(data_packet[Raw].load)

# [4] 接收回傳的資料
s = L3RawSocket()
p = s.recv(MTU)
ack = p[TCP].seq + len(p[Raw])
data_packet = IP(dst=target_ip)/TCP(sport=src_port, dport=target_port, flags="A", seq=seq, ack=ack)
send(data_packet)

# [5]
data_packet = IP(dst=target_ip)/TCP(sport=src_port, dport=target_port, flags="PA", seq=seq, ack=ack)/Raw(load=b"A"*0x100)
data_response = sr1(data_packet)
old_seq = seq
seq += len(data_packet[Raw].load)
assert(data_response.ack == seq)

# [6]
while True:
    input("bad seq >")
    data_packet = IP(dst=target_ip)/TCP(sport=src_port, dport=target_port, flags="PA", seq=seq + 10, ack=ack + 10)/Raw(load=b"A"*0x100)
    send(data_packet)

但因為 host 是用 network forwarding 與跑在 VM 內的 server 互動,因此用 Scapy 發送的請求會先過 host kernel network,並被視為異常封包而擋下來,沒辦法轉發到 VM 內。不過如果 target server 是實體機,應該不會有這個問題,不過環境處理上有點小麻煩,加上手邊沒多的機器,因此該測試方法就先擱置。

# ...
-netdev user,id=mynet0,hostfwd=tcp::1337-:1337 \
-device e1000,netdev=mynet0 \
# ...

嘗試 3 - 透過 debugger 手動調整 sequence

透過先前的執行流程分析能得知 function tcp_v4_rcv() 才會開始處理 TCP header,因此斷點在該 function 中間 [1] 並且直接用 debugger 改 TCP sequence。

int tcp_v4_rcv(struct sk_buff *skb)
{
    // [...]
    th = (const struct tcphdr *)skb->data; // [1]
    // [...]
    tcp_v4_fill_cb(skb, iph, th);
    // [...]
    if (!sock_owned_by_user(sk)) {
        ret = tcp_v4_do_rcv(sk, skb);
    }
    // [...]
}

所有與 sequence 的檢查都會在 function tcp_rcv_established() 之後。tcp_rcv_established() 得知 sequence 與預期 (tp->rcv_nxt) 不相符時就會走 slow path,並且透過 slow path 處理的 packet 需要設置 ACK、RST 與 SYN 中至少一個 flag。之後會呼叫 tcp_validate_incoming() [2] 檢查 packet sequence,若沒問題才會繼續往下執行。

void tcp_rcv_established(struct sock *sk, struct sk_buff *skb)
{
    if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt && /* ... */) {}
    // [...]

slow_path:
    // [...]
    if (!th->ack && !th->rst && !th->syn) {
        reason = SKB_DROP_REASON_TCP_FLAGS;
        goto discard;
    }
  
    // [...]
    if (!tcp_validate_incoming(sk, skb, th, 1)) // [2]
        return;
  
    // [...]
    tcp_data_queue(sk, skb);
    // [...]
}

如果 packet 是 RST 或 SYN,tcp_validate_incoming() 就會判斷該 packet 要被 drop 掉,但如果是 ACK,還是需要滿足 sequence 在範圍內。一共有兩個限制:end sequence 不能小於 tp->rcv_wup (rcv_nxt on last window update sent) [3],以及 sequence 不能超過預期的 receive sequence + receive windows size [4]。

static enum skb_drop_reason tcp_sequence(const struct tcp_sock *tp,
                     u32 seq, u32 end_seq)
{
    if (before(end_seq, tp->rcv_wup)) // [3]
        return SKB_DROP_REASON_TCP_OLD_SEQUENCE;

    if (after(seq, tp->rcv_nxt + tcp_receive_window(tp))) // [4]
        return SKB_DROP_REASON_TCP_INVALID_SEQUENCE;

    return SKB_NOT_DROPPED_YET;
}

當上述條件滿足,就會接著執行到 tcp_data_queue()。如果 sequence 小於預期 [5],就會執行 tcp_queue_rcv(),反之如果 sequence 大於預期 [6],就會被視為 out-of-order packet,呼叫 tcp_data_queue_ofo() 處理。

static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
    if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
queue_and_out:
        // [...]
        eaten = tcp_queue_rcv(sk, skb, &fragstolen);
        // ... 原本的處理邏輯 
    }
  
    if (!before(TCP_SKB_CB(skb)->seq,
        tp->rcv_nxt + tcp_receive_window(tp))) {
        // [...]
        // drop packet
    }
  
    if (before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) { // [5]
        // [...]
        goto queue_and_out;
    }
  
    tcp_data_queue_ofo(sk, skb); // [6]
}

tcp_data_queue_ofo() 會把 skb enqueue 到 out of order queue 而非 receive queue [7]。雖然乍看之下這條 path 可以避免 receive queue 的 skb 做 coalesce,但實際上後續執行流程還是會遍歷整個 out of order RB tree 並嘗試合併 overlap 的 skb segment [8],因此發送大於預期的 sequence 可能行不通

static void tcp_data_queue_ofo(struct sock *sk, struct sk_buff *skb)
{
    // [...]
    p = &tp->out_of_order_queue.rb_node;
    if (RB_EMPTY_ROOT(&tp->out_of_order_queue)) {
        // [...]
        // 將 skb 存成 root
        goto end;
    }
  
    if (tcp_ooo_try_coalesce(sk, tp->ooo_last_skb,
                   skb, &fragstolen)) {
        // [...]
        // 做 coalesce
    }
    // [...]
insert:
    /* Insert segment into RB tree. */
    rb_link_node(&skb->rbnode, parent, p);
    rb_insert_color(&skb->rbnode, &tp->out_of_order_queue); // [7]

merge_right:
    /* Remove other segments covered by skb. */
    while ((skb1 = skb_rb_next(skb)) != NULL) { // [8]
        // [...]
        // 找出 overlap 的 segment 並移除
    }
}




當 sequence 小於預期時,tcp_data_queue() 一樣會呼叫 tcp_queue_rcv() 接收 packet,並且一定程度下可以控制 sequence value,讓 tcp_try_coalesce() 回傳 false 而不跟前一個 skb 做 coalesce。

於是我們得到理論上可以讓 enqueue 的 packet 不會被合併的四個 sequence value 限制:

  1. !before(end_seq, tp->rcv_wup) - end sequence 不能是 old sequence
  2. !after(seq, tp->rcv_nxt + tcp_receive_window(tp)) - sequence 不能超出 receive window
  3. before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt) - sequence 需要小於 kernel 預期收到的
  4. TCP_SKB_CB(from)->seq != TCP_SKB_CB(to)->end_seq - sequence 不能是前一個 packet 的 end_seq

tcp_receive_window() 會回傳剩下還能用的 window size。

static inline u32 tcp_receive_window(const struct tcp_sock *tp)
{
    /**
     * rcv_wup - rcv_nxt on last window update sent
     * rcv_nxt - What we want to receive next
     * rcv_wnd - Current receiver window
     */
    s32 win = tp->rcv_wup + tp->rcv_wnd - tp->rcv_nxt;

    if (win < 0)
        win = 0;
    return (u32) win;
}

關於 TCP Window 可以參考該文章,介紹得非常詳細。一般情況下,存起來的 skb 跟 receive window 的關係大概會長得像下圖,其中 skb1 與 skb2 會被 coalesce 成同一個。

image-20240619105529385

但如果在傳送的過程中故意省略掉一些 sequence,就會變成類似下圖的情況。

image-20240619105659285

不過這個想法仍屬於猜測階段,還沒實際測試過,因此不保證是個可行的方式。

6. 假設不成立

6.1. UDP

UDP request handler 的進入點為 udp_rcv(),後續會執行到 udp_queue_rcv_skb()。而 UDP 有 GSO (Generic Segmentation Offload) 的機制,但如果沒有啟用或是設定失敗,就會直接由 udp_queue_rcv_one_skb() 處理 [1];反之如果有,就會先呼叫 udp_rcv_segment() 處理 UDP GRO [2]。

static int udp_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{
    struct sk_buff *next, *segs;
    int ret;

    if (likely(!udp_unexpected_gso(sk, skb)))
        return udp_queue_rcv_one_skb(sk, skb); // [1]
    
    __skb_push(skb, -skb_mac_offset(skb));
    segs = udp_rcv_segment(sk, skb, true); // [2]
    skb_list_walk_safe(segs, skb, next) {
        __skb_pull(skb, skb_transport_offset(skb));

        udp_post_segment_fix_csum(skb);
        ret = udp_queue_rcv_one_skb(sk, skb);
        // [...]
    }
    return 0;
}

udp_queue_rcv_one_skb() 會處理 UDP Encapsulation 以及 UDP-lite,但如果是一般的 UDP packet 最後會執行到 __udp_enqueue_schedule_skb()。該 function 檢查 read memory 分配是否超出 receive buffer [3],看 buffer 是否有足夠空間放下 packet [4],最後在更新 buffer size 後 enqueue skb [5]。

int __udp_enqueue_schedule_skb(struct sock *sk, struct sk_buff *skb)
{
    struct sk_buff_head *list = &sk->sk_receive_queue;
    int rmem, err = -ENOMEM;
    spinlock_t *busy = NULL;
    int size;

    rmem = atomic_read(&sk->sk_rmem_alloc);
    if (rmem > sk->sk_rcvbuf) // [3]
        goto drop;

    // [...]
    size = skb->truesize;
    rmem = atomic_add_return(size, &sk->sk_rmem_alloc);
    if (rmem > (size + (unsigned int)sk->sk_rcvbuf)) // [4]
        goto uncharge_drop;

    spin_lock(&list->lock);
    err = udp_rmem_schedule(sk, size);
    // [...]
    sk_forward_alloc_add(sk, -size);
    sock_skb_set_dropcount(sk, skb);
    __skb_queue_tail(list, skb); // [5]
    // [...]
    return 0;
}

不過到這邊才發現 receiver buffer size 是用 skb->truesize 來更新,也就是 object size 與 data buffer size 的總和。舉例來說,payload 大小為 1 的 packet 其 skb->truesize 會是 1280,等同於 size of skbuff_head_cache (0x100) 加上 buffer size allocated at __napi_alloc_skb() (0x400)。因此一個 socket 所牽扯到的 packet object 最多就只能有 212992 bytes (sysctl_rmem_max) — 也就是 208 KB 的記憶體使用量,並不是指 payload 大小的總和。

6.2. TCP Check

當發現 UDP 有這個檢查時,就覺得 TCP 一定有類似的操作,只是我當初沒有注意到在哪邊處理。

tcp_data_queue() 接收 packet 之前,會先檢查 receive buffer 是否有足夠的空間 [1]。

static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
    // [...]
    if (tcp_try_rmem_schedule(sk, skb, skb->truesize)) { // [1]
        inet_csk(sk)->icsk_ack.pending |=
                (ICSK_ACK_NOMEM | ICSK_ACK_NOW);
        inet_csk_schedule_ack(sk);
        sk->sk_data_ready(sk);

        if (skb_queue_len(&sk->sk_receive_queue)) {
            reason = SKB_DROP_REASON_PROTO_MEM;
            NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPRCVQDROP);
            goto drop;
        }
        sk_forced_mem_schedule(sk, skb->truesize);
    }
    // [...]
    eaten = tcp_queue_rcv(sk, skb, &fragstolen);
    // [...]
}

tcp_try_rmem_schedule() 會檢查累積分配的記憶體 (sk->sk_rmem_alloc) 是否超出 receive buffer size [2]。如果超過 buffer size,就會 prune receive queue [3] 以及 out-of-order queue [4] 直到有記憶體為止。

static int tcp_try_rmem_schedule(struct sock *sk, struct sk_buff *skb,
                 unsigned int size)
{
    if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || // [2]
        !sk_rmem_schedule(sk, skb, size)) {

        if (tcp_prune_queue(sk, skb) < 0) // [3]
            return -1;

        while (!sk_rmem_schedule(sk, skb, size)) {
            if (!tcp_prune_ofo_queue(sk, skb)) // [4]
                return -1;
        }
    }
    return 0;
}

每次 tcp_queue_rcv() enqueue 時都會更新 sk->sk_rmem_alloc 以及 sk->sk_forward_alloc [4] [5]。sk->sk_forward_alloc 看名稱會以為是預分配的記憶體的大小,不過實際上並不會額外分配記憶體,而是被用來管理 socket 使用記憶體的狀況 。

static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb,
                      bool *fragstolen)
{
    int eaten;
    struct sk_buff *tail = skb_peek_tail(&sk->sk_receive_queue);

    eaten = (tail &&
         tcp_try_coalesce(sk, tail, // [4]
                  skb, fragstolen)) ? 1 : 0;
    // [...]
    if (!eaten) {
        __skb_queue_tail(&sk->sk_receive_queue, skb);
        skb_set_owner_r(skb, sk); // [5]
    }
    // [...]
}

因此 TCP 紀錄的也是 socket 所有佔掉 memory size,並且一樣會限制 total size 會落在 receive buffer size 之內。

7. 結論

不論使用 UDP、TCP 或是在 TCP 中構造奇怪的 sequence value,每個 socket 最多就只能佔 sysctl_rmem_max 的 memory size,而該變數的值預設會是 208 KB (212992 bytes)。