Driver GPU dan compiler shader internal (seperti Vulkan loader atau shader runtime WebGPU) kerap mengasumsikan bahwa bytecode SPIR-V yang diterima telah terformat sempurna. Mengirimkan bytecode SPIR-V mentah dari sumber yang tidak tepercaya—seperti aset modifikasi game, compute shader remote, atau input web—dapat memicu crash driver, out-of-bounds read/write, alokasi memori berlebih, hingga eksekusi kode acak pada level native.
Validasi pre-flight struktural pada boundary aplikasi mutlak diperlukan sebelum byte diserahkan ke API driver (seperti vkCreateShaderModule). Menggunakan Zig, validasi biner defensif dapat dibangun dengan zero-allocation, memanfaatkan keamanan slice bawaan, dan mengeliminasi risiko runtime panic.
Struktur Biner SPIR-V dan Vektor Serangan
File biner SPIR-V adalah aliran kata 32-bit (4-byte words). Lima kata pertama merupakan header biner standar:
- Word 0 - Magic Number: Nilai tetap
0x07230203. Jika urutan byte terbaca terbalik (0x03022307), payload menggunakan endianness berbeda dari host. - Word 1 - Version: Versi spesifikasi (misalnya
0x00010500untuk SPIR-V 1.5). - Word 2 - Generator Magic: Identifier toolchain pembuat shader.
- Word 3 - Bound: Batas atas untuk seluruh ID SSA dalam modul. Setiap ID yang digunakan atau dideklarasikan harus memiliki nilai strictly
< Bound. - Word 4 - Schema: Dipesan untuk format schema (selalu bernilai
0pada SPIR-V standar).
Setelah offset kata ke-5, payload berisi deretan instruksi. Setiap instruksi diawali dengan satu kata 32-bit berkode ganda:
- 16 bit rendah (
word & 0xFFFF): Opcode instruksi (misalOpCapability,OpFunction). - 16 bit tinggi (
word >> 16): Word Count (panjang instruksi dalam satuan kata 32-bit, termasuk kata header instruksi itu sendiri).
Titik Rentan (Attack Vectors)
- Zero Word Count DoS: Jika
word_count == 0, loop parser naif yang menambahkan counter instruksi sebesar panjang kata akan mengalami infinite loop, membekukan thread compiler shader. - Instruction Boundary Overrun: Nilai
word_countyang lebih besar dari sisa buffer memori memicu out-of-bounds memory reading atau buffer overflow saat parser mencoba membaca operand berikutnya. - Bound Exhaustion: Penyerang dapat menyetel Word 3 (
Bound) ke nilai ekstrem seperti0xFFFFFFFF. Jika downstream compiler mengalokasikan array referensi ID berdasarkan nilai Bound tanpa validasi batas atas, sistem akan kehabisan RAM (Out-Of-Memory DoS).
Implementasi Parser Defensif di Zig
Kode parser di bawah memverifikasi integritas struktural SPIR-V tanpa alokasi memori dinamis dan bebas panic. Parser memastikan alignment byte, validitas magic number, batasan resource, serta kepatuhan word count setiap instruksi terhadap ukuran buffer.
const std = @import("std");
pub const ValidationError = error{
BufferTooSmall,
InvalidAlignment,
InvalidMagicNumber,
InvalidWordCount,
InstructionOutOfBounds,
ResourceLimitExceeded,
};
pub const ValidationLimits = struct {
max_instructions: usize = 100_000,
max_id_bound: u32 = 0x0040_0000, // Maksimum 4 juta ID
};
pub const SpirvHeader = struct {
endian: std.builtin.Endian,
version: u32,
generator: u32,
bound: u32,
schema: u32,
};
pub fn validateSpirv(bytes: []const u8, limits: ValidationLimits) ValidationError!SpirvHeader {
// 1. Validasi ukuran minimal: Header SPIR-V adalah 5 kata 32-bit (20 byte)
if (bytes.len < 20) {
return ValidationError.BufferTooSmall;
}
// 2. Validasi alignment 4-byte
if (bytes.len % 4 != 0) {
return ValidationError.InvalidAlignment;
}
// 3. Deteksi Magic Number & Endianness
const magic_raw = bytes[0..4].*;
const endian: std.builtin.Endian = if (std.mem.eql(u8, &magic_raw, &[_]u8{ 0x03, 0x02, 0x23, 0x07 }))
.little
else if (std.mem.eql(u8, &magic_raw, &[_]u8{ 0x07, 0x23, 0x02, 0x03 }))
.big
else
return ValidationError.InvalidMagicNumber;
// Helper closure untuk membaca u32 sesuai endianness
const readWord = struct {
fn get(buf: []const u8, word_index: usize, e: std.builtin.Endian) u32 {
const offset = word_index * 4;
const slice = buf[offset .. offset + 4];
return std.mem.readInt(u32, slice[0..4], e);
}
}.get;
const version = readWord(bytes, 1, endian);
const generator = readWord(bytes, 2, endian);
const bound = readWord(bytes, 3, endian);
const schema = readWord(bytes, 4, endian);
// 4. Batasi ID Bound untuk mencegah alokasi memori berlebih di compiler
if (bound == 0 or bound > limits.max_id_bound) {
return ValidationError.ResourceLimitExceeded;
}
// 5. Validasi aliran instruksi
const total_words = bytes.len / 4;
var word_offset: usize = 5;
var instruction_count: usize = 0;
while (word_offset < total_words) {
instruction_count += 1;
if (instruction_count > limits.max_instructions) {
return ValidationError.ResourceLimitExceeded;
}
const first_word = readWord(bytes, word_offset, endian);
const word_count = @as(usize, @intCast(first_word >> 16));
// Word count < 1 adalah pelanggaran spesifikasi (memicu infinite loop DoS)
if (word_count == 0) {
return ValidationError.InvalidWordCount;
}
// Cegah out-of-bounds truncation
if (word_offset + word_count > total_words) {
return ValidationError.InstructionOutOfBounds;
}
word_offset += word_count;
}
return SpirvHeader{
.endian = endian,
.version = version,
.generator = generator,
.bound = bound,
.schema = schema,
};
}
Pengujian Terhadap Serangan Binary Malformed
Pengujian unit berikut membuktikan bahwa parser berhasil menolak payload berbahaya tanpa memicu panic runtime.
test "Reject payload too small and invalid alignment" {
const empty_data = [_]u8{};
try std.testing.expectError(ValidationError.BufferTooSmall, validateSpirv(&empty_data, .{}));
const unaligned = [_]u8{ 0x03, 0x02, 0x23, 0x07, 0x00 };
try std.testing.expectError(ValidationError.InvalidAlignment, validateSpirv(&unaligned, .{}));
}
test "Reject zero word count DoS payload" {
// Header valid (20 byte) + Instruksi ilegal (Word count = 0, Opcode = 1)
var payload = [_]u8{
0x03, 0x02, 0x23, 0x07, // Magic (Little Endian)
0x00, 0x00, 0x01, 0x00, // Version 1.0
0x00, 0x00, 0x00, 0x00, // Generator 0
0x10, 0x00, 0x00, 0x00, // Bound 16
0x00, 0x00, 0x00, 0x00, // Schema 0
0x01, 0x00, 0x00, 0x00, // Opcode=1, WordCount=0 (Exploit!)
};
try std.testing.expectError(ValidationError.InvalidWordCount, validateSpirv(&payload, .{}));
}
test "Reject instruction out of bounds buffer overrun" {
// Header valid (20 byte) + Instruksi mengklaim panjang 5 kata, tetapi buffer hanya punya 1 kata
var payload = [_]u8{
0x03, 0x02, 0x23, 0x07,
0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00,
0x10, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x05, 0x00, // Opcode=1, WordCount=5
};
try std.testing.expectError(ValidationError.InstructionOutOfBounds, validateSpirv(&payload, .{}));
}
test "Reject excessive Bound allocation attack" {
var payload = [_]u8{
0x03, 0x02, 0x23, 0x07,
0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0x7F, // Bound = 0x7FFFFFFF (Exceeds max limit)
0x00, 0x00, 0x00, 0x00,
};
try std.testing.expectError(ValidationError.ResourceLimitExceeded, validateSpirv(&payload, .{}));
}
Trade-offs dan Integrasi Pipeline
Catatan: Parser ini memverifikasi integritas format biner, framing instruksi, dan batas alokasi. Validasi ini tidak menggantikan full semantic validation (misalnya validasi tipe SSA, kontrol alur blok cabang, atau validasi type-check SPIR-V lengkap seperti yang disediakan oleh
spirv-val).
- Pre-flight vs Full-validation: Mengompilasi dan mengintegrasikan pustaka C/C++
spirv-val(dari SPIRV-Tools) menambah dependensi build yang berat dan overhead runtime. Pre-flight checker di Zig ini mengeksekusi pemeriksaan batas level $O(N)$ kata dalam microsecond, memfilter payload rusak sebelum menyentuh driver GPU. - Zero Allocation: Fungsi
validateSpirvtidak menggunakanstd.mem.Allocatorsama sekali, menjadikannya aman digunakan pada thread kritis atau sistem berdaya rendah.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!