
Rex es un marco de extensiones de kernel seguro y utilizable que permite cargar y ejecutar programas de extensión de kernel en Rust en lugar de eBPF.
____ _______ __ _____ _ _
| _ \| ____\ \/ / | ____|_ _| |_ ___ _ __ ___(_) ___ _ __ ___
| |_) | _| \ / | _| \ \/ / __/ _ \ '_ \/ __| |/ _ \| '_ \/ __|
| _ <| |___ / \ | |___ > <| || __/ | | \__ \ | (_) | | | \__ \
|_| \_\_____/_/\_\ |_____/_/\_\\__\___|_| |_|___/_|\___/|_| |_|___/
Rex es un marco de extensiones de kernel seguro y utilizable que permite cargar y ejecutar programas de extensión de kernel en Rust en lugar de eBPF. A diferencia de los marcos basados en eBPF como Aya, los programas de Rex no pasan por el verificador en-kernel; en cambio, los programas se implementan en el subconjunto seguro de Rust, sobre el cual el compilador de Rust realiza las comprobaciones de seguridad necesarias y genera código nativo directamente. Este enfoque evita los requisitos de verificación excesivamente restrictivos (por ejemplo, las restricciones de complejidad del programa) y los consiguientes errores de verificación arcana, al mismo tiempo que potencialmente ofrece una mejor oportunidad de optimización en el backend del compilador nativo (es decir, LLVM) que el enfoque backend eBPF + JIT en-kernel.
Rex actualmente soporta las siguientes características:
kprobe, perf_event, tracepoint, xdp y
tc.El siguiente ejemplo implementa un programa kprobe que se adjunta a una
llamada al sistema seleccionada e inyecta un error (especificado por errno) en la
llamada al sistema en un proceso (especificado por su pid). El ejemplo completo,
incluyendo el programa cargador, se puede encontrar en
samples/error_injector.
#![no_std]
#![no_main]
use rex::kprobe::kprobe;
use rex::map::RexHashMap;
use rex::pt_regs::PtRegs;
use rex::rex_kprobe;
use rex::rex_map;
use rex::Result;
#[allow(non_upper_case_globals)]
#[rex_map]
static pid_to_errno: RexHashMap<i32, u64> = RexHashMap::new(1, 0);
#[rex_kprobe]
pub fn err_injector(obj: &kprobe, ctx: &mut PtRegs) -> Result {
obj.bpf_get_current_task()
.map(|t| t.get_pid())
.and_then(|p| obj.bpf_map_lookup_elem(&pid_to_errno, &p).cloned())
.map(|e| obj.bpf_override_return(ctx, e))
.ok_or(0)
}
Se pueden encontrar más programas de ejemplo en samples.
Puede encontrar la guía detallada aquí.
La documentación de diseño adicional se puede encontrar en docs.
La extensión eBPF existente depende del verificador eBPF en-kernel para proporcionar garantías de seguridad. Esto desafortunadamente conduce a problemas de usabilidad donde programas seguros son rechazados por el verificador, incluyendo pero no limitándose a:
Rex tiene como objetivo abordar estos problemas aprovechando directamente la garantía de seguridad de Rust seguro. Los desarrolladores pueden implementar sus programas de cualquier manera que se pueda escribir en Rust seguro con pocas restricciones, y ya no necesitan preocuparse por la complejidad del programa, el generador de código, ni encontrar la forma (muchas veces contraintuitiva) de expresar la misma lógica para complacer al verificador.
Demostramos esto con la implementación de la Caché Memcached BPF (BMC), un programa de extensión de última generación para la aceleración de Memcached. Como un programa eBPF complejo, BMC se ve obligado a dividirse en varios componentes conectados por llamadas de cola BPF y utilizar implementaciones de bucles/ramas incómodas para complacer al verificador, que no son necesarias en su implementación en Rex.
Por ejemplo, mostramos el código en la lógica de invalidación de caché de BPF-BMC
que busca un comando SET en la carga útil del paquete:
// Searches for SET command in payload
for (unsigned int off = 0;
off < BMC_MAX_PACKET_LENGTH && payload + off + 1 <= data_end;
off++) {
if (set_found == 0 && payload[off] == 's' &&
payload + off + 3 <= data_end && payload[off + 1] == 'e' &&
payload[off + 2] == 't') {
off += 3;
set_found = 1;
}
...
}
El código no solo introduce una restricción adicional en el bucle (off < BMC_MAX_PACKET_LENGTH) únicamente para pasar el verificador, sino que también emplea
código repetitivo para verificar los extremos del paquete (data_end) y una lógica
engorrosa para encontrar la cadena "set" en el paquete.
Ninguna de estas cargas es necesaria con el poder de Rust seguro en Rex, que no tiene límites de complejidad y proporciona más libertad en la implementación:
let set_iter = payload.windows(4).enumerate().filter_map(|(i, v)| {
if v == b"set " {
Some(i)
} else {
None
}
});
La implementación completa de BMC en Rex se puede encontrar en samples/bmc.
Rex está licenciado bajo la licencia GPLv2. Los submódulos (Linux, Rust, LLVM) en este repositorio están licenciados bajo sus propios términos. Consulte los archivos de licencia correspondientes para más detalles. Adicionalmente, el benchmark de memcached está licenciado bajo la licencia MIT.