REGRESSION(310826@main): "Zhuyin - Traditional" input method stalls for multiple seconds
Component: WebKit UIProcess (macOS) | 2b95292
macOS용 WebKit은 키보드 이벤트를 NSTextInputClient의 handleEventByInputMethod:를 통해 라우팅하며, 이 메서드는 XPC를 거쳐 시스템 입력기로 비동기 dispatch됩니다. 이전 fix (297270@main)는 m_interpretKeyEventHoldingTank를 도입해 DOM 이벤트 순서를 강제했습니다. Google Docs 같은 에디터가 요구하는 대로, keydown이 IM이 발생시키는 compositionstart/update 이벤트보다 먼저 web process에 도달해야 하기 때문입니다. 이를 위해 진행 중인 IM 호출이 있으면 모든 키보드 이벤트를 그 뒤로 직렬화하는 방식을 사용했습니다.
Source/WebKit/UIProcess/mac/WebViewImpl.h
Source/WebKit/UIProcess/mac/WebViewImpl.mm
TCIM은 이 직렬화 모델 하에서 정상 동작하지 않습니다. XPC 큐 깊이를 liveness 신호로 사용하기 때문입니다. 큐가 비어 있으면 메인 스레드가 백그라운드 작업 쪽으로 흘러가면서 수 초간 정지하는 현상이 발생합니다. 이번 fix는 keydown이 holding tank를 완전히 우회하도록 만들어 IM의 runloop에 이벤트가 끊임없이 공급되도록 합니다. 반면 keyup은 짝을 이루는 keydown의 completion이 발생할 때까지 계속 보류됩니다. 단일 optional command vector는 keydown별 큐를 담는 Deque로 바뀌었으며, front slot은 항상 IM이 현재 처리 중인 keydown에 대응합니다.
Before:
KeyDown N KeyDown N+1 KeyUp N
│ │ │
├──► IM (XPC) ├──► HoldingTank ├──► HoldingTank
│ in-flight │ (TCIM XPC │ (waiting)
│ │ queue empty, │
│ │ stalls) │
└── completes ────┴──────────────────┴─── drain tank (burst)
After:
KeyDown N KeyDown N+1 KeyUp N
│ │ │
├──► IM (XPC) ├──► IM (XPC) ├──► HoldingTank
│ Deque[0] │ Deque[1] │ (waiting)
│ (front) │ (back) │
└── N completes ──┘ └── released by N's completion
└──► handleEventByInputMethod: (bypass tank)
Significance
이제 복수의 keydown이 동시에 입력기로 in-flight 상태를 유지할 수 있으며, 단일 holding tank는 keydown별 Deque로 대체되었습니다. 이 파이프라인은 UI process를 시스템 입력기 및 web process와 연결하는 지점입니다. Web process에 대한 keydown→keyup IPC 순서 보장은 그대로 유지되지만, 이제는 엄격한 직렬화가 아니라 Deque front-slot 대응 관계에 의존하는 구조가 되었습니다.
Audit directions
Narrow: front-of-deque invariant, 즉 "front는 항상 IM이 현재 처리 중인 keydown이다"라는 전제는 IM 스레드가 순서를 지켜 동작한다는 가정일 뿐, WebKit이 직접 강제하는 규칙이 아닙니다. 오동작하거나 공격자가 제어하는 입력기, 예를 들어 시스템 전역에 설치된 악성 IME가 doCommandBySelector:/insertText:/setMarkedText: 콜백을 순서에 어긋나게 발생시키면 이 대응 관계가 깨집니다. 그 결과 한 키 입력의 명령이 다른 키 입력의 큐로 잘못 라우팅될 수 있습니다. 단서는 순서 보장이 외부 컴포넌트에만 의존하는 큐에 대해 takeFirst()를 호출한다는 점입니다.
Wider: 하나의 Deque에 두 경로가 기록되는 구조입니다. collectKeyboardLayoutCommandsForEvent는 prepend를 사용하는 반면 IM 경로는 append를 사용하며, 이 prepend는 back에서 다른 IM 기반 키 입력이 in-flight 상태이더라도 콜백을 동기 경로의 큐로 라우팅하도록 명시적으로 설계되어 있습니다. 서로 반대쪽 끝에서 삽입하는 두 writer가 하나의 순서 있는 구조를 공유하는 패턴은 전반적으로 점검해볼 가치가 있습니다. WebViewImpl과 WKContentView의 다른 Deque 멤버들에 대해서도 prepend/append가 혼재된 writer가 있는지, interleaving 케이스는 어떻게 처리되는지 살펴볼 필요가 있습니다.
// collectKeyboardLayoutCommandsForEvent: prepend (synchronous path, must be front)
m_collectedKeypressCommands.prepend(Vector<WebCore::KeypressCommand> { });
// ...
auto commands = m_collectedKeypressCommands.takeFirst();
// interpretKeyEvent IM path: append (concurrent keydowns go to back)
if ([event type] == NSEventTypeKeyDown)
m_collectedKeypressCommands.append(Vector<WebCore::KeypressCommand> { });
Widest: 동기 completion handler를 통한 re-entrancy입니다. Keyup이 released되는 경로는 Deque를 비우지 않는 inline completion과 함께 handleEventByInputMethod:를 dispatch합니다. 동기적 completion은 Cocoa에서 유효한 동작이므로, keyup의 inline block이 아직 스택에 남아 있는 상태에서 다음 keydown의 completion handler가 발생할 수 있습니다. 이때 drain 로직이 이런 중첩 상황을 고려하지 않았을 가능성이 있습니다. 이는 위에서 다룬 WKRevealItemPresenter UAF와 동일한 형태이므로, 비동기로 가정된 Cocoa completion handler는 모두 점검할 필요가 있습니다. 관련해서, 이런 비동기 lambda 안에서 이루어지는 모든 WeakPtr→CheckedPtr dereference는 이전부터 존재했던 teardown-during-composition 표면이지만, 이제는 더 많은 동시성 경로를 통해 실제로 실행되는 지점이 됩니다.