여담
Copy Fail을 보고 나면 자연스럽게 다음 질문이 생긴다.
“AF_ALG만 막으면 끝나는가?”, “splice로 들어온 page-cache page를 in-place crypto가 수정하는 다른 경로는 없는가?”
Dirty Frag는 그 질문에서 출발한 후속 vulnerability class다. Copy Fail이 AF_ALG TX scatterlist에서 page provenance를 잃었다면, Dirty Frag는 networking의 struct sk_buff.frags[]에서 같은 문제가 다시 나타난다.
이 글은 Dirty Frag를 구성하는 두 취약점, CVE-2026-43284 xfrm-ESP와 CVE-2026-43500 RxRPC를 하나의 본편으로 정리한다.
공개된 CVE, 연구자 write-up, upstream patch를 기반으로 한 분석 글이다. 실제 시스템에서는 PoC 실행보다 배포판 kernel update와 reboot가 우선이다.
읽는 순서
Copy Fail에서 이미 정리한 page cache와 splice/pipe는 다시 쓰지 않았다. 필요한 부분만 다음 순서로 이어 보면 된다.
기존 Copy Fail Background (4) splice()와 pipe Code by Code
Dirty Frag Background (1) sk_buff와 paged frags
Dirty Frag Background (2) XFRM, ESP와 ESN
Dirty Frag Background (3) RxRPC, RXKAD와 fcrypt
본편 두 page-cache write와 patch
Plain Text
복사
Copy Fail의 authencesn sink가 기억나지 않으면 본편의 4-byte STORE 부분만 다시 보면 된다.
취약점 요약
구분 | xfrm-ESP variant | RxRPC variant |
CVE | CVE-2026-43284 | CVE-2026-43500 |
취약 subsystem | esp4, esp6, IPv4/IPv6 datagram splice path | rxrpc, RXKAD packet verification |
write sink | crypto_authenc_esn_decrypt() | rxkad_verify_packet_1()의 pcbc(fcrypt) decrypt |
primitive | controlled 4-byte STORE | key-selected 8-byte STORE |
namespace | 일반적으로 user/net namespace와 namespace 내부 CAP_NET_ADMIN 필요 | user namespace 불필요 |
주요 제약 | unprivileged user namespace 정책 | rxrpc.ko 제공·autoload 여부와 최종 PAM strategy |
두 CVE의 공통 결과는 읽기만 가능한 file의 page-cache page를 networking receive path의 crypto destination으로 오인시켜 RAM에서 수정하는 것이다.
race condition이 아니라 straight-line logic bug이므로 timing window에 의존하지 않는다.
한 줄로 보는 공통 root cause
target file page cache P
-> splice(file -> pipe)
-> splice(pipe -> socket)
-> MSG_SPLICE_PAGES
-> sender skb frags[]가 P를 참조
-> loopback/network receive
-> receiver가 shared frag provenance를 놓침
-> skb_to_sgvec()
-> src == dst인 in-place crypto
-> P에 STORE
Plain Text
복사
정상적인 zero-copy와 취약점의 경계는 page reference 전달 자체가 아니다.
핵심 invariant는 다음이다.
외부 source와 공유된 page는 packet을 수정하는 in-place path에 들어가기 전에 kernel-private copy로 분리되어야 한다.
Dirty Frag에서는 이 invariant가 SKBFL_SHARED_FRAG, skb_has_shared_frag(), skb_cow_data()/skb_unshare() 경계에서 깨졌다.
코드 읽는 지도
파일 | 역할 | 핵심 함수 |
fs/splice.c | pipe page를 bio_vec iterator로 만들어 socket send path에 넘기고 kernel 내부 MSG_SPLICE_PAGES를 설정한다. | splice_to_socket() |
net/core/skbuff.c | iterator에서 page를 추출해 skb paged frag로 붙인다. | skb_splice_from_iter(), skb_append_pagefrags() |
include/linux/skbuff.h | skb_frag_t, skb_shared_info, shared-frag flag와 ownership helper를 정의한다. | skb_has_shared_frag() |
net/ipv4/ip_output.c, net/ipv6/ip6_output.c | UDP/IP datagram의 splice page를 skb에 붙인다. ESP patch가 shared flag 표시를 추가한 지점이다. | __ip_append_data(), __ip6_append_data() |
net/ipv4/esp4.c, net/ipv6/esp6.c | ESP receive packet을 SGL로 바꾸고 in-place AEAD decrypt한다. | esp_input(), esp6_input() |
crypto/authencesn.c | ESN high 32-bit를 재배치하며 4-byte scratch STORE를 수행한다. | crypto_authenc_esn_decrypt() |
net/rxrpc/call_event.c, conn_event.c | RxRPC DATA/RESPONSE packet을 security ops로 넘기기 전에 unshare 여부를 결정한다. | rxrpc_input_call_event(), rxrpc_verify_response() |
net/rxrpc/rxkad.c | RXKAD AUTH packet의 첫 8 bytes를 in-place decrypt한다. | rxkad_verify_packet_1() |
crypto/fcrypt.c | RXKAD가 사용하는 AFS 전용 64-bit block cipher다. | fcrypt_decrypt() 계열 |
CVE-2026-43284: xfrm-ESP Page-Cache Write
Step 1. target file page를 UDP skb frag에 심는다
PoC는 ESP header와 IV를 userspace page로 pipe에 넣고, 이어서 target file의 일부를 같은 pipe에 splice한다. pipe 전체를 UDP send socket으로 splice하면 kernel이 MSG_SPLICE_PAGES 경로를 사용한다.
vmsplice(pipe_write, ESP_header_and_IV, ...);
splice(target_fd, &target_off, pipe_write, NULL, 16, ...);
splice(pipe_read, NULL, udp_send_socket, NULL, 24 + 16, ...);
C
복사
실제 sender skb는 다음 모양이 된다.
skb.linear
ESP header + IV
skb.frags[0]
page = target file page-cache P
offset = target file offset
size = 16
Plain Text
복사
여기까지는 zero-copy send의 정상 동작이다.
Step 2. loopback packet을 ESP input으로 보낸다
수신 UDP socket에 UDP_ENCAP_ESPINUDP를 설정하면 packet은 일반 UDP payload로 queue되지 않고 XFRM receive path에 들어간다.
udp_rcv()
-> xfrm4_udp_encap_rcv()
-> xfrm_input()
-> esp_input()
Plain Text
복사
PoC는 새 user/net namespace에서 attacker-controlled SA를 등록한다. SA의 algorithm은 authencesn(hmac(sha256),cbc(aes))이고 ESN을 활성화한다.
Step 3. esp_input이 COW를 건너뛴다
취약한 조건은 다음이다.
if (!skb_cloned(skb)) {
if (!skb_is_nonlinear(skb)) {
goto skip_cow;
} else if (!skb_has_frag_list(skb)) {
goto skip_cow; /* shared frags[]도 통과 */
}
}
skb_cow_data(skb, 0, &trailer);
C
복사
skb가 cloned되지 않았고 frag_list만 없으면 paged frags[]의 실제 owner를 확인하지 않는다. file page P가 그대로 보존된다.
Step 4. page P가 in-place AEAD dst가 된다
skb_to_sgvec(skb, sg, 0, skb->len);
aead_request_set_crypt(req, sg, sg, elen + ivlen, iv);
crypto_aead_decrypt(req);
C
복사
src == dst다. sg[] 뒤쪽 entry는 page P를 가리키므로 Crypto API가 수행하는 STORE도 P에 반영된다.
Step 5. authencesn이 controlled 4 bytes를 쓴다
crypto_authenc_esn_decrypt()은 authentication 전에 ESN high bits를 dst 끝으로 옮긴다.
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0);
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1);
scatterwalk_map_and_copy(tmp + 1, dst,
assoclen + cryptlen, 4, 1);
C
복사
마지막 호출이 page P에 도달한다. 위치는 packet length로 맞추고, 값은 SA 등록 시 XFRMA_REPLAY_ESN_VAL.seq_hi에 넣은 32-bit 값으로 제어한다.
AEAD authentication이 뒤에서 -EBADMSG로 실패해도 이미 수행된 4-byte STORE는 rollback되지 않는다.
Step 6. 4-byte STORE를 LPE로 확장한다
공개 PoC는 /usr/bin/su의 첫 192 bytes를 48개의 4-byte chunk로 나눠 작은 ELF payload로 교체한다.
disk inode와 setuid bit는 그대로다. 이후 init user namespace의 parent process가 execve("/usr/bin/su")를 호출하면 kernel은 오염된 page-cache content를 매핑하고 setuid-root 권한으로 payload entry point를 실행한다.
xfrm-ESP patch
Mainline fix: f4c50a4034e62ab75f1d5cdd191dd5f9c77fdff4
패치는 양쪽 경계를 함께 고친다.
1.
IPv4/IPv6 datagram append path가 splice page를 skb에 붙일 때 SKBFL_SHARED_FRAG를 표시한다.
2.
esp_input()/esp6_input() fast path가 skb_has_shared_frag(skb)를 검사하고 shared라면 skb_cow_data()로 보낸다.
- } else if (!skb_has_frag_list(skb)) {
+ } else if (!skb_has_frag_list(skb) &&
+ !skb_has_shared_frag(skb)) {
goto skip_cow;
}
Diff
복사
결과적으로 file page P는 crypto dst가 되지 않고 kernel-private copy가 decrypt된다.
CVE-2026-43500: RxRPC Page-Cache Write
Step 1. attacker key로 RXKAD connection을 만든다
PoC는 process keyring에 attacker-controlled RXKAD v1 token을 add_key("rxrpc", ...)로 등록한다.
가짜 UDP server가 CHALLENGE를 보내면 AF_RXRPC client kernel은 RESPONSE를 만들며 token의 session key K로 pcbc(fcrypt) security context를 초기화한다.
이 과정에는 user namespace나 CAP_NET_ADMIN이 필요하지 않다.
Step 2. target file page가 RxRPC DATA skb frag가 된다
forged DATA packet의 protocol header는 userspace buffer에서 만들고, security header로 처리될 8 bytes는 target file에서 splice한다.
loopback receive 후 skb frag가 target page-cache page P를 계속 가리키는 상태로 RxRPC input queue에 들어간다.
Step 3. cloned 검사만 통과하고 security ops에 들어간다
취약한 event path는 packet을 private copy로 만드는 조건을 skb_cloned(skb) 하나로 판단했다.
그러나 !skb_cloned(skb)인 skb도 externally-owned paged frag나 frag_list를 가질 수 있다. shared P가 그대로 RXKAD verifier에 전달된다.
Step 4. RXKAD가 P 위에서 8-byte decrypt를 수행한다
ret = skb_to_sgvec(skb, sg, sp->offset, 8);
memset(&iv, 0, sizeof(iv));
skcipher_request_set_crypt(req, sg, sg, 8, iv.x);
crypto_skcipher_decrypt(req);
C
복사
single block, IV=0이므로 page에 쓰이는 값은 개념적으로 다음과 같다.
plaintext P8 = fcrypt_decrypt(ciphertext C8, attacker key K)
Plain Text
복사
ESP처럼 원하는 bytes를 직접 넣는 구조는 아니다. 대신 userspace에서 K를 바꿔가며 원하는 plaintext pattern이 나오는 key를 찾는다.
Step 5. overlapping STORE로 /etc/passwd를 바꾼다
공개 PoC는 offset 4, 6, 8에 8-byte write를 겹쳐 적용한다. 각 다음 brute-force는 직전 write가 바꿔 놓은 ciphertext를 반영해야 한다.
root:x:0:0:root:...
|
+-- overlapping 8-byte STORE x 3
root::0:0:GGGGGG:...
Plain Text
복사
root password field가 비고 PAM nullok가 허용되는 환경에서는 su -가 password 없이 root shell로 이어질 수 있다.
여기서 8-byte page-cache write primitive는 kernel bug의 결과이고, 빈 password를 이용하는 su/PAM chain은 배포판 정책에 의존하는 LPE strategy다.
RxRPC patch
Mainline fix: aa54b1d27fe0c2b78e664a34fd0fdf7cd1960d71
패치는 DATA와 RESPONSE handler 모두에서 unshare 조건을 확장한다.
- if (skb_cloned(skb)) {
+ if (skb_cloned(skb) ||
+ skb_has_frag_list(skb) ||
+ skb_has_shared_frag(skb)) {
/* make a private packet before in-place decrypt */
}
Diff
복사
kernel-private NIC/GRO frags의 zero-copy fast path는 유지하면서, splice/frag_list처럼 외부 ownership이 있는 page만 copy 대상으로 보낸다.
초기 연구자 문서는 PoC가 실용적으로 도달한 RxRPC 범위를 2dc334f1a63a부터 설명했다. 현재 kernel.org CVE record와 fix commit은 d0d5c0cd1e71을 Fixes 기준으로 사용한다. 운영 영향 범위는 mainline 숫자만 추측하지 말고 배포판 advisory와 backport 상태를 기준으로 확인해야 한다.
왜 두 취약점을 chain이라고 부르는가
여기서 chain은 두 bug를 한 process에서 연속 실행해야만 root가 된다는 의미라기보다, 배포판별 blind spot을 서로 보완하는 exploit package라는 의미에 가깝다.
환경 | ESP | RxRPC |
unprivileged user namespace 허용 | 도달 가능 | module이 있으면 도달 가능 |
Ubuntu AppArmor가 userns 제한 | 일반적으로 차단 | userns가 필요 없어 우회 경로가 될 수 있음 |
rxrpc.ko 미제공 | ESP가 대안 | 도달 불가 |
즉 하나의 kernel에서 두 primitive를 반드시 합성하는 것이 아니라, xfrm-ESP와 RxRPC 중 해당 배포판에서 열려 있는 경로를 선택한다.
Copy Fail, Dirty Pipe와 비교
취약점 | page-cache page가 들어가는 곳 | 잘못된 write |
Dirty Pipe | pipe_buffer.page | stale PIPE_BUF_FLAG_CAN_MERGE로 pipe write가 file page에 append |
Copy Fail | AF_ALG TX SGL | 취약한 AEAD src/dst 구성과 authencesn 4-byte scratch STORE |
Dirty Frag ESP | skb_shared_info.frags[] | COW를 건너뛴 ESP in-place AEAD와 같은 authencesn STORE |
Dirty Frag RxRPC | skb_shared_info.frags[] | RXKAD pcbc(fcrypt) in-place 8-byte decrypt |
Copy Fail의 algif_aead module을 blacklist해도 Dirty Frag는 막히지 않는다. ESP variant는 AF_ALG가 아니라 XFRM receive path에서 같은 authencesn sink에 도달하고, RxRPC variant는 별도 skcipher sink를 사용한다.
정리
Dirty Frag는 “network crypto가 위험하다”는 취약점이 아니다. zero-copy로 외부 page를 skb frag에 연결한 뒤 그 provenance를 놓치고, kernel-private packet이라는 잘못된 전제 아래 in-place decrypt를 수행한 ownership bug다.
핵심 흐름은 다음 한 줄로 기억하면 된다.
file page cache -> pipe -> skb frag -> SGL -> in-place crypto -> page-cache STORE
Plain Text
복사
xfrm-ESP는 Copy Fail과 같은 authencesn 4-byte sink를 재사용하고, RxRPC는 pcbc(fcrypt) 8-byte decrypt로 별도 primitive를 만든다. 두 경로는 배포판별 namespace/module 차이를 서로 보완한다.








