부팅할 때마다 정체를 바꾸는 키보드
유령 Maple 사건의 후일담. 같은 증상이 다른 PC 에서 재발해 이번에는 자동 복구 스케줄러까지 깔았고, 내친김에 "왜 이 매크로패드만 이러는가"를 끝까지 파 봤다. 답은 부팅할 때마다 USB 정체성을 한 번 갈아치우는 부팅 시퀀스, 그리고 그 밑에 깔린 십수 년 묵은 부트로더였다.
지난 수사에서 검거했던 유령 Maple, 다른 PC 에서 재발했습니다. 이번에는 잡는 것보다 다시 잡으러 오지 않아도 되게 만드는 쪽에 시간을 썼고, 왜 이 패드만 이러는지도 이참에 정리해 둡니다.
1. 재발, 그리고 이번엔 자동화#
증상은 지난번과 정확히 같았다. 장치 관리자에는 부트로더(Maple, 1EAF:0003)만 열거돼 있고 앱 펌웨어(D010:1601)는 어디에도 없다. OLED 는 꺼져 있고 키 입력도 죽어 있다. 키보드 자체는 멀쩡히 돌고 있는데 Windows 가 낡은 부트로더 항목을 물고 있는 상태, 즉 유령 Maple 이다.
처방도 같았다. kick 스크립트가 유령 Maple 장치를 pnputil /restart-device 로 재시작해 재열거를 유도하면, 앱이 VIAL 컴포지트로 다시 올라온다. 케이블은 손대지 않았다.
달라진 것은 그다음이다. 이 PC 에는 자동 kick 스케줄러가 없었기에 설치했다. 트리거는 두 개, 재연결(PnP 제거 이벤트) 직후와 로그온 시점이다. 평상시에는 아무것도 하지 않고, 앱이 이미 올라와 있으면 "nothing to do" 로 조용히 끝난다. 유령이 나타났을 때만 감지해서 재열거를 때린다.
2. 왜 이 키보드만 죽는가#
핵심은 하나다. KB16 은 부팅할 때 USB 정체성을 한 번 바꾸는데, 보통 키보드와 마우스는 그런 짓을 하지 않는다.
일반 HID 장치는 전원이 들어오면 처음부터 최종 정체성 하나로 한 번만 열거된다. 꽂는 순간 VID:PID 가 정해지고 끝. 반면 KB16 은 QMK/vial-qmk 계열이라 STM32 의 Maple DFU 부트로더(1EAF:0003)가 먼저 버스에 올라오고, 몇백 ms 뒤 앱 펌웨어(D010:1601)로 점프하면서 USB 를 스스로 뗐다가 다시 붙인다. 짧은 순간에 detach 와 reattach 가 한 번 일어나고, 그 사이 버스상의 정체성이 부트로더에서 앱으로 바뀐다.
이 아주 빠른 재부착 이벤트가 문제의 씨앗이다.
- 본체 직결: 루트 포트가 detach/reattach 를 빠르고 확실하게 재열거해서 앱이 제대로 올라온다.
- KVM 과 허브를 거치는 체인: 중간의 스위칭 로직, 전원 시퀀싱, transaction translator 가 그 찰나의 재부착 이벤트를 삼키거나 뭉갠다. Windows 는 "아직 부트로더다"라는 낡은 상태를 계속 물고 있고, 앱은 영영 안 올라와 OLED 와 키가 죽는다.
마우스나 다른 키보드에서 이 증상이 절대 안 나는 이유가 여기 있다. 그것들은 재열거 자체를 안 하니 삼켜질 이벤트가 없다. KB16 만 매 부팅마다 정체성 전환 이벤트를 만들어내고, 하필 KVM 과 허브의 조합이 그런 순간적 트랜지션에 약하다.
부차적으로 두 가지가 더 겹친다. 상용 장치는 온갖 허브·KVM 토폴로지에서 USB 컴플라이언스를 검증받지만 취미용 QMK 펌웨어는 이런 지저분한 실제 체인에서 검증된 적이 없다는 것, 그리고 OLED 가 달린 KB16 은 소비 전력이 조금 더 커서 KVM 을 거친 허브의 전원 공급이 재열거 타이밍과 겹치면 두 번째 열거를 못 끝내는 쪽으로 기운다는 것.
3. 1EAF 이 가리키는 곳#
그럼 왜 이렇게 만들었을까. 결정적 단서가 부트로더의 정체값 1EAF:0003 에 있다. VID 0x1EAF 은 LeafLabs, PID 0x0003 은 그 회사의 Maple DFU 부트로더다. KB16 이 부팅할 때 잠깐 뜨는 "Maple" 은 제조사 DOIO 가 만든 것이 아니라, 십수 년 전 아두이노 시절 STM32 생태계(STM32duino/Maple)에서 흘러온 레거시 부트로더다.
이 부트로더의 특성이 문제의 원흉이다. 전원이 들어오면 매번 USB 에 DFU 장치로 먼저 열거된다. 플래시 명령을 기다리는 몇백 ms 짜리 창이고, 그 창이 지나면 앱으로 점프하면서 재열거가 일어난다. 반면 요즘 제대로 만든 키보드는 부트로더가 버스에 아예 나타나지 않는다. 플래시 조건(특정 키 홀드, 매직 플래그)이 없으면 USB 열거를 시작하기도 전에 앱으로 바로 점프해서, 밖에서 보면 정체성이 하나로 고정돼 있다.
| Maple DFU (KB16) | 요즘 커스텀 부트로더 | |
|---|---|---|
| 평상시 부팅 | 매번 DFU 로 먼저 열거된 뒤 앱으로 재열거 (정체성 2개) | 앱으로 곧장 점프 (정체성 1개) |
| 플래시 진입 | 부팅 초입에 항상 열리는 대기 창 | 명시적 조건이 있을 때만 버스에 등장 |
| 토폴로지 검증 | 취미 생태계 기본값, 허브·KVM 검증 없음 | 벤더가 온갖 조합에서 컴플라이언스 테스트 |
4. 아낀 것은 부품값이 아니라 공수#
"저가형이라 그런가" 라는 가설은 반만 맞다. 부품값을 아낀 것이 아니라 엔지니어링 수고를 아꼈다. Maple DFU 를 쓰면 사용자가 ST-Link 나 SWD 같은 별도 하드웨어 없이 USB 만으로 펌웨어를 구울 수 있다. 취미용 QMK/vial 생태계에서 "그냥 되는" 기본값이라, 만들지 않고 갖다 쓴 것이다.
반대편의 대형 벤더는 버스에 안 뜨는 커스텀 부트로더를 직접 짜고, 그것을 온갖 허브·KVM 조합에서 검증까지 한다. 비싼 것은 그 개발과 검증 공수이고, 매크로패드 한 종 팔자고 거기까지 하기는 수지가 안 맞는다. 생태계 기본 부트로더를 얹고 나머지는 사용자가 감수하는 구조다.
그러면 부트로더를 갈아치우면 되지 않나 싶지만, 그쪽은 SWD 로 직접 구워야 해서 벽돌 위험이 있고, 이 패드의 커스텀 펌웨어가 vial 호환을 위해 2023-01-08 베이스에 고정돼 있다는 제약도 걸린다. 커스텀 펌웨어를 굽더라도 그것은 앱 펌웨어만 바꾸는 것이고 밑의 Maple 부트로더는 공장 그대로다. 실무적으로는 삼켜진 재열거를 pnputil 로 대신 한 번 더 때려주는 kick 스크립트 우회가 맞는 선택이다.
5. 곁가지: 사라진 척한 스케줄러#
이번 작업에도 삽질이 하나 있었다. 스케줄러 태스크를 설치한 직후 조회하니 태스크가 "등록됐다가 사라진 것"처럼 보였고, EDR 이 지웠나 하는 의심까지 갔다. 실제로는 SYSTEM 프린시펄로 등록된 태스크라 비관리자 조회(WSL 에서 부른 powershell.exe)에 안 보였던 것뿐이다. elevated 로 보면 처음부터 State=Ready, lastResult=0 으로 멀쩡히 등록돼 있었다.
태스크가 증발했다고 한참 소동을 벌였는데, 제 조회 권한이 낮았던 것뿐이었습니다. 유령을 쫓다 보면 멀쩡한 것까지 유령으로 보입니다. 다음부터는 elevated 로 먼저 봅니다.
이것으로 이 사건 폴더는 자동으로 닫히는 폴더가 됐다. 재발하면 PnP 이벤트를 받은 스케줄러가 유령을 감지해 재열거하고, 자동이 안 걸리는 경우에만 수동 kick 이 fallback 이다. 세 번의 수사와 두 번의 오판 끝에 낸 결론이 이제 스케줄러 한 줄로 자동 집행된다.
다음 재발은 제가 출동하기 전에 끝나 있을 예정입니다. 그게 제일 좋은 결말입니다.