All of lore.kernel.org
 help / color / mirror / Atom feed
* Re: erofs: use of uninitialized variable in erofs_real_inode()
       [not found] <CAM_WGAzh1WWJgdK=NV+MR9bU8Bn8Pe14QJW73y=XUjGt032ybA@mail.gmail.com>
@ 2026-10-24 15:44 ` Gao Xiang
  0 siblings, 0 replies; only message in thread
From: Gao Xiang @ 2026-10-24 15:44 UTC (permalink / raw)
  To: YoungWoo Rim; +Cc: security, xiang, linux-erofs

[-- Attachment #1: Type: text/plain, Size: 10378 bytes --]

Hi,

(Cc linux-erofs@lists.ozlabs.org to record this.)

Sorry, I checked your reproducer (I reattach this in this reply):
you're deliberately set in-memory inodes as I_FREEING with your
crafted kernel module (your reproducer is actually a crafted
kernel module instead of a userspace workload, not sure if it's
LLM-generated):

 ...

                        /* Set I_FREEING on all real inodes */
                        spin_lock(&vis->ishare_lock);
                        list_for_each_entry(real_iter, &vis->ishare_list, ishare_list) {
                                spin_lock(&real_iter->vfs_inode.i_lock);
                                inode_state_set(&real_iter->vfs_inode, I_FREEING);
                                spin_unlock(&real_iter->vfs_inode.i_lock);
                        }
                        spin_unlock(&vis->ishare_lock);
 ...

That violates the following Linux VFS constraints:

 - I_FREEING should be **only** set after i_count == 0:
       see evict_inodes() or iput_final()
   your kernel module violates this constraint.

 - If i_count == 0 and I_FREEING is set, the particular in-memory
   inode won't be reused anymore: see find_inode() ->
   __wait_on_freeing_inode().

Actually I found a real issue out of mmap() path of the page cache
sharing feature, the timing is like:

 1) Mount an EROFS filesystem with inode_share enabled and map
    a shared file into a process's address space;

 2) Close the original file descriptors and release other references
    to the original EROFS inodes.  The VMA continues to hold the
    shared backing file.

 3) Under inode/dentry cache reclaim, the original EROFS inodes can
    become unreferenceable and enter eviction. erofs_evict_inode()
    removes each original inode from the shared list through
    erofs_ishare_free_inode(). 

 4) Access a previously untouched folio in the still-live mapping.
    The page fault can invoke read_folio() on the shared
    pseudo-inode's address space;

 5) erofs_real_inode() attempts to obtain a reference to an
    original inode, but no candidate may be successfully acquired.

However, page cache sharing is marked as an experimental feature and
it needs privilege to mount (with a specific mount option), so I
think it's just another normal bug that needs to be resolved.

I disclose my finding based on these facts, and I will send out
a proper fix about the issue I described above later.

Thanks,
Gao Xiang

On Sat, Oct 24, 2026 at 05:33:55PM +0900, YoungWoo Rim wrote:
> Hi,
> 
> I'd like to report a security vulnerability in the EROFS page cache sharing
> feature (CONFIG_EROFS_FS_PAGE_CACHE_SHARE).
> 
> ## Summary
> 
> erofs_real_inode() in fs/erofs/ishare.c declares a local pointer variable
> `realinode` without initialization. Under a race condition during concurrent
> filesystem unmount, all igrab() calls within the function can fail, causing
> the uninitialized stack value to be returned. All callers immediately
> dereference this value without NULL checking, leading to kernel oops/panic
> or potential information leakage.
> 
> ## Affected Versions
> 
> All kernel versions from v7.0 through v7.3-rc7 (latest mainline) with
> CONFIG_EROFS_FS_PAGE_CACHE_SHARE=y. Verified by inspecting source directly
> from kernel.org on 2026-10-24:
> 
>   - v7.0.0:   vulnerable (struct inode *realinode; — no init)
>   - v7.0.9:   vulnerable
>   - v7.2.9:   vulnerable
>   - v7.3-rc7: vulnerable
> 
> This is distinct from CVE-2026-80697 (f_path NULL in backing files).
> 
> ## Vulnerable Code
> 
> fs/erofs/ishare.c, erofs_real_inode(), lines 175-199:
> 
>   struct inode *erofs_real_inode(struct inode *inode, bool *need_iput)
>   {
>       struct erofs_inode *vi, *vi_share;
>       struct inode *realinode;                   // NOT initialized
> 
>       *need_iput = false;
>       if (!erofs_is_ishare_inode(inode))
>           return inode;
> 
>       vi_share = EROFS_I(inode);
>       spin_lock(&vi_share->ishare_lock);
>       DBG_BUGON(list_empty(&vi_share->ishare_list));
>       list_for_each_entry(vi, &vi_share->ishare_list, ishare_list) {
>           realinode = igrab(&vi->vfs_inode);
>           if (realinode) {
>               *need_iput = true;
>               break;
>           }
>       }
>       spin_unlock(&vi_share->ishare_lock);
> 
>       DBG_BUGON(!realinode);                    // no-op in release builds
>       return realinode;                         // returns stack garbage
>   }
> 
> ## Race Window
> 
> The VFS inode freeing path sets I_FREEING (inode.c:1951) before calling
> evict() -> erofs_evict_inode() -> erofs_ishare_free_inode() -> list_del().
> 
> Between I_FREEING being set and list_del() completing:
>   - igrab() returns NULL (because I_FREEING is set)
>   - The inode is still in ishare_list (not yet removed)
> 
> If all inodes in ishare_list are in this state simultaneously (e.g., two
> EROFS mounts being unmounted concurrently), the loop exhausts without
> assigning realinode. Alternatively, if list_del() completes first, the
> list becomes empty and the loop body never executes.
> 
> Both paths leave realinode uninitialized in release builds (DBG_BUGON
> is compiled out).
> 
> ## Impact
> 
>   - Denial of Service: kernel oops/panic from dereferencing stack garbage
>   - Information Leak: if the garbage value points to valid kernel memory,
>     erofs_err() at zdata.c:1898 logs realinode->i_sb and
>     EROFS_I(realinode)->nid to dmesg
> 
> ## Reproduction
> 
> Tested on Linux v7.0.0 (aarch64, QEMU) with
> CONFIG_EROFS_FS_PAGE_CACHE_SHARE=y,
> CONFIG_KASAN=y. Bug confirmed via a kernel module that deterministically
> simulates the race condition by setting I_FREEING on all real inodes in
> the ishare_list before calling igrab().
> 
> ### Environment
>   - Kernel: 7.0.0 (built from kernel.org source)
>   - erofs-utils: 1.9.4 (mkfs.erofs --xattr-inode-digest)
>   - Mount options: -o loop,domain_id=test,inode_share
> 
> ### Steps
>   1. Create two EROFS images with identical file content and fingerprint
> xattrs
>   2. Mount both with domain_id + inode_share (shared inode created)
>   3. Load kernel module that:
>      a. Finds the shared inode (ino=0, pseudo_erofs FS) via ishare_list
>      b. Lists real inodes: 2 entries (ino=54 from each mount)
>      c. Sets I_FREEING on both (simulating concurrent unmount race)
>      d. Calls igrab() on each — both return NULL
>      e. realinode variable left UNINITIALIZED
> 
> ### Kernel log (dmesg) output
> 
>   TRIGGER: FOUND shared inode! ino=0
>   TRIGGER: real[0] ino=54 state=0x0 cnt=1
>   TRIGGER: real[1] ino=54 state=0x0 cnt=1
>   TRIGGER: 2 real inodes. Setting I_FREEING...
>   TRIGGER: igrab(ino=54) = NULL
>   TRIGGER: igrab(ino=54) = NULL
>   TRIGGER: ========================================
>   TRIGGER: *** BUG CONFIRMED ***
>   TRIGGER: ALL igrab returned NULL
>   TRIGGER: realinode is UNINITIALIZED
>   TRIGGER: ========================================
> 
> The full kernel module source and dmesg trace are available upon request.
> 
> ## Proposed Patch
> 
> The minimal fix initializes the variable and adds caller-side NULL checks:
> 
> --- a/fs/erofs/ishare.c
> +++ b/fs/erofs/ishare.c
> @@ -178,7 +178,7 @@
> -    struct inode *realinode;
> +    struct inode *realinode = NULL;
> @@ -197,7 +197,8 @@
> -    DBG_BUGON(!realinode);
> +    if (unlikely(!realinode))
> +        pr_warn_ratelimited("erofs: ishare has no live real inode\n");
>      return realinode;
> 
> Additionally, all six callers (fileio.c:160,175; data.c:389,407;
> zdata.c:1885,1913) should check for NULL before dereferencing:
> 
>   struct inode *realinode = erofs_real_inode(..., &need_iput);
>   if (unlikely(!realinode)) {
>       folio_unlock(folio);  // or simply return for readahead
>       return -EIO;
>   }
> 
> I can provide a complete patch series if needed.
> 
> ## Threat Assessment
> 
> The primary threat is Denial of Service (kernel panic). A secondary risk
> is kernel address information leakage via dmesg, which could aid KASLR
> bypass if chained with a separate write vulnerability. Remote Code
> Execution is not feasible with this vulnerability alone — all
> dereferences are reads, there is no function pointer invocation through
> the fake inode, and EROFS is a read-only filesystem with no userdata →
> kernel memory write path.
> 
>   - Primary: DoS (High) — kernel oops/panic
>   - Secondary: Info Leak (Medium) — kernel heap addresses in dmesg
>   - RCE: Not applicable standalone
> 
> ## Classification
> 
>   - CWE-457: Use of Uninitialized Variable
>   - CWE-362: Concurrent Execution Using Shared Resource with
>              Improper Synchronization (Race Condition)
>   - Estimated CVSS 3.1: 5.3-6.5 (Medium)
> 
> ## CVE Request
> 
> Could you please assign a CVE ID for this issue? This vulnerability is
> distinct from CVE-2026-80697, which addresses a different bug (NULL
> f_path in backing files) in the same feature.
> 
> ## Disclosure
> 
> I am happy to coordinate on a disclosure timeline. I would suggest
> a standard 90-day embargo from the date of this report, but I am
> flexible and will follow the kernel security team's preferred process.
> 
> If you prefer encrypted communication, I can provide a PGP public key.
> 
> Regards,
> Youngwoo Rim
> .

> [  130.613096] TRIGGER: target=/mnt/erofs1/file.bin
> [  130.613128] TRIGGER: ino=54 mapping=ffff00000d344b80 i_data=ffff00000d344b80 redir=0
> [  130.613136] TRIGGER: ishare_list empty=0 sb_type=erofs
> [  130.613142] TRIGGER: peer[0] ino=0 sb=pseudo_erofs mapping=ffff00000d3445e8 i_data=ffff00000d3445e8 redir=0
> [  130.613150] TRIGGER: peer[1] ino=54 sb=erofs mapping=ffff00000d344050 i_data=ffff00000d344050 redir=0
> [  130.613157] TRIGGER: 2 peers in list
> [  130.613161] TRIGGER: FOUND shared inode! ino=0
> [  130.616348] TRIGGER: real[0] ino=54 state=0x0 cnt=1
> [  130.617044] TRIGGER: real[1] ino=54 state=0x0 cnt=1
> [  130.617112] TRIGGER: 2 real inodes. Setting I_FREEING...
> [  130.617129] TRIGGER: igrab(ino=54) = NULL
> [  130.617134] TRIGGER: igrab(ino=54) = NULL
> [  130.617139] TRIGGER: ========================================
> [  130.617144] TRIGGER: *** BUG CONFIRMED ***
> [  130.617148] TRIGGER: ALL igrab returned NULL
> [  130.617152] TRIGGER: realinode is UNINITIALIZED
> [  130.617156] TRIGGER: ========================================
> [  130.617163] TRIGGER: done




[-- Attachment #2: trigger_ishare_bug.c --]
[-- Type: text/plain, Size: 4234 bytes --]

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/namei.h>
#include "/home/user/linux-7.0/fs/erofs/internal.h"

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Trigger erofs ishare uninit variable bug");

static char *target = "/mnt/erofs1/file.bin";
module_param(target, charp, 0444);

static int __init trigger_init(void)
{
	struct path p;
	struct inode *inode;
	struct erofs_inode *vi, *tmp;
	int ret, cnt;

	pr_err("TRIGGER: target=%s\n", target);

	ret = kern_path(target, LOOKUP_FOLLOW, &p);
	if (ret) {
		pr_err("TRIGGER: kern_path: %d\n", ret);
		return ret;
	}

	inode = p.dentry->d_inode;
	vi = EROFS_I(inode);

	pr_err("TRIGGER: ino=%lu mapping=%px i_data=%px redir=%d\n",
	       inode->i_ino, inode->i_mapping, &inode->i_data,
	       inode->i_mapping != &inode->i_data);
	pr_err("TRIGGER: ishare_list empty=%d sb_type=%s\n",
	       list_empty(&vi->ishare_list),
	       inode->i_sb->s_type->name);

	/* Walk the ishare_list from this inode's perspective */
	cnt = 0;
	list_for_each_entry(tmp, &vi->ishare_list, ishare_list) {
		struct inode *ti = &tmp->vfs_inode;
		pr_err("TRIGGER: peer[%d] ino=%lu sb=%s mapping=%px i_data=%px redir=%d\n",
		       cnt, ti->i_ino, ti->i_sb->s_type->name,
		       ti->i_mapping, &ti->i_data,
		       ti->i_mapping != &ti->i_data);
		cnt++;
		if (cnt > 20) break;
	}
	pr_err("TRIGGER: %d peers in list\n", cnt);

	/*
	 * If any peer has redirected mapping, use its mapping->host
	 * as the shared inode. Or if a peer is on erofs_anon_fs_type,
	 * that IS the shared inode.
	 */
	cnt = 0;
	list_for_each_entry(tmp, &vi->ishare_list, ishare_list) {
		struct inode *ti = &tmp->vfs_inode;
		if (ti->i_sb->s_type == &erofs_anon_fs_type) {
			struct erofs_inode *vis = tmp;
			struct erofs_inode *real_iter;
			int rcnt = 0;

			pr_err("TRIGGER: FOUND shared inode! ino=%lu\n", ti->i_ino);

			/* Count real inodes in the shared inode's list */
			spin_lock(&vis->ishare_lock);
			list_for_each_entry(real_iter, &vis->ishare_list, ishare_list) {
				pr_err("TRIGGER: real[%d] ino=%lu state=0x%lx cnt=%d\n",
				       rcnt, real_iter->vfs_inode.i_ino,
				       (unsigned long)inode_state_read(&real_iter->vfs_inode),
				       atomic_read(&real_iter->vfs_inode.i_count));
				rcnt++;
			}
			spin_unlock(&vis->ishare_lock);

			pr_err("TRIGGER: %d real inodes. Setting I_FREEING...\n", rcnt);

			/* Set I_FREEING on all real inodes */
			spin_lock(&vis->ishare_lock);
			list_for_each_entry(real_iter, &vis->ishare_list, ishare_list) {
				spin_lock(&real_iter->vfs_inode.i_lock);
				inode_state_set(&real_iter->vfs_inode, I_FREEING);
				spin_unlock(&real_iter->vfs_inode.i_lock);
			}
			spin_unlock(&vis->ishare_lock);

			/* Test igrab — same as erofs_real_inode() */
			{
				struct erofs_inode *vi_iter;
				struct inode *realinode;
				int got = 0;

				spin_lock(&vis->ishare_lock);
				list_for_each_entry(vi_iter, &vis->ishare_list, ishare_list) {
					realinode = igrab(&vi_iter->vfs_inode);
					pr_err("TRIGGER: igrab(ino=%lu) = %s\n",
					       vi_iter->vfs_inode.i_ino,
					       realinode ? "OK" : "NULL");
					if (realinode) {
						got = 1;
						break;
					}
				}
				spin_unlock(&vis->ishare_lock);

				if (!got) {
					pr_err("TRIGGER: ========================================\n");
					pr_err("TRIGGER: *** BUG CONFIRMED ***\n");
					pr_err("TRIGGER: ALL igrab returned NULL\n");
					pr_err("TRIGGER: realinode is UNINITIALIZED\n");
					pr_err("TRIGGER: ========================================\n");
				} else {
					pr_err("TRIGGER: igrab succeeded (bug not triggered)\n");
					iput(realinode);
				}
			}

			/* Restore inode states */
			spin_lock(&vis->ishare_lock);
			list_for_each_entry(real_iter, &vis->ishare_list, ishare_list) {
				spin_lock(&real_iter->vfs_inode.i_lock);
				inode_state_clear_raw(&real_iter->vfs_inode, I_FREEING);
				spin_unlock(&real_iter->vfs_inode.i_lock);
			}
			spin_unlock(&vis->ishare_lock);

			break;
		}
		cnt++;
		if (cnt > 20) break;
	}

	if (cnt > 20)
		pr_err("TRIGGER: shared inode not found in list\n");

	path_put(&p);
	pr_err("TRIGGER: done\n");
	return -EAGAIN;
}

static void __exit trigger_exit(void) {}
module_init(trigger_init);
module_exit(trigger_exit);

^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~2026-10-24 15:44 UTC | newest]

Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
     [not found] <CAM_WGAzh1WWJgdK=NV+MR9bU8Bn8Pe14QJW73y=XUjGt032ybA@mail.gmail.com>
2026-10-24 15:44 ` erofs: use of uninitialized variable in erofs_real_inode() Gao Xiang

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.