Home
System Hacking
🧩

Dirty Frag Background (1) - sk_buff와 paged frags

Type
Research
날짜
2026/08/03
종류
Kernel
Linux
1 more property

여담

Copy Fail에서는 file page cache -> pipe_buffer -> bio_vec -> scatterlist까지 봤다. Dirty Frag에서 새로 추가되는 구간은 그 page reference가 네트워크 패킷의 skb_frag_t로 들어가는 부분이다.
이 글은 splice() 자체를 다시 설명하지 않는다. 기존 글의 pipe-to-socket 경로 다음부터 시작해서, struct sk_buff, struct skb_shared_info, SKBFL_SHARED_FRAG가 왜 Dirty Frag의 ownership boundary가 되는지만 정리한다.

먼저 결론

Dirty Frag의 공통 전제는 다음 pointer identity다.
file page-cache page P == pipe_buffer.page == bio_vec.bv_page == skb_frag_page(skb_shinfo(skb)->frags[i]) == sg_page(sg[i])
Plain Text
복사
이 상태는 그 자체로 취약점이 아니다. zero-copy는 원래 page reference를 복사 없이 전달하기 위한 정상 기능이다.
문제는 수신 측 코드가 P를 외부에서 공유된 read-only source page가 아니라, skb가 독점 소유한 수정 가능한 payload page로 잘못 판단하고 in-place decrypt를 수행할 때 생긴다.

struct sk_buff는 무엇인가

Linux networking에서 패킷 하나를 표현하는 중심 구조체가 struct sk_buff다. 중요한 점은 sk_buff 구조체 자체가 모든 packet byte를 품고 있는 것이 아니라, packet data를 가리키는 metadata라는 것이다.
struct sk_buff head/data/tail/end -> linear data skb_shared_info nr_frags frags[] -> paged fragments frag_list -> chained skb flags
Plain Text
복사
작은 header는 linear 영역에 있고 큰 payload는 page 단위 fragment로 붙을 수 있다. skb_shared_info.frags[]의 각 entry는 본질적으로 다음 descriptor다.
typedef struct skb_frag { netmem_ref netmem; unsigned int len; unsigned int offset; } skb_frag_t;
C
복사
즉 frag도 scatterlist처럼 data copy가 아니라 page + offset + length를 표현한다.

splice(pipe -> socket)가 skb frag를 만드는 과정

기존 Copy Fail Background (4)에서 splice_to_socket()이 pipe buffer를 bio_vec[]로 바꾸는 지점까지 봤다. 그 뒤에는 다음 흐름이 이어진다.
splice_to_socket() bvec_set_page(&bvec[i], pipe_buffer.page, ...) msg.msg_flags = MSG_SPLICE_PAGES iov_iter_bvec(&msg.msg_iter, ITER_SOURCE, bvec, ...) sock_sendmsg(sock, &msg) UDP/IP send path __ip_append_data() skb_splice_from_iter() iov_iter_extract_pages() skb_append_pagefrags() skb_shinfo(skb)->frags[] = page reference
Plain Text
복사
MSG_SPLICE_PAGES는 userspace가 직접 전달하는 일반 send flag가 아니다. splice(pipe -> socket)를 처리하는 kernel의 splice_to_socket()이 내부적으로 설정한다.
최종적으로 file의 page-cache page가 copy되지 않은 채 sender skb의 paged frag가 된다.

cloned와 shared frag는 다른 상태다

Dirty Frag를 볼 때 가장 많이 헷갈리는 부분이다.
skb_cloned(skb): 다른 skb가 같은 skb data buffer를 참조하는지 본다.
skb_has_frag_list(skb): 별도 skb chain이 붙어 있는지 본다.
skb_has_shared_frag(skb): frag가 vmsplice()/sendfile()/splice() 같은 외부 source와 공유되어 덮어써질 수 있는지 본다.
따라서 !skb_cloned(skb)라고 해서 frags[]의 page까지 kernel-private라는 뜻은 아니다. skb metadata는 하나뿐이어도 frag가 file page cache를 가리킬 수 있다.
SKBFL_SHARED_FRAG의 역할은 바로 이 provenance를 표시하는 것이다.
static inline bool skb_has_shared_frag(const struct sk_buff *skb) { return skb_is_nonlinear(skb) && skb_shinfo(skb)->flags & SKBFL_SHARED_FRAG; }
C
복사
Dirty Frag의 두 취약점은 이 상태를 표시하지 않거나 검사하지 않은 채 skb_cloned()만 신뢰하면서 발생한다.

skb_to_sgvec와 in-place crypto

네트워크 crypto 코드는 skb payload를 Crypto API에 넘기기 위해 skb_to_sgvec()를 사용한다.
skb frags[] -> skb_to_sgvec() -> scatterlist sg[] -> crypto request src/dst
Plain Text
복사
이때 request가 src = sg, dst = sg라면 in-place operation이다. 정상적으로 skb가 page를 독점 소유한다면 문제가 없다. 그러나 frag가 page-cache page P라면 decrypt의 STORE가 그대로 P를 수정한다.
aead_request_set_crypt(req, sg, sg, ...) ^ ^ same SGL skcipher_request_set_crypt(req, sg, sg, ...) ^ ^ same SGL
Plain Text
복사
ESP variant와 RxRPC variant는 서로 다른 protocol에서 출발하지만 정확히 이 경계에서 만난다.

COW가 필요한 지점

shared frag를 수정해야 한다면 먼저 kernel-private copy를 만들어야 한다.
ESP path: skb_cow_data()
RxRPC path: skb_copy() / skb_unshare() 계열
패치의 핵심은 crypto algorithm을 바꾸는 것이 아니라, externally-owned paged frag가 in-place decrypt destination에 들어가기 전에 복사되도록 ownership 검사를 보강하는 것이다.

정리

Dirty Frag는 sk_buff 자체의 메모리 corruption이 아니라, skb frag가 가리키는 page의 provenance를 잃어버린 상태에서 정상적인 in-place crypto STORE가 잘못된 물리 page로 향하는 취약점 class다.
기억할 것은 세 가지다.
1.
splice()는 file page-cache page reference를 socket send path로 운반할 수 있다.
2.
skb_cloned() == false는 paged frag가 kernel-private하다는 보장이 아니다.
3.
SKBFL_SHARED_FRAG를 표시하고 검사해야 in-place modification 전에 COW를 강제할 수 있다.

Reference

본편으로 돌아가기