← All issues

[2] IPInt PC desynchronization via variable-length LEB128 sub-opcodes

Severity: High | Component: JSC IPInt (In-Place Interpreter) | 0acf64e

High로 분류한 근거는 다음과 같습니다. 유효한 bytecode에서 in-place interpreter가 잘못된 metadata로 잘못된 명령어를 실행하는 interpreter desynchronization이 관측 가능한 형태로 발생하며, desynchronization 이후의 instruction stream에 대해 공격자가 상당한 수준의 제어권을 갖습니다. Type confusion primitive로의 투영에 대한 confidence는 0.88이며, 패치 이전 advancePC 상수는 diff에서 직접 확인되는 것이 아니라 commit message와 test case에서 추론한 값입니다.

IPInt에서 prefix 기반 opcode(GC prefix 0xFB, SIMD prefix 0xFD)의 sub-opcode는 VarUInt32 LEB128 형식으로 인코딩됩니다. 기존에는 이를 건너뛰기 위해 advancePC(상수값)를 하드코딩해 사용했지만, LEB128은 동일한 값을 가변 길이 바이트로 인코딩하는 방식을 허용합니다. 이번 fix에서는 prefix 기반 opcode의 실제 명령어 길이를 동적으로 추적하도록 변경되었습니다.

JSTests/wasm/stress/ipint-variable-length-gc-opcodes.js

+ // redundant LEB128 인코딩을 생성하는 헬퍼 함수
+ function createRedundantLEB128(value, totalBytes) {
+ if (totalBytes === 1) {
+ return [value];
+ }
+ let result = [];
+ for (let i = 0; i < totalBytes - 1; i++) {
+ if (i === 0) {
+ result.push(value | 0x80); // 첫 번째 바이트: continuation bit 설정
+ } else {
+ result.push(0x80); // 중간 바이트: continuation bit만 있음
+ }
+ }
+ result.push(0x00); // 마지막 바이트: continuation bit 없음, 값 0
+ return result;
+ }
+ // 2바이트 redundant encoding으로 ref.i31 테스트
+ {
+ let extendedOp = createRedundantLEB128(0x1C, 2); // ref.i31 redundant: [0x9C, 0x00]
+ let codeBody = [
+ 0x00, // 로컬 변수 없음
+ 0x41, 0x2A, // i32.const 42
+ 0xFB, ...extendedOp, // redundant encoding을 사용한 ref.i31
+ 0x0B // end
+ ];
+ let module = new WebAssembly.Module(bytes);
+ let instance = new WebAssembly.Instance(module);
+ assert.eq(instance.exports.f(), 42);
+ }

이번 patch는 WasmIPIntGenerator.cpp에서 GC와 SIMD prefix opcode 핸들러 전반의 PC advancement를 동적으로 기록하도록 수정되었습니다. 대상 함수는 addRefI31, addI31GetS, addI31GetU, addArrayLen, addArrayFill, addArrayCopy, addAnyConvertExtern, addExternConvertAny 및 SIMD 관련 변형들입니다. WasmFunctionParser.h::simd()의 calling convention도 명령어 길이 정보를 전달하도록 변경되었습니다. 한편 BBQ, OMG, ConstExpr 등 모든 JIT tier에서 일부 generator method 이름이 새로운 규칙에 맞게 변경되었습니다. addExtractLaneaddSIMDExtractLane, addReplaceLaneaddSIMDReplaceLane, addConstantaddSIMDConstant입니다. InPlaceInterpreter64.asm의 dispatch logic도 함께 수정되었으며, redundant LEB128로 인코딩된 sub-opcode를 포함한 Wasm module을 구성해 올바른 실행을 검증하는 regression test도 추가되었습니다.

Before:                                  After:
Wasm bytecode: [0xFB, 0x9C, 0x00, ...]  Wasm bytecode: [0xFB, 0x9C, 0x00, ...]

IPInt PC ──► advancePC(2)                IPInt PC ──► advancePC(dynamicLen)
             skips 2 bytes                            skips 3 bytes (actual encoding)
             PC lands on 0x00 ← WRONG                 PC lands on next opcode ← CORRECT
             next decode: garbage                      next decode: correct instruction

Wasm in-place interpreter의 LEB128 가변 길이 sub-opcode에 대한 고정 길이 PC advancement 수정.

WebAssembly prefix opcode는 2단계 dispatch 구조를 사용합니다. 먼저 prefix 바이트가 opcode 그룹을 식별하는데, GC/reference type은 0xFB, SIMD는 0xFD를 사용합니다. 이어지는 sub-opcode는 해당 그룹 내 구체적인 명령어를 식별하며, VarUInt32 LEB128 형식으로 인코딩됩니다. LEB128은 가변 길이 정수 인코딩 방식으로, 각 바이트의 7비트는 데이터를, 상위 1비트는 연속 플래그로 사용합니다.

LEB128은 redundant encoding을 허용합니다. 동일한 값을 extra continuation byte로 패딩하는 방식입니다. 예를 들어 값 28(ref.i31)은 [0x1C](1바이트), [0x9C, 0x00](2바이트), [0x9C, 0x80, 0x00](3바이트)로 모두 표현 가능하며, 세 가지 모두 Wasm 명세상 의미적으로 동일한 유효한 인코딩입니다.

IPInt(In-Place Interpreter)는 JSC의 최하위 tier Wasm 실행 엔진입니다. 컴파일 없이 Wasm bytecode를 직접 해석하며, cold code 처리와 BBQ 또는 OMG JIT로 tier-up 이전의 초기 실행에 사용됩니다. IPInt는 두 개의 커서를 유지합니다. PC(program counter)는 Wasm bytecode 내 현재 위치를 가리키고, MC(metadata counter)는 디코딩된 상수, branch target, 명령어 길이 등을 담은 사전 생성 metadata를 가리킵니다. 이 두 커서는 반드시 동기화 상태를 유지해야 합니다. 각 명령어는 두 커서를 정확한 양만큼 진행시키는데, 동기화가 깨지면 이후 명령어들이 잘못된 metadata를 읽어 실행 오류가 발생합니다.

WasmFunctionParser의 validation pass는 실제 바이트 수를 파싱하여 가변 길이 LEB128을 올바르게 처리합니다. 하드코딩된 상수를 사용한 것은 IPInt 실행 tier에 한정된 문제였습니다.

이 버그의 root cause는 IPInt의 하드코딩된 PC advancement와 prefix sub-opcode의 실제 가변 길이 인코딩 사이의 불일치입니다. 패치 이전에는 ref.i31 같은 GC opcode 핸들러가 advancePC(2)를 사용했습니다. prefix 1바이트와 sub-opcode 1바이트를 합산한 2바이트를 가정한 것입니다. 그러나 sub-opcode가 redundant LEB128 바이트로 인코딩된 경우(예: [0xFB, 0x1C] 대신 [0xFB, 0x9C, 0x00]), interpreter는 PC를 2만큼 진행했지만 실제 명령어 길이는 3바이트였습니다. 결과적으로 PC가 현재 명령어의 trailing LEB128 바이트 중간에 위치하게 되고, 이후 모든 명령어가 잘못된 offset에서 디코딩되었습니다.

이는 interpreter desynchronization 버그에 해당합니다. PC와 MC 커서가 서로 어긋나게 됩니다. IPInt 아키텍처상 MC advancement가 PC 길이와 독립적이라면, MC는 올바르게 진행되는 반면 PC는 1바이트 이상 뒤처진 상태가 됩니다. 이후 각 decode는 desynchronization 양만큼 shift된 offset에서 bytecode를 읽어, 완전히 다른 명령어를 위한 metadata를 소비하면서 임의의 바이트를 opcode와 operand로 해석하게 됩니다.

🔒

Explores the PC/MC desynchronization mechanism in depth and assesses what primitives an attacker could construct from misaligned interpreter state

더 확인하려면 구독해 주세요

🔒

Multiple reusable audit patterns identified for interpreter cursor synchronization bugs, with concrete grep targets across Wasm execution tiers

더 확인하려면 구독해 주세요