CVE-2026-98981 in Linux
Resumen
por VulDB • 2026-10-24
En el kernel de Linux, se ha resuelto la siguiente vulnerabilidad:
namespace: desvincular (unhash) un dentry antes de separar los puntos de montaje sobre él.
unlink(), rmdir() y rename() eliminan la entrada del sistema de archivos, llaman a detach_mounts() en la entrada con el inode bloqueado y luego llaman a d_delete() una vez que el inode se desbloquea.
El problema es que entre detach_mounts() y d_delete(), el dentry sigue estando vinculado (hashed) y positivo. Pero después de detach_mounts() nada cubre ya el dentry. Una búsqueda que encuentre este dentry en la caché de directorios (dcache) puede descubrir lo que los puntos de montaje ocultaban.
Esto es un problema al eliminar archivos o directorios que son puntos de montaje en otros espacios de nombres de montaje. Cualquier proceso que tuviera cubierta la entrada subyacente puede entrar en una condición de carrera con la llamada detach_mount() hasta que se haya ejecutado d_delete().
La ventana de carrera no es pequeña. Abarca namespace_unlock() con un período de gracia full synchronize_rcu_expedited() y el inode_unlock() de la entrada.
En mis experimentos, tres procesos caminadores (walkers) que intentaban continuamente leer un archivo sobre-montado lograron hacerlo en 11938 de los 16140 unlinkings que eliminaron su punto de montaje de una unión montada (bind mount) del sistema de archivos dentro de un minuto.
Dato curioso, d_invalidate() tiene el mismo problema de ordenamiento pero lo resuelve correctamente. Desvincula primero el dentry y separa los puntos de montaje después. Hagamos lo mismo en __detach_mounts():
- Una búsqueda que aún no ha encontrado el dentry la perderá en la caché de directorios (dcache) y esperará al bloqueo del directorio que mantiene el llamador hasta que el nombre haya desaparecido por completo.
- Una búsqueda sin bloqueo (lockless lookup) que ya lo encontró fallará en la comprobación mount_lock mientras el dentry sigue contando como un punto de montaje, y también fallará en la comprobación d_seq una vez que deje de serlo, repitiendo así la operación.
Esto corrige la ruta sin bloqueo. Todavía necesitamos corregir la búsqueda del conteo de referencias en un parche posterior.
Por lo tanto, se aplica d_drop() al dentry. El dentry permanece positivo y retenido pero ya no puede encontrarse. Luego se procede con la separación (detach) y el unlinking.
Reproductor:
Las lecturas fueron contadas con el siguiente programa, tres procesos caminadores durante 60 segundos en una máquina virtual con 4 CPUs. La ventana de carrera se extiende mediante el parche de depuración pegado aquí. fs.detach_race_walk_us hace sleep en lookup_fast() una vez que ha encontrado un dentry de punto de montaje y antes de cruzar sus puntos de montaje. fs.detach_race_unlink_us hace sleep en vfs_unlink() entre detach_mounts() y d_delete().
// SPDX-License-Identifier: GPL-2.0 /* * unlink_covered: eliminar (unlink) un archivo que es un punto de montaje en una copia desvinculada de su montaje, contra búsquedas que lo abren a través de esa copia. * * A es un tmpfs con el archivo f1 (contenido SECRET_F) y el archivo plano MARK_A. A' es una copia open_tree(OPEN_TREE_CLONE) de A con MARK_A vinculado en A'/f1, mantenido mediante un fd O_PATH sobre su raíz una vez que se cierra el fd del árbol. Los caminadores abren f1 a través de ese fd mientras el controlador elimina A/f1, donde nada está montado sobre él. Un caminador puede leer MARK_A o obtener ENOENT. Una lectura de SECRET_F es un acierto: el nombre fue encontrado después de que su montaje hubiera desaparecido. Las búsquedas sin bloqueo utilizan openat2(RESOLVE_CACHED). * * uso: unlink_covered [-t segundos] [-w caminadores]
*/ #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include <errno.h> #include <fcntl.h> #include <pthread.h> #include <stdatomic.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/mount.h> #include <sys/stat.h> #include <sys/syscall.h> #include <linux/openat2.h>
#ifndef __NR_open_tree #define __NR_open_tree 428 #endif #ifndef __NR_move_mount #define __NR_move_mount 429 #endif #ifndef __NR_openat2 #define __NR_openat2 437 #endif #ifndef OPEN_TREE_CLONE #define OPEN_TREE_CLONE 1 #endif #ifndef OPEN_TREE_CLOEXEC #define OPEN_TREE_CLOEXEC O_CLOEXEC #endif #ifndef MOVE_MOUNT_F_EMPTY_PATH #define MOVE_MOUNT_F_EMPTY_PATH 0x00000004 #endif
#define WORK "/tmp/uc" #define ADIR WORK "/A"
static int duration = 60, nwalkers = 3; static atomic_int stop, writer_waiting; static pthread_rwlock_t cur_lock = PTHREAD_RWLOCK_INITIALIZER; static int cur_fd = -1; /* la raíz de A', -1 wh
If you want to get the best quality for vulnerability data then you always have to consider VulDB.