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:
Sergio
2026-09-01 16:59:22 +00:00
co-authored by Claude Opus 5
parent 1df77985fa
commit 9591e69b70
2 changed files with 24 additions and 11 deletions
+16 -9
View File
@@ -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
+8 -2
View File
@@ -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++)