Menjaga keamanan memori saat mengekspos API Rust ke C++ atau JavaScript melalui Foreign Function Interface (FFI) memerlukan pendekatan sistematis. Perbedaan model kepemilikan (ownership) dan manajemen alokasi antar-runtime rentan memicu use-after-free, double free, dan kebocoran memori. Diplomat menyederhanakan binding FFI dengan menghasilkan kode C-ABI yang aman dan tipe native di bahasa target, tetapi verifikasi perilakunya tetap memerlukan strategi pengujian berbasis sanitizer dan interpreter semantik memori.

Arsitektur Kepemilikan Memori pada Diplomat

Diplomat menggunakan aturan kepemilikan eksplisit pada modul definisinya. Tipe data Rust yang dibungkus dalam modul Diplomat umumnya ditranslasikan menjadi pointer opaque berbobot kepemilikan (owned) atau referensi sementara (borrowed).

// Rust FFI Definition (src/lib.rs)
#[diplomat::bridge]
pub mod ffi {
    use std::sync::atomic::{AtomicUsize, Ordering};

    static ACTIVE_INSTANCES: AtomicUsize = AtomicUsize::new(0);

    #[diplomat::opaque]
    pub struct SessionManager {
        payload: Vec<u8>,
    }

    impl SessionManager {
        pub fn create(size: usize) -> Box<SessionManager> {
            ACTIVE_INSTANCES.fetch_add(1, Ordering::SeqCst);
            Box::new(SessionManager {
                payload: vec![0u8; size],
            })
        }

        pub fn process_data(&self) -> usize {
            self.payload.len()
        }

        pub fn active_count() -> usize {
            ACTIVE_INSTANCES.load(Ordering::SeqCst)
        }
    }

    impl Drop for SessionManager {
        fn drop(&mut self) {
            ACTIVE_INSTANCES.fetch_sub(1, Ordering::SeqCst);
        }
    }
}

Aturan penting yang wajib diverifikasi:

  • Objek bertipe Box<T> ditransfer sebagai pointer eksklusif ke host (C++ atau JS). Host bertanggung jawab memanggil destruktor bawaan Diplomat (session_manager_destroy atau ekuivalen RAII std::unique_ptr di wrapper C++).
  • Metode yang menerima &self mengharuskan pointer host tetap valid selama siklus eksekusi panggilan fungsi dan tidak boleh dimutasi dari thread lain secara bersamaan tanpa sinkronisasi.
  • Runtime berbasis Garbage Collector seperti V8 (Node.js) mengandalkan FinalizationRegistry untuk memicu pembersihan objek Rust. Keterlambatan siklus GC dapat menyebabkan retensi memori sementara di heap Rust.

Strategi Verifikasi Alokasi Memori Lintas Bahasa

Pengujian regresi alokasi memori memerlukan assertion pada level integritas native pointer dan penghitung alokasi (allocator instrumentation).

1. Verifikasi RAII pada C++

Pada target C++, Diplomat menghasilkan wrapper RAII berupa std::unique_ptr kustom. Uji harness harus memastikan alokasi dan dealokasi terjadi simetris tanpa memory leak.

// tests/test_memory.cpp
#include <cassert>
#include "SessionManager.hpp"

void test_ownership_lifecycle() {
    assert(SessionManager::active_count() == 0);
    {
        auto session = SessionManager::create(1024);
        assert(SessionManager::active_count() == 1);
        assert(session.process_data() == 1024);
        // session keluar dari scope, destruktor terpanggil
    }
    assert(SessionManager::active_count() == 0);
}

int main() {
    test_ownership_lifecycle();
    return 0;
}

2. Verifikasi Siklus Hidup pada JavaScript/Node.js

Karena waktu penanganan GC di JS tidak deterministik, pengujian perlu memaksa pembersihan via flag internal V8 (--expose-gc) untuk memastikan pointer Rust tidak menggantung (dangling).

// tests/test_lifecycle.mjs
import { SessionManager } from '../js-bindings/index.mjs';
import assert from 'node:assert';

function runScope() {
    const session = SessionManager.create(2048);
    assert.strictEqual(session.processData(), 2048);
    // Referensi session dilepas setelah scope ini berakhir
}

runScope();
if (global.gc) {
    global.gc();
} else {
    console.warn("Run node with --expose-gc for accurate lifecycle assertions");
}

assert.strictEqual(SessionManager.activeCount(), 0);

Verifikasi Semantik Memori Menggunakan Miri

AddressSanitizer mendeteksi pelanggaran memori pada binary hasil kompilasi, tetapi Miri memeriksa semantik internal Rust (Stacked Borrows / Tree Borrows) untuk memastikan lapisan FFI tidak menghasilkan undefined behavior sebelum kode diintegrasikan ke C-ABI.

Eksekusi Miri langsung pada unit test Rust FFI:

# Periksa kepatuhan pointer aliasing dan undefined behavior
cargo +nightly miri test --lib
Catatan: Miri berjalan pada level representasi MIR Rust murni dan tidak mengeksekusi kode biner C/C++. Oleh karena itu, target unit test untuk Miri harus memvalidasi wrapper FFI tanpa memanggil compiler C++ eksternal.

Integrasi AddressSanitizer (ASan) pada Pipeline CI

Untuk mendeteksi buffer overflow, use-after-free, dan memory leak pada boundary Rust-C++, kedua sisi harus dikompilasi menggunakan flag sanitizer yang kompatibel.

Konfigurasi GitHub Actions

name: FFI Reliability & Memory Sanity

on: [push, pull_request]

jobs:
  ffi-asan-verification:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust Nightly Toolchain
        uses: dtolnay/rust-toolchain@nightly
        with:
          components: miri

      - name: Execute Miri on Rust FFI Core
        run: cargo miri test

      - name: Build Rust Native Library with ASan
        run: |
          RUSTFLAGS="-Zsanitizer=address" \
          cargo +nightly build --target x86_64-unknown-linux-gnu

      - name: Compile and Run C++ Tests with ASan
        run: |
          clang++ -fsanitize=address -g -I./generated-bindings/cpp \
            tests/test_memory.cpp \
            ./target/x86_64-unknown-linux-gnu/debug/libffi_core.a \
            -lpthread -ldl -o test_cpp_asan
          ASAN_OPTIONS=detect_leaks=1:abort_on_error=1 ./test_cpp_asan

      - name: Run JS FFI Memory Validation
        run: |
          npm install
          node --expose-gc tests/test_lifecycle.mjs

Kesalahan Umum dan Mitigasi

  • Ketidakcocokan Alokator: Membebaskan memori yang dialokasikan oleh Rust menggunakan free() standar C. Solusi: Selalu gunakan fungsi pembebasan yang dihasilkan oleh Diplomat agar dealokasi kembali dieksekusi oleh Rust allocator (biasanya jemalloc atau System).
  • Passing Borrowed Struct melewati Batas Lifetime: Mengakses struct berstatus borrowed setelah objek induknya di-dealokasi di sisi target. Solusi: Tandai dependensi lifetime secara ketat pada anotasi bridge Diplomat dan enforce ownership checks di wrapper C++.
  • ASan False Positives karena Versi LLVM Tidak Sinkron: Membangun modul Rust dengan rustc (berbasis LLVM versi N) dan mengompilasi C++ dengan Clang versi lain. Solusi: Samakan versi mayor Clang yang digunakan oleh runner CI dengan versi internal LLVM rustc (dapat dicek via rustc -vV).