harkaq: el salto del check de arquitectura ya no se apoya en UB (SDD 25 T17)
`f[k++] = BPF_JUMP(..., I_KILL - k - 1 + 1)` leía y modificaba `k` en la misma expresión, sin punto de secuencia: UB en C11. Andaba porque gcc y clang leen `k` después del incremento y ese `+1` era la compensación exacta, pero si algún compilador leyera antes el salto caería una instrucción más allá del final. El índice se fija ahora en `I_ARCH` antes de usarlo. El código generado es IDÉNTICO —mismo objdump del objeto a -O1 y a -O2, la única línea que difiere es el nombre del fichero—, que es lo que prueba que es la misma cuenta escrita sin UB y no un arreglo que además mueve el salto. Comprobado además que la jaula sigue puesta: ptrace desde dentro da EPERM y un ejecutable fuera de la clausura no arranca. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
This commit is contained in:
@@ -325,12 +325,17 @@ por `fork`/`exec`, así que lo paga el primer proceso de la fase y lo cobran los
|
||||
mide N+3, así que la denylist tiene un **techo de ~252 entradas**: pasado eso el desplazamiento
|
||||
da la vuelta y el filtro deja de decir lo que dice su código fuente. Hoy son 25 y no hay
|
||||
problema; el problema es que crecer la lista es justo lo que uno hace sin pensarlo.
|
||||
2. **`f[k++] = BPF_JUMP(..., I_KILL - k - 1 + 1)` modifica y lee `k` en la misma expresión** — sin
|
||||
punto de secuencia, o sea UB en C11. Funciona porque gcc y clang leen `k` *después* del
|
||||
incremento, y el `+1` final es exactamente la compensación de eso (comprobado con los dos, `-O0`
|
||||
y `-O2`, en esta máquina). Si algún compilador leyera antes, el salto caería una instrucción
|
||||
más allá del final: el verificador clásico lo rechazaría y harkaq saldría por su rama de
|
||||
`RECHAZO`. Falla ruidoso, no silencioso — pero está apoyado en UB.
|
||||
2. **`f[k++] = BPF_JUMP(..., I_KILL - k - 1 + 1)` modificaba y leía `k` en la misma expresión** —
|
||||
sin punto de secuencia, o sea UB en C11. Funcionaba porque gcc y clang leen `k` *después* del
|
||||
incremento, y el `+1` final era exactamente la compensación de eso. Si algún compilador leyera
|
||||
antes, el salto caería una instrucción más allá del final: el verificador clásico lo rechazaría
|
||||
y harkaq saldría por su rama de `RECHAZO` — falla ruidoso, no silencioso, pero apoyado en UB.
|
||||
|
||||
**CERRADO el 2026-09-01**: el índice se fija en `I_ARCH` antes de usarlo. **El código generado es
|
||||
idéntico** —mismo `objdump` del objeto a `-O1` y a `-O2`, la única línea que difiere es el nombre
|
||||
del fichero— y el binario parcheado sigue poniendo la jaula: `ptrace` desde dentro da `EPERM` y
|
||||
un ejecutable fuera de la clausura no arranca. Que el desensamblado no cambie es lo que prueba
|
||||
que era exactamente la misma cuenta escrita sin UB, y no un arreglo que además mueve el salto.
|
||||
|
||||
### T18 · Landlock: el precio es ESTAR enjaulado, no cuántas reglas hay
|
||||
|
||||
@@ -828,9 +833,11 @@ Su precio, concreto:
|
||||
algún día hace falta (filtrar `ioctl` por petición, `clone` por flags), el precio es la cadena
|
||||
entera en cada llamada de esa syscall — 0,158 ns por instrucción, medido. Se decide con ese
|
||||
número delante, no con la intuición de que «un filtro es un filtro».
|
||||
3. **La denylist tiene un techo de ~252 entradas** por el `__u8` de los saltos de la BPF clásica,
|
||||
y `poner_seccomp()` no lo comprueba. Hoy son 25. Un `_Static_assert` cuesta nada y evita que
|
||||
crecer la lista rompa el filtro en silencio — la misma forma de fallo que CLAUDE.md §3.
|
||||
3. **La denylist tiene un techo de ~252 entradas** por el `__u8` de los saltos de la BPF clásica.
|
||||
Hoy son 25. **HECHO**: un `_Static_assert` en `harkaq-exec.c` lo cierra, para que crecer la
|
||||
lista no rompa el filtro en silencio — la misma forma de fallo que CLAUDE.md §3. Y **el UB del
|
||||
punto (2) de T17 también está cerrado**, con el desensamblado como prueba de que no cambia
|
||||
nada más.
|
||||
|
||||
Y la política **fichero a fichero** de D1 queda confirmada por el lado del coste: cambiarla por
|
||||
reglas de directorio ahorraría 225 ns por `open` de los 698, o sea el 32% de lo barato, a cambio
|
||||
|
||||
@@ -156,8 +156,14 @@ static int poner_seccomp(void) {
|
||||
// el error clásico de los filtros seccomp escritos a mano.
|
||||
f[k++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
|
||||
offsetof(struct seccomp_data, arch));
|
||||
f[k++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0,
|
||||
I_KILL - k - 1 + 1);
|
||||
// El desplazamiento se cuenta desde ESTA instrucción, así que hace falta su índice. Fijarlo en
|
||||
// una variable y no escribir `f[k++] = ... (I_KILL - k - 1 + 1)`: eso lee y modifica `k` en la
|
||||
// misma expresión, sin punto de secuencia — UB en C11. Andaba (gcc y clang leen `k` después del
|
||||
// incremento, y ese `+1` era justo la compensación), pero andaba apoyado en UB. Comprobado que
|
||||
// el código generado no cambia, a `-O1` y `-O2`. Ver SDD 25 T17.
|
||||
const int I_ARCH = k++;
|
||||
f[I_ARCH] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0,
|
||||
I_KILL - I_ARCH - 1);
|
||||
f[k++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
|
||||
offsetof(struct seccomp_data, nr));
|
||||
for (int i = 0; i < n; i++, k++)
|
||||
|
||||
Reference in New Issue
Block a user