* 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.