🔍 CVE-2026-99170

0.0
CVSS Score
UNKNOWN
2026-10-28
2026-10-28 11:17
✅ 완료

🇰🇷 한글 설명

Linux 커널에서 잠긴(MNT_LOCKED) 마운트가 copy_tree()로 복사될 때 마운트 네임스페이스 파일의 마운트가 제외되고 잠금 검사가 누락되는 문제입니다. 이로 인해 사용자 네임스페이스의 root 권한 태스크가 MOVE_MOUNT_BENEATH와 unshare(CLONE_NEWNS)를 이용해 잠긴 마운트 커버를 제거할 수 있습니다. 커널 패치는 잠긴 마운트 복사를 거부하고 잠긴 마운트 아래로의 이동을 차단합니다.

💡 쉬운 설명

권한이 제한된 사용자가 시스템이 숨겨 둔 파일의 가림막을 우회해 원래 감춰진 내용을 읽을 수 있습니다. 커널 업데이트로 이 우회 경로가 막힙니다.

🇺🇸 원문 (English)

In the Linux kernel, the following vulnerability has been resolved: namespace: a mount that copy_tree() leaves out can't take over a lock Since commit c62a4766937e ("move_mount: transfer MNT_LOCKED") a mount can be moved beneath a locked mount. The lock moves from the old cover to the new mount which takes over the job of hiding what's under it and the old cover can be unmounted. That works as long as the new mount stays. But the caller picks the new mount and copy_tree() leaves a mount of a mount namespace file out of every copy made without CL_COPY_MNT_NS_FILE, together with everything mounted on it, and never looks at MNT_LOCKED. unshare(CLONE_NEWNS) and mount propagation make such copies. So a task that is root in its own user namespace gets rid of a locked cover on a file without a race and without touching the filesystem below it. It makes a child with a mount namespace of its own, binds the child's /proc/<pid>/ns/mnt beneath the cover with MOVE_MOUNT_BENEATH, which hands the lock to that bind, and calls unshare(CLONE_NEWNS). The new mount namespace has nothing on the file. That's the usual way to mask a single file, a bind of /dev/null or of an empty file. With a bind of an ordinary file the file stays covered. copy_tree() already refuses to leave out a locked unbindable mount with -EPERM since commit df7342b24018 ("mount: Don't allow copying MNT_UNBINDABLE|MNT_LOCKED mounts"). Do the same for a locked mount of a mount namespace file. That also covers a tree copied with CL_COPY_MNT_NS_FILE and attached into a mount namespace of another user namespace where lock_mnt_tree() locks it. Note that unshare(CLONE_NEWNS) and clone(CLONE_NEWNS) fail with EPERM from such a mount namespace from then on. The mount can't be left out and it can't be unmounted. And refuse to move a mount of a mount namespace file beneath a locked mount in the first place. Otherwise the task ends up with a mount it can't unmount and an unshare() that fails for good. Chris also found a second mount that goes away by itself, a clone of an automounted mount that shrink_submounts() took along with its parent. Commit f78ddcc87224 ("namespace: never expire a locked mount") took care of that one. Chris's reproducer is appended. Mode nsfile reads SECRET after the unshare() on 7.3-rc7, with this patch the move_mount() fails with EINVAL and the file stays covered. nsfile-control uses an ordinary file and reads MASK both ways. // SPDX-License-Identifier: GPL-2.0 /* * Get rid of a locked cover with MOVE_MOUNT_BENEATH. * * The "admin" (root) makes a tmpfs W: * W/D file, holds SECRET, covered by a bind of a file with MASK * W/Dd/inner file, holds SECRET, Dd is covered by a bind of a directory * whose "inner" holds MASK * W/own directory of uid 1000 with the file "mine" and the * directory "minedir", whose "inner" holds MINE * W/dbg debugfs, mode 755. dbg/tracing is an automount point * The "attacker" is uid 1000. It enters a new user namespace with a new mount * namespace, so W and both covers are locked there. * * usage: prog MODE * plain put a bind of own/mine beneath the cover of D, unmount the cover * nsfile put a bind of the mount namespace file of a child beneath the * cover of D, then unshare(CLONE_NEWNS) * nsfile-control the same with a bind of own/mine * shrink put a bind of the automounted tracefs beneath the cover of Dd, * unmount the cover, put a tmpfs beneath W, call umount(W), which fails * shrink-control the same with a bind of own/minedir * The environment variable AUTOMOUNT_DIR names another automount point for "shrink". */ #define _GNU_SOURCE #include <errno.h> #include <fcntl.h> #include <grp.h> #include <sched.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> ---truncated---

📍 영향받는 시스템

CVE 정보상 영향 버전이 명시되지 않음 (커널 7.3-rc7 등 최신 개발 버전에서 재현, 해당 커밋 이후 Linux 커널 배포판 전반 확인 필요)

⚔️ 공격 벡터

로컬 비특권 사용자가 사용자 네임스페이스에서 root 권한을 얻은 뒤 마운트를 조작해 잠긴 파일 마스킹을 제거하고 원본 데이터를 읽음

🛡️ 대응 방법

Linux 커널을 해당 CVE를 수정한 최신 안정 버전으로 업데이트하고 재부팅

🔧 검증 코드

출력된 커널 버전을 배포판의 보안 권고문에서 이 CVE 수정이 포함된 버전과 비교하여, 같거나 이후 버전이면 패치된 것이고 이전 버전이면 업데이트가 필요합니다.

Shell / Script
uname -r

💻 영향받는 소프트웨어

Linux Kernel