VYPR
Unrated severityNVD Advisory· Published May 30, 2026

CVE-2026-46242

CVE-2026-46242

Description

In the Linux kernel, the following vulnerability has been resolved:

eventpoll: fix ep_remove struct eventpoll / struct file UAF

ep_remove() (via ep_remove_file()) cleared file->f_ep under file->f_lock but then kept using @file inside the critical section (is_file_epoll(), hlist_del_rcu() through the head, spin_unlock). A concurrent __fput() taking the eventpoll_release() fastpath in that window observed the transient NULL, skipped eventpoll_release_file() and ran to f_op->release / file_free().

For the epoll-watches-epoll case, f_op->release is ep_eventpoll_release() -> ep_clear_and_put() -> ep_free(), which kfree()s the watched struct eventpoll. Its embedded ->refs hlist_head is exactly where epi->fllink.pprev points, so the subsequent hlist_del_rcu()'s "*pprev = next" scribbles into freed kmalloc-192 memory.

In addition, struct file is SLAB_TYPESAFE_BY_RCU, so the slot backing @file could be recycled by alloc_empty_file() -- reinitializing f_lock and f_ep -- while ep_remove() is still nominally inside that lock. The upshot is an attacker-controllable kmem_cache_free() against the wrong slab cache.

Pin @file via epi_fget() at the top of ep_remove() and gate the critical section on the pin succeeding. With the pin held @file cannot reach refcount zero, which holds __fput() off and transitively keeps the watched struct eventpoll alive across the hlist_del_rcu() and the f_lock use, closing both UAFs.

If the pin fails @file has already reached refcount zero and its __fput() is in flight. Because we bailed before clearing f_ep, that path takes the eventpoll_release() slow path into eventpoll_release_file() and blocks on ep->mtx until the waiter side's ep_clear_and_put() drops it. The bailed epi's share of ep->refcount stays intact, so the trailing ep_refcount_dec_and_test() in ep_clear_and_put() cannot free the eventpoll out from under eventpoll_release_file(); the orphaned epi is then cleaned up there.

A successful pin also proves we are not racing eventpoll_release_file() on this epi, so drop the now-redundant re-check of epi->dying under f_lock. The cheap lockless READ_ONCE(epi->dying) fast-path bailout stays.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A use-after-free in Linux kernel's eventpoll subsystem allows local privilege escalation via concurrent ep_remove and __fput operations.

Vulnerability

The vulnerability is a use-after-free (UAF) in the Linux kernel's eventpoll subsystem, specifically in the ep_remove() function (called via ep_remove_file()). The bug occurs when ep_remove() clears file->f_ep under file->f_lock but continues to use the struct file pointer inside the critical section. A concurrent __fput() taking the eventpoll_release() fastpath can observe the transient NULL, skip eventpoll_release_file(), and proceed to f_op->release / file_free(). For the epoll-watches-epoll case, this leads to freeing the watched struct eventpoll while ep_remove() still holds a reference to its hlist_head. Additionally, struct file is SLAB_TYPESAFE_BY_RCU, so the memory slot could be recycled while ep_remove() is still inside the lock. The vulnerability affects Linux kernel versions prior to the fix commits referenced in [1], [2], [3].

Exploitation

An attacker needs local access to the system and the ability to create and manipulate epoll instances. The race window is between ep_remove() clearing file->f_ep and the subsequent hlist_del_rcu() and spin_unlock. A concurrent __fput() on the same file can trigger the fastpath, leading to the UAF. The attacker must trigger the race condition, which requires precise timing. No special privileges beyond the ability to use epoll are required.

Impact

Successful exploitation results in a use-after-free of both a struct file and a struct eventpoll. This can lead to an attacker-controllable kmem_cache_free() against the wrong slab cache, potentially allowing local privilege escalation or denial of service. The attacker may gain arbitrary write or read of kernel memory, leading to full compromise of the system.

Mitigation

The fix is to pin @file via epi_fget() at the top of ep_remove() and gate the critical section on the pin succeeding. This prevents the race by ensuring the file cannot reach refcount zero during the operation. The fix has been applied to the Linux kernel stable trees as commits [1], [2], [3]. Users should update to the latest stable kernel version that includes these commits. No workaround is available; updating is the only mitigation.

AI Insight generated on May 30, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

3
ef4ca02e9536

eventpoll: fix ep_remove struct eventpoll / struct file UAF

1 file changed · +10 7
  • fs/eventpoll.c+10 7 modified
    diff --git a/fs/eventpoll.c b/fs/eventpoll.c
    index df6994943e59ff..e9e6938f7184ae 100644
    --- a/fs/eventpoll.c
    +++ b/fs/eventpoll.c
    @@ -912,22 +912,26 @@ static bool ep_remove_epi(struct eventpoll *ep, struct epitem *epi)
      */
     static void ep_remove_safe(struct eventpoll *ep, struct epitem *epi)
     {
    -	struct file *file = epi->ffd.file;
    +	struct file *file __free(fput) = NULL;
     
     	lockdep_assert_irqs_enabled();
     	lockdep_assert_held(&ep->mtx);
     
     	ep_unregister_pollwait(ep, epi);
     
    -	/* sync with eventpoll_release_file() */
    +	/* cheap sync with eventpoll_release_file() */
     	if (unlikely(READ_ONCE(epi->dying)))
     		return;
     
    -	spin_lock(&file->f_lock);
    -	if (epi->dying) {
    -		spin_unlock(&file->f_lock);
    +	/*
    +	 * If we manage to grab a reference it means we're not in
    +	 * eventpoll_release_file() and aren't going to be.
    +	 */
    +	file = epi_fget(epi);
    +	if (!file)
     		return;
    -	}
    +
    +	spin_lock(&file->f_lock);
     	ep_remove_file(ep, epi, file);
     
     	if (ep_remove_epi(ep, epi))
    -- 
    cgit 1.3-korg
    
    
    
ced39b6a8062

eventpoll: fix ep_remove struct eventpoll / struct file UAF

1 file changed · +10 7
  • fs/eventpoll.c+10 7 modified
    diff --git a/fs/eventpoll.c b/fs/eventpoll.c
    index 4971074ab476a5..8c03de028c4824 100644
    --- a/fs/eventpoll.c
    +++ b/fs/eventpoll.c
    @@ -912,22 +912,26 @@ static bool ep_remove_epi(struct eventpoll *ep, struct epitem *epi)
      */
     static void ep_remove_safe(struct eventpoll *ep, struct epitem *epi)
     {
    -	struct file *file = epi->ffd.file;
    +	struct file *file __free(fput) = NULL;
     
     	lockdep_assert_irqs_enabled();
     	lockdep_assert_held(&ep->mtx);
     
     	ep_unregister_pollwait(ep, epi);
     
    -	/* sync with eventpoll_release_file() */
    +	/* cheap sync with eventpoll_release_file() */
     	if (unlikely(READ_ONCE(epi->dying)))
     		return;
     
    -	spin_lock(&file->f_lock);
    -	if (epi->dying) {
    -		spin_unlock(&file->f_lock);
    +	/*
    +	 * If we manage to grab a reference it means we're not in
    +	 * eventpoll_release_file() and aren't going to be.
    +	 */
    +	file = epi_fget(epi);
    +	if (!file)
     		return;
    -	}
    +
    +	spin_lock(&file->f_lock);
     	ep_remove_file(ep, epi, file);
     
     	if (ep_remove_epi(ep, epi))
    -- 
    cgit 1.3-korg
    
    
    
a6dc643c6931

eventpoll: fix ep_remove struct eventpoll / struct file UAF

1 file changed · +10 7
  • fs/eventpoll.c+10 7 modified
    diff --git a/fs/eventpoll.c b/fs/eventpoll.c
    index 5ee4398a6cb84f..0f785c0a1544f3 100644
    --- a/fs/eventpoll.c
    +++ b/fs/eventpoll.c
    @@ -912,22 +912,26 @@ static bool ep_remove_epi(struct eventpoll *ep, struct epitem *epi)
      */
     static void ep_remove(struct eventpoll *ep, struct epitem *epi)
     {
    -	struct file *file = epi->ffd.file;
    +	struct file *file __free(fput) = NULL;
     
     	lockdep_assert_irqs_enabled();
     	lockdep_assert_held(&ep->mtx);
     
     	ep_unregister_pollwait(ep, epi);
     
    -	/* sync with eventpoll_release_file() */
    +	/* cheap sync with eventpoll_release_file() */
     	if (unlikely(READ_ONCE(epi->dying)))
     		return;
     
    -	spin_lock(&file->f_lock);
    -	if (epi->dying) {
    -		spin_unlock(&file->f_lock);
    +	/*
    +	 * If we manage to grab a reference it means we're not in
    +	 * eventpoll_release_file() and aren't going to be.
    +	 */
    +	file = epi_fget(epi);
    +	if (!file)
     		return;
    -	}
    +
    +	spin_lock(&file->f_lock);
     	ep_remove_file(ep, epi, file);
     
     	if (ep_remove_epi(ep, epi))
    -- 
    cgit 1.3-korg
    
    
    

Vulnerability mechanics

Root cause

"Missing file reference pinning in ep_remove() allows a concurrent __fput() to free the struct file and struct eventpoll while they are still in use."

Attack vector

An attacker can trigger a use-after-free by exploiting a race condition in the epoll subsystem. When `ep_remove()` clears `file->f_ep` under `file->f_lock`, a concurrent `__fput()` on the same file may observe the transient NULL and take the `eventpoll_release()` fastpath, skipping `eventpoll_release_file()`. This allows `f_op->release` to run, which for epoll-watches-epoll calls `ep_free()` and kfree()s the watched `struct eventpoll`. The subsequent `hlist_del_rcu()` then writes into freed kmalloc-192 memory. Additionally, because `struct file` is `SLAB_TYPESAFE_BY_RCU`, the slab slot may be recycled and reinitialized while `ep_remove()` still holds the lock, leading to an attacker-controllable `kmem_cache_free()` against the wrong slab cache.

Affected code

The vulnerability is in `fs/eventpoll.c` in the `ep_remove()` (or `ep_remove_safe()`) function. The function cleared `file->f_ep` under `file->f_lock` but continued to use `@file` inside the critical section, allowing a concurrent `__fput()` to observe a transient NULL and skip `eventpoll_release_file()`, leading to a use-after-free of both the `struct file` and the watched `struct eventpoll`.

What the fix does

The patch pins `@file` via `epi_fget()` at the top of `ep_remove()` and gates the critical section on the pin succeeding. With the pin held, `@file` cannot reach refcount zero, which holds `__fput()` off and transitively keeps the watched `struct eventpoll` alive across the `hlist_del_rcu()` and the `f_lock` use, closing both UAFs. If the pin fails, the function bails before clearing `f_ep`, so the concurrent `__fput()` path takes the slow path into `eventpoll_release_file()` and blocks on `ep->mtx` until the waiter side's `ep_clear_and_put()` drops it. The redundant re-check of `epi->dying` under `f_lock` is removed since a successful pin proves no race with `eventpoll_release_file()`.

Preconditions

  • configThe attacker must be able to create an epoll instance that watches another epoll instance (epoll-watches-epoll scenario).
  • inputThe attacker must be able to trigger a race between `ep_remove()` and a concurrent `__fput()` on the watched file.
  • authThe attacker must have the ability to create and close file descriptors to induce the race window.

Generated on May 30, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.