Aplikasi yang membaca block device langsung tanpa melibatkan Kernel Virtual File System (VFS) dan Page Cache—seperti database berkinerja tinggi, tool forensik, atau custom direct storage engine—bekerja dengan mem-parse struktur filesystem secara independen di userspace. Pendekatan ini menghilangkan overhead alokasi skb/page cache dan konteks kernel switch, tetapi mengorbankan jaminan konsistensi yang disediakan OS.

Ketika software membaca block device langsung sementara filesystem sedang atau baru saja dimodifikasi oleh proses lain, Anda berisiko membaca data usang (stale reads) atau data korup sebagian (dirty reads). Untuk menjamin keandalan parser filesystem userspace tersebut, diperlukan sistem differential testing otomatis yang memvalidasi hasil pembacaan raw reader terhadap POSIX ground truth.

Risiko Data Drift dan Dirty Reads Akibat Bypass Page Cache

Saat aplikasi standar menulis data ke file via POSIX write(), data masuk ke kernel Page Cache dalam status dirty sebelum di-flush ke disk fisik oleh background thread kernel (flusher threads/pdflush). Jika raw disk reader membuka block device (misal /dev/nvme0n1p1 atau file image) secara langsung:

  • Dirty Read / Stale Data: Raw reader mengambil blok langsung dari storage layer. Data yang berada di dirty page cache belum ditransmisikan ke physical media, menyebabkan raw reader membaca versi data yang lama atau blok kosong (nol).
  • Metadata Drift: Kernel mungkin telah memperbarui inode di memori (misalnya ukuran file atau pointer extent tree), namun modifikasi superblok atau inode table di disk belum selesai. Raw traversal parser berisiko mem-parse metadata usang yang memetakan file ke data block yang salah.
  • Torn Reads: Pembacaan blok fisik yang tidak sinkron dapat menangkap sebagian penulisan multi-blok jika proses write dari VFS terjadi paralel dengan raw read.

Catatan: Raw disk reader hanya dapat menjamin integritas paritas data 100% jika filesystem di-mount dalam mode read-only, atau storage dalam keadaan quiesced (seluruh dirty page telah di-flush dengan sync() sebelum raw read dilakukan).

Setup Test Harness di CI Menggunakan Linux Loop Device

Verifikasi raw reader tidak memerlukan disk fisik di lingkungan CI. Kita dapat mengisolasi lingkungan pengujian menggunakan file image sparse yang di-attach ke Linux loopback device via losetup.

#!/usr/bin/env bash
set -euo pipefail

IMAGE_PATH="/tmp/ext4_parity_test.img"
MOUNT_DIR="/mnt/test_parity"
BLOCK_SIZE=4096
IMAGE_SIZE_MB=64

# 1. Buat sparse file dan format ext4
truncate -s "${IMAGE_SIZE_MB}M" "${IMAGE_PATH}"
mkfs.ext4 -b "${BLOCK_SIZE}" -O extents,dir_index "${IMAGE_PATH}"

# 2. Attach loop device dengan partisi scan aktif
LOOP_DEV=$(losetup -fP --show "${IMAGE_PATH}")
echo "Loop device created: ${LOOP_DEV}"

# 3. Mount filesystem untuk generate ground truth
mkdir -p "${MOUNT_DIR}"
mount "${LOOP_DEV}" "${MOUNT_DIR}"

# Trap cleanup saat script selesai/gagal
trap 'umount -f "${MOUNT_DIR}" 2>/dev/null || true; losetup -d "${LOOP_DEV}" 2>/dev/null || true; rm -f "${IMAGE_PATH}"' EXIT

Tahap ini memberikan dua interface akses yang identik ke storage yang sama: interface POSIX ter-mount di /mnt/test_parity, dan raw block interface di ${LOOP_DEV}.

Desain Differential Test Suite

Differential test harness membandingkan output parser biner internal (raw reader) langsung terhadap output kernel VFS (POSIX API). Jalur verifikasinya adalah:

  1. Sisi Ground Truth: Buka file path menggunakan open(), lakukan pembacaan byte stream menggunakan POSIX pread() standar.
  2. Sisi Raw Reader: Buka LOOP_DEV dengan flag O_RDONLY | O_DIRECT. Lakukan parsing manual pada ext4 superblock, block group descriptors, inode table, dan inode extents header untuk menemukan nomor blok fisik file. Baca physical block tersebut langsung dari disk.
  3. Komparasi: Verifikasi kecocokan byte-by-byte antara buffer POSIX dan buffer raw parser.
// Pseudocode logika diferensial di runner test
void verify_file_parity(const char* posix_path, const char* loop_dev, uint32_t inode_num) {
    int fd_posix = open(posix_path, O_RDONLY);
    int fd_raw = open(loop_dev, O_RDONLY | O_DIRECT);
    
    struct stat st;
    fstat(fd_posix, &st);
    off_t file_size = st.st_size;

    uint8_t* posix_buf = malloc(file_size);
    uint8_t* raw_buf = malloc(file_size);

    // 1. Baca POSIX ground truth
    pread(fd_posix, posix_buf, file_size, 0);

    // 2. Traversal raw disk reader manual
    raw_disk_read_by_inode(fd_raw, inode_num, raw_buf, file_size);

    // 3. Bitwise Assertion
    assert(memcmp(posix_buf, raw_buf, file_size) == 0);

    free(posix_buf);
    free(raw_buf);
    close(fd_posix);
    close(fd_raw);
}

Simulasi Edge Cases Ekstrem

Bug pada raw filesystem parser biasanya muncul saat struktur struktur internal block allocation terpecah. CI harus menyuntikkan tiga skenario edge case sebelum assertion dijalankan:

1. Fragmentasi Inode Extent Tree

Pada ext4, jika alokasi blok tidak berurutan, struktur tree berubah dari flat extent (ext4_extent_header.eh_depth == 0) menjadi multi-level index tree (eh_depth > 0). Raw parser harus mampu melakukan rekursi traversal node index.

# Generate fragmentasi blok fisik secara acak
python3 -c '
with open("/mnt/test_parity/fragmented.bin", "wb") as f:
    for i in range(1000):
        f.seek(i * 16384) # Lompat offset (buat sparse holes)
        f.write(b"FRAGMENT_CHUNK_" + bytes(str(i), "ascii") + b"_" * 4000)
'
# Flush agar metadata extent tree ditulis ke disk
sync

2. Boundary Unaligned Read

Storage device beroperasi pada physical boundary (misalnya 512-byte atau 4096-byte alignment). Pengujian harus mengeksekusi pembacaan data di mana panjang file atau offset request tidak selaras dengan batasan blok (misal: offset 4093, length 17 byte yang memotong boundary antara block N dan block N+1).

3. Simulasi Unwritten Cache (Dirty Pages)

Uji divergensi data dengan menulis data baru tanpa memanggil sync atau fsync, lalu baca via raw reader:

# Tulis data ke Page Cache tanpa sync
python3 -c '
with open("/mnt/test_parity/dirty_test.bin", "wb") as f:
    f.write(b"CACHE_DATA_NOT_ON_DISK")
'
# PENTING: Jangan panggil sync!
# Test harness memverifikasi bahwa Raw Reader TIDAK melihat data ini (melihat blok kosong)
# Hal ini mengonfirmasi batasan isolasi Page Cache secara deterministik.

Implementasi Assertions Deterministik pada CI Pipeline

Untuk mendeteksi silent corruption tanpa harus menyimpan seluruh data ke memori saat pengujian file berukuran besar, gunakan chunked cryptographic hashing (misal BLAKE3 atau SHA-256) per rentang 64 KiB.

#!/usr/bin/env python3
import hashlib
import os
import sys

def hash_stream(fd, offset, size, chunk_size=65536):
    hasher = hashlib.sha256()
    remaining = size
    curr_offset = offset
    
    while remaining > 0:
        read_len = min(remaining, chunk_size)
        os.lseek(fd, curr_offset, os.SEEK_SET)
        chunk = os.read(fd, read_len)
        if not chunk:
            break
        hasher.update(chunk)
        remaining -= len(chunk)
        curr_offset += len(chunk)
        
    return hasher.hexdigest()

def main():
    # Argumen: file POSIX dan file block dev yang diekstrak raw reader
    posix_file = sys.argv[1]
    raw_extracted_file = sys.argv[2]

    file_size = os.path.getsize(posix_file)
    raw_size = os.path.getsize(raw_extracted_file)

    if file_size != raw_size:
        print(f"[FATAL] Size mismatch: POSIX={file_size} vs RAW={raw_size}")
        sys.exit(1)

    fd_posix = os.open(posix_file, os.O_RDONLY)
    fd_raw = os.open(raw_extracted_file, os.O_RDONLY)

    # Verifikasi konsistensi hash per-chunk untuk mendeteksi lokasi offset korupsi
    chunk_size = 1048576 # 1 MB chunks
    num_chunks = (file_size + chunk_size - 1) // chunk_size

    for i in range(num_chunks):
        offset = i * chunk_size
        length = min(chunk_size, file_size - offset)
        
        h_posix = hash_stream(fd_posix, offset, length)
        h_raw = hash_stream(fd_raw, offset, length)

        if h_posix != h_raw:
            print(f"[CORRUPTION DETECTED] Chunk {i} at offset {offset} mismatch!")
            print(f"POSIX Hash : {h_posix}")
            print(f"RAW Hash   : {h_raw}")
            sys.exit(2)

    print("[PASS] Bitwise parity verified deterministically.")
    os.close(fd_posix)
    os.close(fd_raw)

if __name__ == "__main__":
    main()

Checklist Integrasi Pipeline

  • Hak Akses Root / Sudo: Setup loop device membutuhkan kapabilitas CAP_SYS_ADMIN di container Docker CI (jalankan container dengan opsi --cap-add=SYS_ADMIN --device /dev/loop-control).
  • Force Flush Sebelum Parity Run: Selalu jalankan sync && echo 3 > /proc/sys/vm/drop_caches sebelum memvalidasi paritas data reguler untuk memastikan status memori dan physical block identik.
  • Unmount Safety: Lakukan unmount loop device sebelum raw reader menjalankan traversal jika parser Anda belum memiliki konkurensi locking terhadap journal write ext4.