{"id":"CVE-2026-98981","kind":"vulnerability","aliases":["CVE-2026-98981"],"related":[],"title":"Linux kernel: unlinked mountpoint dentry stays findable, letting lookups read covered files","description":"In the Linux kernel, the following vulnerability has been resolved:\n\nnamespace: unhash a dentry before detaching the mounts on it\n\nunlink(), rmdir() and rename() remove the entry from the filesystem,\ncall detach_mounts() on the entry with the inode locked and then call\nd_delete() once the inode is unlocked.\n\nThe thing is that between detach_mounts() and d_delete() the dentry is\nstill hashed and positive. But after detach_mounts() nothing covers the\ndentry anymore. A lookup that finds this dentry in the dcache can\nuncover what the mounts hid.\n\nThat's a problem when unlinking files or directories that are\nmountpoints in other mount namespaces. Everybody who had the underlying\nentry covered can race the detach_mount() call until d_delete() has run.\n\nThe race window isn't all that small. It encompasses namespace_unlock()\nwith a full synchronize_rcu_expedited() grace period and the\ninode_unlock() of the entry.\n\nIn my experiments three walkers that kept trying read an overmounted\nfile in 11938 of the 16140 unlinks that removed its mountpoint from a\nbind mount of the filesystem within a minute.\n\nFun fact, d_invalidate() has the same ordering problem but gets it\nright. It unhashes the dentry first and detaches the mounts afterwards.\nLet's do the same in __detach_mounts():\n\n- A lookup that hasn't found the dentry yet misses it in the dcache and\n  waits for the directory lock that the caller holds until the name is\n  gone for good.\n\n- A lockless lookup that found it already fails the mount_lock check\n  while the dentry still counts as a mountpoint and the d_seq check once\n  it doesn't, and retries.\n\nThis fixes the lockless path. We still need to fix the reference count\nlookup in a follow-up patch.\n\nSo d_drop() the dentry. The dentry stays positive and held but can't be\nfound anymore. Then proceed with the detach and unlink.\n\nReproducer:\n\nThe reads were counted with the program below, three walkers for 60 s\nin a VM with 4 CPUs. The race window is stretched with the debug patch\npasted here. fs.detach_race_walk_us sleeps in lookup_fast() once it\nfound a mountpoint dentry and before its mounts are crossed.\nfs.detach_race_unlink_us sleeps in vfs_unlink() between detach_mounts()\nand d_delete().\n\n  // SPDX-License-Identifier: GPL-2.0\n  /*\n   * unlink_covered: unlink a file that is a mountpoint in a detached copy of\n   * its mount, against lookups that open it through that copy.\n   *\n   * A is a tmpfs with the file f1 (content SECRET_F) and the plain file\n   * MARK_A. A' is an open_tree(OPEN_TREE_CLONE) copy of A with MARK_A bound\n   * on A'/f1, held through an O_PATH fd on its root once the tree fd is\n   * closed. Walkers open f1 through that fd while the driver unlinks A/f1,\n   * where nothing is mounted on it. A walker may read MARK_A or get ENOENT.\n   * A read of SECRET_F is a hit: the name was found after its mount was\n   * gone. The lockless walks use openat2(RESOLVE_CACHED).\n   *\n   * usage: unlink_covered [-t seconds] [-w walkers]\n   */\n  #ifndef _GNU_SOURCE\n  #define _GNU_SOURCE\n  #endif\n  #include <errno.h>\n  #include <fcntl.h>\n  #include <pthread.h>\n  #include <stdatomic.h>\n  #include <stdio.h>\n  #include <stdlib.h>\n  #include <string.h>\n  #include <unistd.h>\n  #include <sys/mount.h>\n  #include <sys/stat.h>\n  #include <sys/syscall.h>\n  #include <linux/openat2.h>\n\n  #ifndef __NR_open_tree\n  #define __NR_open_tree 428\n  #endif\n  #ifndef __NR_move_mount\n  #define __NR_move_mount 429\n  #endif\n  #ifndef __NR_openat2\n  #define __NR_openat2 437\n  #endif\n  #ifndef OPEN_TREE_CLONE\n  #define OPEN_TREE_CLONE 1\n  #endif\n  #ifndef OPEN_TREE_CLOEXEC\n  #define OPEN_TREE_CLOEXEC O_CLOEXEC\n  #endif\n  #ifndef MOVE_MOUNT_F_EMPTY_PATH\n  #define MOVE_MOUNT_F_EMPTY_PATH 0x00000004\n  #endif\n\n  #define WORK \"/tmp/uc\"\n  #define ADIR WORK \"/A\"\n\n  static int duration = 60, nwalkers = 3;\n  static atomic_int stop, writer_waiting;\n  static pthread_rwlock_t cur_lock = PTHREAD_RWLOCK_INITIALIZER;\n  static int cur_fd = -1;\t\t/* the root of A', -1 wh\n---truncated---","summary":"In the Linux kernel, unlink, rmdir and rename leave a mountpoint dentry hashed after its mounts are detached, so a concurrent lookup can find it and read the file that the mount had hidden. The fix unhashes the dentry before detaching mounts, as d_invalidate() already does.","severity":"unknown","cvss":null,"epss":null,"exploited":false,"exploited_at":null,"products":[],"ecosystems":[],"tags":["bind-mounts","containers","linux-kernel","runtime"],"not_affected_if":["default-config","no-shared-mount-namespaces"],"how_to_check":"- Run `uname -r` and compare against the kernel release that contains the upstream fix for CVE-2026-98981\n- Check whether the kernel build includes the commit 'namespace: unhash a dentry before detaching the mounts on it'\n- Check whether bind mounts or mount namespaces are used on multi-tenant hosts (`findmnt`, `lsns -t mnt`)","fix":"- Update the Linux kernel to a release that includes the upstream fix for CVE-2026-98981 (version unknown from sources)\n- Reboot after the kernel update so the fix is active","status":"open","published_at":"2026-10-23T13:17:19+00:00","first_seen_at":"2026-10-23T21:57:33+00:00","triaged_at":"2026-10-25T04:56:17+00:00","updated_at":"2026-10-25T04:56:17+00:00","category":"runtime","confidence":0.5,"triage":{"at":"2026-10-25T04:56:17Z","model":"claude-haiku-5-5","output":{"fix":"- Update the Linux kernel to a release that includes the upstream fix for CVE-2026-98981 (version unknown from sources)\n- Reboot after the kernel update so the fix is active","reach":"local","title":"Linux kernel: unlinked mountpoint dentry stays findable, letting lookups read covered files","impact":"low","summary":"In the Linux kernel, unlink, rmdir and rename leave a mountpoint dentry hashed after its mounts are detached, so a concurrent lookup can find it and read the file that the mount had hidden. The fix unhashes the dentry before detaching mounts, as d_invalidate() already does.","category":"runtime","nicknames":[],"who_cares":["linux-kernel","containers","bind-mounts"],"confidence":0.5,"needs_auth":"unknown","worst_case":"local attacker reads file contents hidden by a mount","analyst_note":"Confirm the fixed kernel versions and the upstream commit from the NVD entry at https://nvd.nist.gov/vuln/detail/CVE-2026-98981 and the linked kernel.org commit.","how_to_check":"- Run `uname -r` and compare against the kernel release that contains the upstream fix for CVE-2026-98981\n- Check whether the kernel build includes the commit 'namespace: unhash a dentry before detaching the mounts on it'\n- Check whether bind mounts or mount namespaces are used on multi-tenant hosts (`findmnt`, `lsns -t mnt`)","needs_analyst":true,"not_affected_if":["default-config","no-shared-mount-namespaces"],"who_is_affected":"Systems where unlink, rmdir or rename can remove a file or directory that is a mountpoint in another mount namespace, such as container hosts or systems using bind mounts and detached mount copies. Single-user machines without shared or cloned mount namespaces are unlikely to be exposed.","user_interaction":"no","needs_special_setup":"unknown","severity_if_unknown":"unknown"},"cost_usd":0.000839,"input_tokens":4928,"output_tokens":692,"severity_before":"unknown"},"analyst_status":null,"analyst_note":null,"search":"'-1':714C,719C '-2.0':464C '-2026':14A,17A '-98981':15A,18A '/a':688C '/f1':523C '/tmp/uc':684C '0x00000004':680C '1':655C '11938':238C '16140':241C '3':694C '4':412C '428':627C '429':637C '437':645C '60':406C,692C 'a/f1':549C 'adir':686C 'afterward':279C 'alreadi':72B,325C 'anymor':148C,386C 'atom':696C 'bind':20B,249C 'bind-mount':19B 'bound':520C 'cach':588C 'call':103C,115C,192C 'caller':310C 'check':330C,343C 'cloexec':660C,664C,666C 'clone':513C,650C,654C 'close':538C 'concurr':47B 'contain':22B 'content':497C 'copi':477C,488C,514C 'count':335C,362C,398C 'cover':11A,145C,186C,466C,591C 'cpus':413C 'cross':444C 'cur':705C,712C 'cve':13A,16A 'd':70B,116C,131C,194C,259C,341C,371C,457C 'dcach':157C,301C 'debug':421C 'defin':599C,623C,633C,642C,651C,661C,674C,682C,685C 'delet':117C,132C,195C,458C 'dentri':5A,38B,65B,86C,134C,147C,154C,273C,295C,333C,374C,376C,438C 'detach':44B,67B,88C,104C,128C,142C,190C,276C,286C,391C,454C,476C 'directori':172C,306C 'doesn':346C 'driver':547C 'drop':372C 'durat':691C 'empti':672C,678C 'encompass':207C 'endif':602C,628C,638C,646C,656C,667C,681C 'enoent':564C 'entri':99C,108C,185C,224C 'everybodi':180C 'expedit':215C 'experi':227C 'f':499C,569C,671C,677C 'f1':496C,541C 'fact':258C 'fail':326C 'fast':432C 'fd':529C,536C,544C,713C 'file':12A,55B,170C,236C,469C,495C,503C 'filesystem':102C,253C 'find':50B,152C 'findabl':7A 'first':274C 'fix':62B,351C,359C 'follow':78C,367C 'follow-up':366C 'found':293C,323C,385C,435C,576C 'fs.detach':425C,445C 'full':212C 'fun':257C 'get':267C,563C 'gnu':597C,600C 'gone':316C,581C 'good':318C 'gpl':463C 'grace':216C 'hash':39B,137C 'hasn':291C 'held':380C,524C 'hid':163C 'hidden':60B 'hit':572C 'hold':311C 'identifi':462C 'ifndef':596C,619C,629C,639C,647C,657C,668C 'includ':603C,604C,605C,606C,607C,608C,609C,610C,611C,613C,615C,617C 'initi':709C 'inod':111C,120C,220C 'int':690C,697C,711C 'invalid':71B,260C 'isn':201C 'kept':231C 'kernel':2A,25B,30B,76C 'leav':35B 'let':8A,280C 'licens':461C 'linux':1A,24B,29B,75C 'linux-kernel':23B 'linux/openat2.h':618C 'lock':112C,307C,329C,706C 'lockless':320C,353C,583C 'lookup':9A,48B,150C,289C,321C,363C,431C,482C 'mark':504C,518C,560C 'may':558C 'minut':256C 'miss':297C 'mount':21B,42B,58B,68B,90C,105C,129C,143C,162C,178C,191C,250C,278C,287C,328C,442C,455C,480C,553C,579C,632C,636C,670C,676C 'mountpoint':4A,37B,175C,246C,338C,437C,473C 'move':631C,635C,669C,675C 'name':314C,574C 'namespac':83C,179C,208C 'need':357C 'noth':144C,551C 'nr':620C,624C,630C,634C,640C,643C 'nwalker':693C 'o':527C,665C 'open':484C,509C,511C,540C,621C,625C,648C,652C,658C,662C 'openat2':586C,641C,644C 'order':264C 'overmount':235C 'past':423C 'patch':369C,422C 'path':354C,528C,673C,679C 'period':217C 'plain':502C 'posit':139C,378C 'problem':167C,265C 'proceed':388C 'program':401C 'pthread':702C,707C 'race':188C,199C,415C,426C,446C 'rcu':214C 'read':10A,53B,233C,396C,559C,566C 'refer':361C 'remov':97C,244C 'renam':34B,96C 'reproduc':394C 'resolv':82C,587C 'retri':349C 'right':269C 'rmdir':32B,94C 'root':532C,716C 'run':197C 'runtim':26B 'rwlock':703C,708C 'second':593C 'secret':498C,568C 'seq':342C 'sleep':429C,449C 'small':205C 'sourc':598C,601C 'spdx':460C 'spdx-license-identifi':459C 'static':689C,695C,701C,710C 'stay':6A,377C 'still':136C,334C,356C 'stop':698C 'stretch':418C 'synchron':213C 'sys/mount.h':612C 'sys/stat.h':614C 'sys/syscall.h':616C 'thing':124C 'three':228C,403C 'tmpfs':492C 'tree':510C,512C,535C,622C,626C,649C,653C,659C,663C 'tri':232C 'truncat':721C 'uncov':159C 'under':184C 'unhash':63B,84C,271C 'unlink':3A,31B,93C,169C,242C,393C,447C,452C,465C,467C,548C,590C 'unlock':122C,209C,221C 'us':428C,448C 'usag':589C 'use':585C 'vfs':451C 'vm':410C 'vulner':79C 'w':594C 'wait':303C,700C 'walk':427C,584C 'walker':229C,404C,539C,557C,595C 'wh':720C 'window':200C,416C 'within':254C 'work':683C,687C 'writer':699C 'yet':296C","nicknames":[],"mitigations":[],"who_affected":"Systems where unlink, rmdir or rename can remove a file or directory that is a mountpoint in another mount namespace, such as container hosts or systems using bind mounts and detached mount copies. Single-user machines without shared or cloned mount namespaces are unlikely to be exposed.","tier":"C","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-23T13:17:19+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"nvd","external_id":"CVE-2026-98981","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-98981","severity":null,"published_at":"2026-10-23T13:17:19+00:00","modified_at":"2026-10-23T13:17:19+00:00","first_seen_at":"2026-10-23T21:57:33+00:00","fetched_at":"2026-10-23T22:29:03+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}