V8 Sandbox dirancang untuk mengisolasi memori engine JavaScript dari heap proses utama guna membatasi dampak exploitasi berbasis pointer corruption. Celah logika isolasi dan race condition pada subsystem V8 dapat merusak integritas memori sandbox. Mengintegrasikan automated fuzzing langsung ke dalam continuous integration (CI) memungkinkan pendeteksian dini regresi memory safety sebelum kode masuk ke branch utama.

Arsitektur V8 Sandbox dan Vektor Kerentanan

V8 Sandbox membatasi akses memori JavaScript engine ke dalam ruang virtual memory address terisolasi (biasanya 1 TB). Pointer internal direpresentasikan sebagai sandbox pointer table index atau offset relatif, bukan raw 64-bit virtual address. Kerusakan timbul jika operasi dereferensi bypass table validation atau runtime API native secara tidak sengaja menerima raw pointer yang berada di luar address space sandbox.

Integrasi LibFuzzer bersama AddressSanitizer (ASan) memungkinkan pemantauan akses out-of-bounds, use-after-free, dan uninitialized memory access pada heap V8 selama proses evaluasi bytecode acak berlangsung.

Membuat Target Harness LibFuzzer untuk V8

Harness yang efisien menghindari inisialisasi engine berulang di dalam inner loop. Alokasi v8::Isolate dan v8::Platform dilakukan sekali, sementara scope eksekusi di-reset setiap iterasi data baru.

#include <stddef.h>
#include <stdint.h>
#include <string>
#include "libplatform/libplatform.h"
#include "v8.h"

static std::unique_ptr<v8::Platform> g_platform;
static v8::Isolate* g_isolate = nullptr;

extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) {
    v8::V8::InitializeICUDefaultLocation((*argv)[0]);
    v8::V8::InitializeExternalStartupData((*argv)[0]);
    g_platform = v8::platform::NewDefaultPlatform();
    v8::V8::InitializePlatform(g_platform.get());
    v8::V8::Initialize();

    v8::Isolate::CreateParams create_params;
    create_params.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator();
    g_isolate = v8::Isolate::New(create_params);
    return 0;
}

extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
    if (size == 0) return 0;

    v8::Isolate::Scope isolate_scope(g_isolate);
    v8::HandleScope handle_scope(g_isolate);
    v8::Local<v8::Context> context = v8::Context::New(g_isolate);
    v8::Context::Scope context_scope(context);

    std::string js_code(reinterpret_cast<const char*>(data), size);
    v8::Local<v8::String> source;
    if (!v8::String::NewFromUtf8(g_isolate, js_code.c_str(), v8::NewStringType::kNormal).ToLocal(&source)) {
        return 0;
    }

    v8::TryCatch try_catch(g_isolate);
    v8::Local<v8::Script> script;
    if (v8::Script::Compile(context, source).ToLocal(&script)) {
        v8::MaybeLocal<v8::Value> result = script->Run(context);
        (void)result;
    }

    return 0;
}

Pipeline GitHub Actions dengan LibFuzzer dan AddressSanitizer

Eksekusi fuzzing pada CI memerlukan trade-off antara coverage dan waktu build. Setup pipeline melibatkan kompilasi binary menggunakan clang++ dengan flag sanitasi, manajemen corpus via cache, serta upload crash artifact jika LibFuzzer mendeteksi pelanggaran memori.

name: V8 Sandbox Fuzzing

on:
  pull_request:
    paths:
      - 'src/**'
      - 'test/fuzz/**'

jobs:
  fuzz:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Cache Fuzz Corpus
        uses: actions/cache@v4
        with:
          path: corpus/
          key: fuzz-corpus-${{ runner.os }}-${{ hashFiles('test/fuzz/**') }}
          restore-keys: |
            fuzz-corpus-${{ runner.os }}-

      - name: Build Harness with ASan
        run: |
          mkdir -p build corpus crashes
          clang++ -O1 -g -fsanitize=fuzzer,address \
            -fno-omit-frame-pointer \
            -I./include \
            test/fuzz/v8_fuzzer.cc \
            -L./out/x64.release/obj -lv8_monolith \
            -lpthread -ldl \
            -o build/v8_fuzzer

      - name: Minimize Corpus
        run: |
          mkdir -p minimized_corpus
          ./build/v8_fuzzer -merge=1 minimized_corpus corpus || true
          rm -rf corpus
          mv minimized_corpus corpus

      - name: Run LibFuzzer
        run: |
          ./build/v8_fuzzer corpus \
            -max_total_time=600 \
            -artifact_prefix=crashes/ \
            -print_final_stats=1

      - name: Upload Crash Artifacts
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: crash-artifacts
          path: crashes/*

Corpus Minimization dan Manajemen Artefak

Seiring waktu, dataset input fuzzing bertambah besar dan berulang. Flag -merge=1 pada LibFuzzer mengeliminasi input duplikat yang memicu execution path yang sama, mengurangi disk space serta waktu warm-up pipeline. Bila crash terdeteksi, LibFuzzer menulis payload penyebab crash ke direktori crashes/. File ini diunggah sebagai build artifact GitHub Actions untuk debugging lokal.

Reproduksi dan Validasi Crash Lokal

Pengembang dapat mereproduksi laporan bug dari artifact CI secara deterministik tanpa perlu menjalankan kembali seluruh fuzzing engine. Jalankan harness target dengan path file artifact sebagai argumen tunggal:

./build/v8_fuzzer crashes/crash-9d8a3f892c5a01b7

AddressSanitizer mencetak stack trace lengkap yang memetakan baris kode spesifik tempat terjadinya alokasi liar, invalid read/write, atau escape dari sandbox boundary.

Optimasi Runtime untuk CI Pipeline

  • Persistent Process Mode: Jangan memuat platform V8 di dalam loop LLVMFuzzerTestOneInput. Inisialisasi platform dan isolate pada LLVMFuzzerInitialize menekan overhead runtime hingga 90%.
  • Batasi Eksekusi per Input: Gunakan parameter -timeout=2 untuk menghentikan input yang memicu loop JavaScript tak terbatas.
  • Bypass JIT Optimization saat Fuzzing Parser: Gunakan flag runtime engine --no-opt atau --jitless jika fokus pengujian berada pada syntax parsing atau AST manipulation untuk mempercepat eksekusi per iterasi.