CVE-2026-31720

CVE-2026-31720

Area
Linux Kernel
Credit
Public Attribution
Credit Note
CVSS
7.8
CVSS Source
NVD
CVSS Vector
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE
CWE-787 (NVD) · CWE-121 (analysis)
Impact
Memory Corruption
Key Finding
Host-controlled wLength가 4-byte stack 변수의 memcpy 길이로 이어져 kernel stack OOB write가 발생했습니다.
Published
May 1, 2026
Score Note
Linux CNA numeric score 없음 · NVD CVSS 3.1 7.8 사용
Severity
High
Status
Public
Target
Linux kernel · USB gadget legacy UAC1
USB host가 정한 control request 길이가 4바이트 stack 변수의 memcpy 길이로 사용됐습니다. KUnit/KASAN으로 경계를 확인하고 mute·volume의 정확한 payload 크기를 고정한 수정까지 검증했습니다.
Taegu Ha · Linux kernel · CWE-121 · 공개 레코드
2026년 5월 1일 공개 · Linux stable tree에 수정 반영 · 공식 기록

한눈에 보기

🔎
USB host가 정한 control request 길이가 4바이트 stack 변수의 복사 길이로 이어졌습니다. KUnit/KASAN으로 OOB write를 확인하고, mute 1바이트·volume 2바이트의 exact-size validation으로 수정 기준을 확정했습니다.
구분
확인 내용
프로젝트·컴포넌트
Linux kernel USB gadget legacy UAC1
공격 입력
USB control request의 host-controlled wLength와 payload
필요 조건
공격자가 gadget device에 USB host 역할로 control request를 보낼 수 있어야 함
취약 지점
f_audio_complete()의 4바이트 u32 data 대상 memcpy
검증 결과
KASAN stack OOB write 확인, 수정 후 invalid 2개·valid 2개 KUnit 통과
영향
kernel stack corruption 및 crash 가능성
수정 원칙
임의 clamp가 아니라 selector별 정확한 길이만 허용하고 불일치는 endpoint halt

공격 흐름

검증 결론: host influence가 completion의 copy length까지 이어지며, 4바이트 destination을 초과하는 stack write가 발생함을 KASAN으로 확인했습니다.

취약점 개요

Linux USB gadget의 legacy UAC1 function은 host가 보낸 mute와 volume control data를 completion callback에서 처리합니다. 취약 코드는 u32 data를 stack에 두고 memcpy(&data, req->buf, req->length)를 호출했습니다.
req->length는 control request의 host-influenced wLength 흐름에서 왔습니다. 4보다 큰 길이가 completion까지 오면 source data가 충분하더라도 destination stack object를 넘어 썼습니다. 지원 control은 실제로 mute 1바이트, volume 2바이트만 필요했습니다.
공격자가 제어하는 값
USB control request의 길이와 payload
취약 함수
f_audio_complete()
고정 destination
stack의 u32 data 4바이트
영향
kernel stack out-of-bounds write

조사 대상

USB gadget은 device 쪽 kernel이 host input을 받는 구조이므로 control request의 wLength, request allocation length, completion의 actuallength를 구분해 추적했습니다.
copy destination이 4바이트인데 source length를 그대로 사용한 지점을 확인한 뒤, protocol에서 지원하는 control selector마다 실제 필요한 크기를 찾았습니다. mute는 1바이트, volume은 little-endian 2바이트였습니다.
host가 큰 control length를 보내면 completion callback이 실제 selector 크기와 무관하게 stack 변수 밖으로 복사하는가?

원인 분석

audio_set_intf_req()는 control request의 wLength를 읽어 request 길이로 반환했습니다. completion에서 그 길이가 req->length로 사용됐고 destination datasizeof와 비교되지 않았습니다.
단순히 min(req->length, 4)로 자르면 stack overflow는 막지만 malformed request를 정상 control처럼 일부 처리하게 됩니다. selector별 expected size와 req->actual이 정확히 같은지 확인하고, 불일치 시 endpoint를 halt하는 수정이 protocol 의미에 맞았습니다.
f_uac1_legacy.c · 취약 copy
static void f_audio_complete(..., struct usb_request *req) { u32 data = 0; ... memcpy(&data, req->buf, req->length); audio->set_con->set(..., le16_to_cpu(data)); } // sizeof(data) == 4 // req->length is host-influenced
입력에서 영향까지의 경로
  1. 악성 USB host가 큰 wLength의 UAC1 control request를 보냅니다.
  1. gadget request가 host 영향 길이로 준비됩니다.
  1. completion callback이 stack u32 data를 만듭니다.
  1. req->length바이트를 그대로 memcpy합니다.
  1. 4바이트 이후 kernel stack이 덮입니다.

재현 및 검증

취약 함수에 KUnit test를 붙이고 KASAN과 USB gadget legacy UAC1 옵션을 켠 kernel을 빌드했습니다. 큰 request length로 completion을 호출해 destination stack 경계를 검사했습니다.
수정 후에는 invalid mute size, invalid volume size, valid mute, valid volume 네 case를 KUnit으로 남겼습니다. 별도 user-space runtime model에서도 invalid 입력은 halt되고 callback이 호출되지 않으며 정상 크기만 정확한 값으로 decode되는지 확인했습니다.
$ ./tools/testing/kunit/kunit.py run \ --kunitconfig=/tmp/uac1.kunitconfig \ --filter_glob=f_uac1_legacy_test
검사
입력·조건
관찰
의미
취약 입력
4바이트보다 큰 request length
KASAN stack OOB write
host length가 destination 초과
Invalid mute
mute, actual ≠ 1
endpoint halt, setter 미호출
부분 decode 거부
Invalid volume
volume, actual ≠ 2
endpoint halt, setter 미호출
selector 크기 검증
정상 대조군
mute 1 / volume 2 bytes
4 KUnit tests pass
정상 기능 보존
수정 후 보존된 KUnit 결과
# Subtest: f_uac1_legacy_test ok 1 f_uac1_legacy_kunit_invalid_mute_size ok 2 f_uac1_legacy_kunit_invalid_volume_size ok 3 f_uac1_legacy_kunit_valid_mute ok 4 f_uac1_legacy_kunit_valid_volume # pass:4 fail:0 skip:0 total:4

판단 변화: 발견에서 upstream까지

🧭
이 절은 완성된 결론만 나열하지 않고, 사용자와 Codex가 가설을 반증하고 증거 수준을 높인 뒤 maintainer 리뷰로 패치를 성숙시킨 과정을 기록합니다. 시간은 KST 기준입니다.
단계
당시 판단·한계
새 근거·반증
바뀐 판단
촉발 주체
1. 후보 탐색
고정 크기 복사처럼 보이는 여러 후보가 있었음
상위 길이 검사·권한 조건을 확인해 NFC·Bluetooth 계열 후보를 폐기
첫 의심을 CVE로 간주하지 않고 외부 입력이 직접 닿는 USB gadget으로 조사 축을 이동
Codex 분석
2. UAC1 후보 포착 3/31 20:06–20:09
wLength가 위험한 copy 길이로 이어질 가능성
wLength → req->length → f_audio_complete() 경로, ep0 최대 4096바이트, 4바이트 u32 data 확인
단순 길이 의심에서 최대 4096바이트가 4바이트 stack 객체를 넘을 수 있는 고신뢰 후보로 승격
Codex 분석
3. 도달성과 범위 확인
dead code이거나 UAC 전체 문제일 가능성
기본 Mute·Volume control 등록 확인, modern f_uac1req->actual 검사와 비교
표준 control로 도달 가능한 legacy UAC1 한정 문제로 범위를 좁힘
Codex 분석
4. 증거 수준 교정 3/31 20:19
source-to-sink 정적 경로는 완성됐지만 실행 증거는 없음
사용자가 ASan/KASAN 검증 여부를 질문
아직 static-only라고 명시하고 동적 검증 전에는 런타임 확정으로 표현하지 않음
사용자 질문
5. 검증 전략 전환 3/31 20:20–20:53
dummy_hcd·configfs를 통한 USB E2E가 가장 강하지만 현재 Docker 환경에서 비용과 제약이 큼
UML은 SND·USB gadget 의존성을 충족하지 못해 폐기, x86_64 QEMU+KUnit+KASAN은 실행 가능
f_audio_complete() 직접 호출로 sink의 memory primitive를 먼저 증명하되 USB E2E와 구분
사용자+Codex 공동 결정
6. 동적 확인과 교차 재현 3/31 20:53–21:15
첫 사용자 재실행은 KTAP 1..0으로 테스트가 0개 실행됨
이를 성공으로 보지 않고 하네스 누락을 진단·재삽입한 뒤 KASAN이 4바이트 객체에 64바이트 write를 확인
stack OOB primitive를 동적으로 확정하고 사용자 재실행으로 결과를 교차 확인
사용자+Codex
7. 영향과 보고 범위 교정
stack corruption이므로 더 높은 악용 가능성은 있으나 exploit은 없음
callback-direct KUnit의 한계와 legacy gadget·악성 USB host라는 전제 확인
최소 실증 영향은 kernel crash/DoS, 안정적인 RCE·권한 상승·물리 USB E2E는 미확인으로 제한
사용자 질문+Codex 검토
8. v1에서 v2로 4/1
v1은 exact-size 방향은 맞았지만 큰 조건식과 unaligned helper로 읽기 어려웠음
Greg의 리뷰가 selector별 switch, constant-size memcpy, endian 처리를 제안
기능상 수정에서 리뷰 가능한 kernel-style 수정으로 발전
Maintainer 리뷰+사용자·Codex 검토
9. v2 검증에서 v3·upstream으로 4/2
v2Build-tested: not tested; 최초 fixed-test 하네스도 dummy endpoint에서 실패
하네스를 보정하고 invalid Mute·Volume과 valid Mute·Volume 4개를 함께 통과
v3 제출 후 upstream commit으로 반영
Maintainer 요구+사용자·Codex 검증

역할과 기여

  • 사용자 — 확실한 검증을 요구하고 static-only 상태를 드러내는 질문을 했으며, 직접 KUnit을 재실행하고 maintainer 리뷰의 타입·복사·endian 의미를 반복 확인했습니다.
  • Codex — 거짓 양성 후보를 폐기하고 source-to-sink·도달성·범위를 추적했으며, 검증 전략 전환·하네스 실패 진단·KASAN 해석·영향 주장 제한을 담당했습니다.
  • Maintainer — USB gadget 위협 모델과 공개 패치 경로를 안내하고, 가독성·constant-size copy·endian·테스트 요구를 통해 v1을 upstream 가능한 v3로 성숙시켰습니다.
🔐
판단 로그의 원본: 비공개 Codex JSONL 세션 019d4298-4219-7690-83a9-1fac5166d5b4 · SHA-256 d3d891622690b53b3e282fe20314660dd26cccd9cc869e6d084e559fb176383d. 공개 페이지에는 민감한 raw transcript 대신 판단 전환과 검증 근거만 요약했습니다.

회고에서 추출한 원칙

후보와 반증

위험해 보이는 copy 하나만으로 취약점을 확정하지 않았습니다. 상위 bound, 권한 조건, 기본 control 등록, 최신 구현의 방어를 차례로 확인하면서 거짓 양성을 버리고 legacy UAC1의 실제 범위를 좁혔습니다.

Length provenance와 서로 다른 경계

처음부터 wLength의 host influence 가능성을 추적했습니다. composite core의 4096바이트 상한은 ep0 source buffer의 범위를 제한하지만, completion의 4바이트 destination을 보호하지는 않습니다. 변수 이름보다 값의 출처와 각 경계가 무엇을 보호하는지를 구분하는 것이 핵심이었습니다.

검토 후 폐기한 대안: 4바이트 Clamp

min(req->length, 4) 방식은 실제로 구현했다가 되돌린 패치가 아니라 검토 가능한 단순 대안입니다. 이 방식은 memory safety만 막고 malformed control의 앞부분을 정상 값처럼 처리할 수 있으므로, 최초 수정 방향부터 Mute 1바이트·Volume 2바이트의 exact-size validation과 mismatch halt를 선택했습니다.

증거 수준

정적 분석은 외부 USB host input에서 completion까지의 reachability를 확인했습니다. callback-direct KUnit/KASAN은 sink의 stack OOB write를 동적으로 확인했지만 실제 USB SET_CUR packet, configfs enumeration, 물리 controller를 통과하지 않았습니다. 따라서 결론은 host-controlled kernel stack write까지로 제한했습니다.

회귀 테스트

정상 Mute·Volume이 실제로 깨진 것을 관찰한 것은 아닙니다. maintainer의 테스트 요구에 따라 malformed 입력을 거부하는 invalid 2개와 정상 기능을 보존하는 valid 2개를 함께 검증했습니다. 이 네 KUnit은 최종 upstream patch에 포함된 테스트가 아니라 e87a0d831825-dirty 임시 test worktree에서 수행한 local validation입니다.

리뷰 기반 패치 성숙

실제 패치 변화는 clamp에서 exact-size로의 변경이 아니라 v1의 복잡한 조건식·unaligned 처리에서 maintainer가 제안한 switch, constant-size memcpy, endian 처리로 바뀐 것입니다. v2의 미검증 상태를 build와 4-case KUnit으로 보강한 뒤 v3가 upstream에 반영됐습니다.

영향과 수정

공격 USB host는 legacy UAC1 gadget 기능을 사용하는 Linux device에 malformed control request를 보내 kernel stack을 덮을 수 있습니다. 물리 또는 USB-over-network 등 host 역할로 gadget에 요청할 수 있는 위치가 필요합니다.

확인한 범위

  • host-influenced length에서 4-byte destination으로의 copy
  • KASAN으로 확인한 stack OOB write
  • selector별 fixed size 수정과 KUnit 회귀 통과

제외한 범위

  • 모든 USB audio driver의 영향
  • 특정 실장치에서의 end-to-end exploit
  • 안정적인 kernel code execution

수정

수정은 generic u32 data copy를 제거하고 mute는 1바이트, volume은 2바이트일 때만 해당 크기를 local 변수로 복사합니다. 실제 길이가 다르면 endpoint를 halt하고 control setter를 호출하지 않습니다.

분석 기준과 증거 보존

항목
최종 감사 결과
소스 기준
보존 KUnit log build ID e87a0d831825-dirty; Docker 감사 checkout 48278fa0309332ac0c4bc0499bb193ef1417f1b0; UAC1 fix tree 34695f8b9775a109521f98a70a955931f3cf04e0
원본 검증
취약 copy path, user-space runtime model, KUnit/KASAN, invalid·valid 대조군, v2·v3 patch와 fix tree의 reverse apply-check
GitHub 보존 상태
Public 저장소 main의 감사 commit c04cacf; evidence 8개와 root manifest.json에 size·SHA-256 고정
무결성
사례별 SHA256SUMS 검증 통과, root manifest의 모든 file hash와 실제 파일 일치, README 상대 링크 확인 완료
보존 경계·제약
KUnit log build와 patch tree의 동일성은 단정하지 않음. 전체 kernel build tree·binary와 물리 USB end-to-end는 제외
감사 완료
2026-08-23 · Docker/로컬 원본 대조 후 GitHub push 완료

관련 파일

2026-08-23 최종 감사

조사 소스, runtime model, KUnit 설정·통과 log, 공개 CVE 레코드와 UAC1 v2·v3 patch를 보존했습니다. 전체 build binary는 제외했습니다.

느낀 점

kernel code에서 길이 변수의 이름보다 그 값이 어디서 왔는지가 중요했습니다. req->length는 내부 구조체 field였지만 결국 USB host의 wLength 흐름과 연결돼 있었습니다.
수정을 단순 clamp가 아니라 protocol 의미에 맞춘 exact-size validation으로 바꾸면서 memory safety와 parser correctness가 같은 문제라는 점을 배웠습니다. invalid와 valid 대조군을 같은 KUnit suite에 남긴 것도 이후 패치 검증의 기준이 됐습니다.