]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
openat2: new OPENAT2_REGULAR flag support
authorDorjoy Chowdhury <dorjoychy111@gmail.com>
Sat, 16 May 2026 14:42:39 +0000 (16:42 +0200)
committerChristian Brauner <brauner@kernel.org>
Thu, 21 May 2026 13:33:47 +0000 (15:33 +0200)
This flag indicates the path should be opened if it's a regular file.
This is useful to write secure programs that want to avoid being
tricked into opening device nodes with special semantics while thinking
they operate on regular files. This is a requested feature from the
uapi-group[1].

The previously introduced EFTYPE error code is returned when the path
doesn't refer to a regular file. For example, if openat2 is called on
path /dev/null with OPENAT2_REGULAR in the flag param, it will return
-EFTYPE.

When used in combination with O_CREAT, either the regular file is
created, or if the path already exists, it is opened if it's a regular
file. Otherwise, -EFTYPE is returned.

When OPENAT2_REGULAR is combined with O_DIRECTORY, -EINVAL is returned
as it doesn't make sense to open a path that is both a directory and a
regular file.

The UAPI bit lives in the upper 32 bits of open_how::flags
(((__u64)1 << 32)) so that open(2) and openat(2) -- whose @flags
argument is a C int -- cannot physically express it. This is a
structural guarantee, not a runtime mask: the bit is unrepresentable in
32 bits.

Because the rest of the VFS open path narrows to 32 bits in several
places (op->open_flag, f->f_flags, the unsigned open_flag argument of
i_op->atomic_open()), build_open_flags() translates OPENAT2_REGULAR
into a kernel-internal lower-32-bit carrier __O_REGULAR (bit 4, unused
as an O_* on every architecture) before the assignment to op->open_flag.
__O_REGULAR then rides through the existing channels exactly like
__FMODE_EXEC. do_dentry_open() strips it so it cannot leak back to
userspace via fcntl(F_GETFL).

Four BUILD_BUG_ON_MSG() invariants in build_open_flags() prevent any
future bit collision or accidental low-32 redefinition:

  - VALID_OPEN_FLAGS fits in 32 bits.
  - OPENAT2_REGULAR lives in the upper 32 bits.
  - OPENAT2_REGULAR does not alias any open()/openat() flag.
  - __O_REGULAR does not alias any user-visible flag.

[1]: https://uapi-group.org/kernel-features/#ability-to-only-open-regular-files

Christian Brauner <brauner@kernel.org> says:

Move OPENAT2_REGULAR to the upper 32 bits of open_how::flags with a
kernel-internal __O_REGULAR carrier so that open(2)/openat(2) cannot
encode the flag; add BUILD_BUG_ON_MSG() invariants and register
__O_REGULAR in the fcntl_init() allocation-uniqueness BUILD_BUG_ON()
(bit count 21 -> 22).

Signed-off-by: Dorjoy Chowdhury <dorjoychy111@gmail.com>
Link: https://patch.msgid.link/20260328172314.45807-2-dorjoychy111@gmail.com
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Reviewed-by: Aleksa Sarai <aleksa@amutable.com>
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
fs/ceph/file.c
fs/fcntl.c
fs/gfs2/inode.c
fs/namei.c
fs/nfs/dir.c
fs/open.c
fs/smb/client/dir.c
include/linux/fcntl.h
include/uapi/linux/openat2.h

index d54d71669176bd16ce02e4ad670c8a9c5c269ad7..0ad42e1cc30588a7b04dc2621ef1928b52f270c0 100644 (file)
@@ -996,6 +996,10 @@ retry:
                        ceph_init_inode_acls(newino, &as_ctx);
                        file->f_mode |= FMODE_CREATED;
                }
+               if ((flags & __O_REGULAR) && !d_is_reg(dentry)) {
+                       err = -EFTYPE;
+                       goto out_req;
+               }
                err = finish_open(file, dentry, ceph_open);
        }
 out_req:
index 7d2165855a9c50128f76757664ed4780417d0aaf..b3ea135b74d8b86e48da64d84133cb081bc09bc3 100644 (file)
@@ -1169,10 +1169,10 @@ static int __init fcntl_init(void)
         * Exceptions: O_NONBLOCK is a two bit define on parisc; O_NDELAY
         * is defined as O_NONBLOCK on some platforms and not on others.
         */
-       BUILD_BUG_ON(21 - 1 /* for O_RDONLY being 0 */ !=
+       BUILD_BUG_ON(22 - 1 /* for O_RDONLY being 0 */ !=
                HWEIGHT32(
                        (VALID_OPEN_FLAGS & ~(O_NONBLOCK | O_NDELAY)) |
-                       __FMODE_EXEC));
+                       __FMODE_EXEC | __O_REGULAR));
 
        fasync_cache = kmem_cache_create("fasync_cache",
                                         sizeof(struct fasync_struct), 0,
index e9bf4879c07f7335f8e53938b44be21209fee4fd..e9895dea0da49a025f17c0bb90868e9329b23858 100644 (file)
@@ -738,6 +738,13 @@ static int gfs2_create_inode(struct inode *dir, struct dentry *dentry,
        inode = gfs2_dir_search(dir, &dentry->d_name, !S_ISREG(mode) || excl);
        error = PTR_ERR(inode);
        if (!IS_ERR(inode)) {
+               if (file && (file->f_flags & __O_REGULAR) &&
+                   !S_ISREG(inode->i_mode)) {
+                       iput(inode);
+                       inode = NULL;
+                       error = -EFTYPE;
+                       goto fail_gunlock;
+               }
                if (S_ISDIR(inode->i_mode)) {
                        iput(inode);
                        inode = NULL;
index c7fac83c9a85ef250bb424af0a91d581ee9f4919..e1fe0f28b923649baf21c27ae2b470f1851f25a4 100644 (file)
@@ -4679,6 +4679,10 @@ static int do_open(struct nameidata *nd,
                if (unlikely(error))
                        return error;
        }
+
+       if ((open_flag & __O_REGULAR) && !d_is_reg(nd->path.dentry))
+               return -EFTYPE;
+
        if ((nd->flags & LOOKUP_DIRECTORY) && !d_can_lookup(nd->path.dentry))
                return -ENOTDIR;
 
index e9ce1883288c55bbf24671c1e4bd1a61a694ae3f..1b9c368fb13387bf5e06a5f4e75d5c897e08bf19 100644 (file)
@@ -2194,6 +2194,10 @@ int nfs_atomic_open(struct inode *dir, struct dentry *dentry,
                        break;
                case -EISDIR:
                case -ENOTDIR:
+                       if (open_flags & __O_REGULAR) {
+                               err = -EFTYPE;
+                               break;
+                       }
                        goto no_open;
                case -ELOOP:
                        if (!(open_flags & O_NOFOLLOW))
index 9e0164a8c1fbe688f85cc0deb525117a5343a2f6..5458668a68e11100539c3b5c1746613875b4a4a9 100644 (file)
--- a/fs/open.c
+++ b/fs/open.c
@@ -960,7 +960,7 @@ static int do_dentry_open(struct file *f,
        if (f->f_mapping->a_ops && f->f_mapping->a_ops->direct_IO)
                f->f_mode |= FMODE_CAN_ODIRECT;
 
-       f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);
+       f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC | __O_REGULAR);
        f->f_iocb_flags = iocb_flags(f);
 
        file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping);
@@ -1184,7 +1184,15 @@ inline int build_open_flags(const struct open_how *how, struct open_flags *op)
        int acc_mode = ACC_MODE(flags);
 
        BUILD_BUG_ON_MSG(upper_32_bits(VALID_OPEN_FLAGS),
-                        "struct open_flags doesn't yet handle flags > 32 bits");
+                        "VALID_OPEN_FLAGS must fit in 32 bits");
+       /* The whole point: OPENAT2_REGULAR must be unrepresentable in int. */
+       BUILD_BUG_ON_MSG(!upper_32_bits(OPENAT2_REGULAR),
+                        "OPENAT2_REGULAR must live in the upper 32 bits of open_how::flags");
+       /* Prevent a future bit collision between UAPI and internal carrier. */
+       BUILD_BUG_ON_MSG(OPENAT2_REGULAR & VALID_OPEN_FLAGS,
+                        "OPENAT2_REGULAR must not alias any open()/openat() flag");
+       BUILD_BUG_ON_MSG(__O_REGULAR & VALID_OPENAT2_FLAGS,
+                        "__O_REGULAR must not alias any user-visible flag");
 
        /*
         * Strip flags that aren't relevant in determining struct open_flags.
@@ -1196,7 +1204,7 @@ inline int build_open_flags(const struct open_how *how, struct open_flags *op)
         * values before calling build_open_flags(), but openat2(2) checks all
         * of its arguments.
         */
-       if (flags & ~VALID_OPEN_FLAGS)
+       if (flags & ~VALID_OPENAT2_FLAGS)
                return -EINVAL;
        if (how->resolve & ~VALID_RESOLVE_FLAGS)
                return -EINVAL;
@@ -1236,6 +1244,14 @@ inline int build_open_flags(const struct open_how *how, struct open_flags *op)
                if (!(acc_mode & MAY_WRITE))
                        return -EINVAL;
        }
+       /*
+        * Asking to open a directory and a regular file at the same time is
+        * contradictory.
+        */
+       if ((flags & (O_DIRECTORY | OPENAT2_REGULAR)) ==
+           (O_DIRECTORY | OPENAT2_REGULAR))
+               return -EINVAL;
+
        if (flags & O_PATH) {
                /* O_PATH only permits certain other flags to be set. */
                if (flags & ~O_PATH_FLAGS)
@@ -1252,6 +1268,19 @@ inline int build_open_flags(const struct open_how *how, struct open_flags *op)
        if (flags & __O_SYNC)
                flags |= O_DSYNC;
 
+       /*
+        * Translate the upper-32-bit UAPI bit OPENAT2_REGULAR into the
+        * kernel-internal lower-32-bit __O_REGULAR carrier so the bit
+        * survives the assignment to op->open_flag (an int) below and the
+        * subsequent flow through f->f_flags (unsigned int) and the
+        * i_op->atomic_open() callback (unsigned). do_dentry_open() strips
+        * __O_REGULAR before the file becomes visible to userspace.
+        */
+       if (flags & OPENAT2_REGULAR) {
+               flags &= ~OPENAT2_REGULAR;
+               flags |= __O_REGULAR;
+       }
+
        op->open_flag = flags;
 
        /* O_TRUNC implies we need access checks for write permissions */
index e4295a5b55b3473bfb6fa0f44e14bf5fa75a8ba4..88a4a1787ff0479fdf0d18d6c7643f6d0b760279 100644 (file)
@@ -241,6 +241,12 @@ static int __cifs_do_create(struct inode *dir, struct dentry *direntry,
                                goto cifs_create_get_file_info;
                        }
 
+                       if ((oflags & __O_REGULAR) && !S_ISREG(newinode->i_mode)) {
+                               CIFSSMBClose(xid, tcon, fid->netfid);
+                               iput(newinode);
+                               return -EFTYPE;
+                       }
+
                        if (S_ISDIR(newinode->i_mode)) {
                                CIFSSMBClose(xid, tcon, fid->netfid);
                                iput(newinode);
@@ -458,9 +464,15 @@ cifs_create_set_dentry:
                goto out_err;
        }
 
-       if (newinode && S_ISDIR(newinode->i_mode)) {
-               rc = -EISDIR;
-               goto out_err;
+       if (newinode) {
+               if ((oflags & __O_REGULAR) && !S_ISREG(newinode->i_mode)) {
+                       rc = -EFTYPE;
+                       goto out_err;
+               }
+               if (S_ISDIR(newinode->i_mode)) {
+                       rc = -EISDIR;
+                       goto out_err;
+               }
        }
 
        *inode = newinode;
index c65c5c73d362ff5d49dade5a589daa230bcd7143..6ad6b9e7a226af4de778c0bdf30d0657f2700ef1 100644 (file)
@@ -4,6 +4,7 @@
 
 #include <linux/stat.h>
 #include <uapi/linux/fcntl.h>
+#include <uapi/linux/openat2.h>
 
 /* List of all valid flags for the open/openat flags argument: */
 #define VALID_OPEN_FLAGS \
         FASYNC | O_DIRECT | O_LARGEFILE | O_DIRECTORY | O_NOFOLLOW | \
         O_NOATIME | O_CLOEXEC | O_PATH | __O_TMPFILE | O_EMPTYPATH)
 
+/* List of all valid flags for openat2(2)'s how->flags argument. */
+#define VALID_OPENAT2_FLAGS    (VALID_OPEN_FLAGS | OPENAT2_REGULAR)
+
+/*
+ * Kernel-internal carrier for OPENAT2_REGULAR. The UAPI bit lives in the
+ * upper 32 bits of open_how::flags so open()/openat() cannot encode it.
+ * build_open_flags() translates it to this internal flag, which then
+ * propagates through op->open_flag and f->f_flags exactly like __FMODE_EXEC.
+ * do_dentry_open() strips it so userspace cannot observe it via
+ * fcntl(F_GETFL).
+ *
+ * Bit 30 is not claimed by any O_* flag on any architecture and stays clear
+ * of the sign bit of the int op->open_flag. fcntl_init() enforces that it
+ * never aliases an open-flag bit.
+ */
+#define __O_REGULAR            (1 << 30)
+
 /* List of all valid flags for the how->resolve argument: */
 #define VALID_RESOLVE_FLAGS \
        (RESOLVE_NO_XDEV | RESOLVE_NO_MAGICLINKS | RESOLVE_NO_SYMLINKS | \
index a5feb7604948791c7a34ec0876c24a97eb5a375b..575c2c59d14a9bb637d132f2c19cb6ba13ec4d37 100644 (file)
@@ -22,6 +22,13 @@ struct open_how {
        __u64 resolve;
 };
 
+/*
+ * how->flags bits exclusive to openat2(2). These live in the upper 32 bits
+ * of @flags so that they cannot be expressed by open(2) / openat(2), whose
+ * @flags argument is a C int.
+ */
+#define OPENAT2_REGULAR                ((__u64)1 << 32) /* Only open regular files. */
+
 /* how->resolve flags for openat2(2). */
 #define RESOLVE_NO_XDEV                0x01 /* Block mount-point crossings
                                        (includes bind-mounts). */