USB host가 정한 control request 길이가 4바이트 stack 변수의
memcpy 길이로 사용됐습니다. KUnit/KASAN으로 경계를 확인하고 mute·volume의 정확한 payload 크기를 고정한 수정까지 검증했습니다.Taegu Ha · Linux kernel · CWE-121 · 공개 레코드
2026년 5월 1일 공개 · Linux stable tree에 수정 반영 · 공식 기록
한눈에 보기공격 흐름취약점 개요조사 대상원인 분석재현 및 검증판단 변화: 발견에서 upstream까지역할과 기여회고에서 추출한 원칙후보와 반증Length provenance와 서로 다른 경계검토 후 폐기한 대안: 4바이트 Clamp증거 수준회귀 테스트리뷰 기반 패치 성숙영향과 수정확인한 범위제외한 범위수정분석 기준과 증거 보존관련 파일2026-08-23 최종 감사느낀 점
한눈에 보기
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의 actual과 length를 구분해 추적했습니다.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 data의 sizeof와 비교되지 않았습니다.단순히
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
입력에서 영향까지의 경로
- 악성 USB host가 큰
wLength의 UAC1 control request를 보냅니다.
- gadget request가 host 영향 길이로 준비됩니다.
- completion callback이 stack
u32 data를 만듭니다.
req->length바이트를 그대로 memcpy합니다.
- 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_uac1의 req->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로 읽기 어려웠음 | 기능상 수정에서 리뷰 가능한 kernel-style 수정으로 발전 | Maintainer 리뷰+사용자·Codex 검토 | |
9. v2 검증에서 v3·upstream으로
4/2 | v2는 Build-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는 제외했습니다.
- f_uac1_legacy-investigation.c — 취약 copy와 조사용 KUnit 코드가 있는 source snapshot
- uac1_legacy_runtime_test.c — invalid/valid selector 크기를 확인한 user-space model
- uac1.kunitconfig — KASAN과 legacy UAC1을 켠 KUnit 설정
- kunit-test.log — 네 regression case가 통과한 kernel log
- cve-record.json — Linux stable fix 목록이 담긴 CVE 레코드
느낀 점
kernel code에서 길이 변수의 이름보다 그 값이 어디서 왔는지가 중요했습니다.
req->length는 내부 구조체 field였지만 결국 USB host의 wLength 흐름과 연결돼 있었습니다.수정을 단순 clamp가 아니라 protocol 의미에 맞춘 exact-size validation으로 바꾸면서 memory safety와 parser correctness가 같은 문제라는 점을 배웠습니다. invalid와 valid 대조군을 같은 KUnit suite에 남긴 것도 이후 패치 검증의 기준이 됐습니다.