User-space fiber (green threads/coroutines) banyak digunakan dalam database analitik modern seperti ClickHouse dan arsitektur serupa Silk untuk memangkas context switch overhead di tingkat OS kernel. Namun, keunggulan alokasi stack fiber yang kecil (umumnya 64 KB hingga 1 MB per fiber) memunculkan vektor serangan kritis: query recursion nesting dalam yang memicu silent memory corruption, serta cooperative scheduler starvation akibat query berat yang memonopoli thread worker. Artikel ini mengulas hardening database fiber C++ untuk mengeliminasi celah tersebut.

Vektor Kerentanan User-Space Fiber

Berbeda dari OS thread standar yang otomatis diproteksi oleh kernel virtual memory subsystem dengan dynamic growth dan fixed guard pages, alokasi memori stack fiber dikelola langsung oleh runtime database pada heap/mmap. Hal ini menimbulkan dua celah eksploitasi DoS utama:

  • Stack Smashing / Corrupted Adjoining Context: Jika parser atau execution pipeline mengeksekusi nested subquery, JSON path, atau deeply nested AST expressions, pointer stack (RSP) akan melewati batas bawah stack fiber. Tanpa trap memory boundary, operasi push akan menimpa memori heap atau stack fiber tetangga, memicu Remote Code Execution (RCE) potensial atau crash unrecoverable.
  • Scheduler Starvation: Fiber scheduler beroperasi secara kooperatif. Jika satu fiber mengeksekusi tight analytical compute loop (misal: scan jutaan tuple tanpa yield), fiber lain pada worker thread yang sama akan terblokir total, memicu Denial of Service (DoS) pada incoming connection.

Proteksi Stack Fiber: Guard Page & Alternative Stack

Solusi arsitektural untuk mendeteksi stack overflow pada user-space fiber adalah mengalokasikan stack menggunakan mmap terpisah, lalu menandai page paling bawah dengan proteksi PROT_NONE melalui mprotect. Ketika stack pointer melanggar batas, kernel mengirimkan SIGSEGV.

Karena stack utama fiber sudah corrupted atau exhaust saat menyentuh guard page, signal handler OS tidak dapat menggunakan stack fiber tersebut untuk push frame. Handler wajib dialihkan ke alternative signal stack via sigaltstack.

Implementasi C++20: Alokasi Fiber Stack & Deteksi Signal

#define _GNU_SOURCE
#include <iostream>
#include <sys/mman.h>
#include <unistd.h>
#include <csignal>
#include <cstdlib>
#include <cstdint>
#include <cstring>

struct FiberContext {
    void* stack_memory = nullptr;
    size_t total_size = 0;
    void* guard_page = nullptr;
    void* stack_top = nullptr; // Arah stack tumbuh ke bawah (x86_64)
};

static thread_local void* current_fiber_bottom = nullptr;

void sigsegv_handler(int sig, siginfo_t* info, void* ctx) {
    void* fault_addr = info->si_addr;
    // Deteksi jika fault terjadi pada area guard page fiber
    if (fault_addr >= current_fiber_bottom && 
        fault_addr < (char*)current_fiber_bottom + 4096) {
        const char msg[] = "[CRITICAL] Stack overflow detected on fiber guard page! Terminating fiber.\n";
        (void)!write(STDERR_FILENO, msg, sizeof(msg) - 1);
        _exit(1); // Terminasi terkontrol, hindari state corruption
    }
    _exit(sig);
}

void setup_signal_handler() {
    // Alokasikan stack alternatif untuk signal handler
    stack_t alt_stack;
    alt_stack.ss_sp = malloc(SIGSTKSZ);
    alt_stack.ss_size = SIGSTKSZ;
    alt_stack.ss_flags = 0;
    sigaltstack(&alt_stack, nullptr);

    struct sigaction sa;
    std::memset(&sa, 0, sizeof(sa));
    sa.sa_sigaction = sigsegv_handler;
    sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
    sigaction(SIGSEGV, &sa, nullptr);
}

FiberContext allocate_fiber_stack(size_t stack_size) {
    size_t page_size = sysconf(_SC_PAGESIZE);
    size_t total_size = stack_size + page_size; // Tambahan 1 page untuk guard

    // Alokasikan memori anonim dengan boundary page-aligned
    void* mem = mmap(nullptr, total_size, PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);

    // Kunci page paling bawah agar membangkitkan SIGSEGV saat diakses
    mprotect(mem, page_size, PROT_NONE);

    FiberContext context;
    context.stack_memory = mem;
    context.total_size = total_size;
    context.guard_page = mem;
    // x86_64: Stack tumbuh dari memori tinggi ke memori rendah
    context.stack_top = static_cast<char*>(mem) + total_size;

    return context;
}

void free_fiber_stack(FiberContext& ctx) {
    if (ctx.stack_memory) {
        munmap(ctx.stack_memory, ctx.total_size);
        ctx.stack_memory = nullptr;
    }
}

Mitigasi DoS: Execution Depth Check & Cooperative Yield Enforcement

Guard page adalah jaring pengaman hardware level. Di tingkat aplikasi, evaluasi AST parser dan plan execution harus dilindungi oleh recursive depth limiter dan cooperative preemption checkpoints.

1. AST Recursion Depth Guard

Cegah stack exhaustion sebelum query sempat diproses fiber executor dengan melacak kedalaman traversal via structural context:

class ExecutionPipeline {
public:
    static constexpr size_t MAX_EXPRESSION_DEPTH = 256;

    void execute_node(const ASTNode& node, size_t depth = 0) {
        if (depth > MAX_EXPRESSION_DEPTH) {
            throw std::runtime_error("Query denied: Maximum expression nesting depth exceeded.");
        }
        for (const auto& child : node.children) {
            execute_node(*child, depth + 1);
        }
    }
};

2. Cooperative Yield Enforcement

Fiber analitik query sering memproses block streaming vektor data. Untuk mencegah worker starvation, operator processing wajib mengimplementasikan counter check yang memaksa yield context kembali ke scheduler secara periodik:

struct AnalyticalScanOperator {
    size_t processed_tuples = 0;
    static constexpr size_t YIELD_BATCH_INTERVAL = 65536;

    void process_block(const VectorBlock& block) {
        for (size_t i = 0; i < block.size(); ++i) {
            // Operasi komputasi agregasi / filter
            compute(block[i]);

            if (++processed_tuples % YIELD_BATCH_INTERVAL == 0) {
                // Yield kembali ke scheduler database
                boost::fibers::this_fiber::yield();
            }
        }
    }
};

Checklist Audit Alokasi Memori Runtime Fiber

Gunakan checklist berikut saat mereview implementasi fiber backend database:

  1. Page Alignment: Pastikan seluruh alokasi stack fiber memiliki ukuran kelipatan sysconf(_SC_PAGESIZE). mprotect akan gagal jika pointer bukan kelipatan base page.
  2. Mmap Flags: Gunakan flag MAP_STACK pada platform yang mendukungnya (seperti Linux) untuk memberi hint ke kernel subsystem dan memory subsystem allocator.
  3. Stack Reuse Sanitization: Jika menggunakan pooling untuk fiber stack (misal boost::fibers::pool), implementasikan madvise(..., MADV_DONTNEED) pada reuse stack yang kotor untuk mengembalikan zeroed state dan memangkas resident set size (RSS).
  4. Signal Handler Safety: Pastikan fungsi di dalam handler SIGSEGV berstatus async-signal-safe. Hindari pemanggilan std::cout atau heap allocator standar (malloc, operator new) di dalam handler.
  5. Watchdog Preemption: Sediakan thread monitor eksternal yang memantau task runtime. Jika fiber tidak melakukan yield dalam jangka waktu threshold (misal: 2 detik), thread watchdog harus menandai fiber context sebagai canceled via atomic cancellation flag.