Merge patch series "OPENAT2_REGULAR flag support for openat2"

Dorjoy Chowdhury <dorjoychy111@gmail.com> says:

I came upon this "Ability to only open regular files" uapi feature
suggestion from
https://uapi-group.org/kernel-features/#ability-to-only-open-regular-files
and thought it would be something I could do as a first patch and get to
know the kernel code a bit better.

The following filesystems have been tested by building and booting the
kernel x86 bzImage in a Fedora 43 VM in QEMU. I have tested with
OPENAT2_REGULAR that regular files can be successfully opened and
non-regular files (directory, fifo etc) return -EFTYPE.

- btrfs
- NFS (loopback)
- SMB (loopback)

Christian Brauner (Amutable) <brauner@kernel.org>:

All atomic_open implementations were audited for OPENAT2_REGULAR handling.
Explicit checks were added to ceph, gfs2, nfs (v4), and cifs/smb — these
are the filesystems whose atomic_open can encounter an existing non-regular
file and would otherwise call finish_open() on it or return a misleading
error code. The checks allow these filesystems to return -EFTYPE directly
and avoid unnecessary open+close round-trips.

The remaining implementations (9p, fuse, vboxsf, nfs v2/v3) don't need
explicit checks. They only call finish_open() on freshly created files
(always S_IFREG) and use finish_no_open() for lookup hits, letting the
VFS catch non-regular files via the do_open() safety net. Notably, fuse
also validates the server response (S_ISREG check on the reply) before
reaching finish_open().

* patches from https://patch.msgid.link/20260328172314.45807-1-dorjoychy111@gmail.com:
  kselftest/openat2: test for OPENAT2_REGULAR flag
  openat2: new OPENAT2_REGULAR flag support

Link: https://patch.msgid.link/20260328172314.45807-1-dorjoychy111@gmail.com
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
Christian Brauner 2026-04-16 12:58:57 +02:00
commit 21688d8122
No known key found for this signature in database
GPG Key ID: 91C61BC06578DCA2
20 changed files with 162 additions and 8 deletions

View File

@ -127,4 +127,6 @@
#define EHWPOISON 139 /* Memory page has hardware error */
#define EFTYPE 140 /* Wrong file type for the intended operation */
#endif

View File

@ -126,6 +126,8 @@
#define EHWPOISON 168 /* Memory page has hardware error */
#define EFTYPE 169 /* Wrong file type for the intended operation */
#define EDQUOT 1133 /* Quota exceeded */

View File

@ -124,4 +124,6 @@
#define EHWPOISON 257 /* Memory page has hardware error */
#define EFTYPE 258 /* Wrong file type for the intended operation */
#endif

View File

@ -117,4 +117,6 @@
#define EHWPOISON 135 /* Memory page has hardware error */
#define EFTYPE 136 /* Wrong file type for the intended operation */
#endif

View File

@ -996,6 +996,10 @@ int ceph_atomic_open(struct inode *dir, struct dentry *dentry,
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:

View 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,

View 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;

View 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;

View 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))

View File

@ -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 */

View 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 @@ static int __cifs_do_create(struct inode *dir, struct dentry *direntry,
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;

View 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 \
@ -12,6 +13,23 @@
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 | \

View File

@ -122,4 +122,6 @@
#define EHWPOISON 133 /* Memory page has hardware error */
#define EFTYPE 134 /* Wrong file type for the intended operation */
#endif

View 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). */

View File

@ -127,4 +127,6 @@
#define EHWPOISON 139 /* Memory page has hardware error */
#define EFTYPE 140 /* Wrong file type for the intended operation */
#endif

View File

@ -126,6 +126,8 @@
#define EHWPOISON 168 /* Memory page has hardware error */
#define EFTYPE 169 /* Wrong file type for the intended operation */
#define EDQUOT 1133 /* Quota exceeded */

View File

@ -124,4 +124,6 @@
#define EHWPOISON 257 /* Memory page has hardware error */
#define EFTYPE 258 /* Wrong file type for the intended operation */
#endif

View File

@ -117,4 +117,6 @@
#define EHWPOISON 135 /* Memory page has hardware error */
#define EFTYPE 136 /* Wrong file type for the intended operation */
#endif

View File

@ -122,4 +122,6 @@
#define EHWPOISON 133 /* Memory page has hardware error */
#define EFTYPE 134 /* Wrong file type for the intended operation */
#endif

View File

@ -310,4 +310,53 @@ TEST_F(openat2, flag_validation)
}
}
#ifndef OPENAT2_REGULAR
#define OPENAT2_REGULAR ((__u64)1 << 32)
#endif
#ifndef EFTYPE
#define EFTYPE 134
#endif
/* Kernel-internal carrier for OPENAT2_REGULAR (see __O_REGULAR in fcntl.h). */
#ifndef __O_REGULAR
#define __O_REGULAR (1 << 30)
#endif
/* Verify that OPENAT2_REGULAR rejects non-regular files with EFTYPE. */
TEST_F(openat2, regular_flag)
{
struct open_how how = {
.flags = OPENAT2_REGULAR | O_RDONLY,
};
int fd;
fd = sys_openat2(AT_FDCWD, "/dev/null", &how);
if (fd == -ENOENT)
SKIP(return, "/dev/null does not exist");
EXPECT_EQ(-EFTYPE, fd) {
TH_LOG("openat2 with OPENAT2_REGULAR should fail with %d (%s), got %d (%s)",
-EFTYPE, strerror(EFTYPE), fd, strerror(-fd));
}
if (fd >= 0)
close(fd);
}
/* open()/openat() must keep ignoring the internal __O_REGULAR bit. */
TEST(legacy_openat_ignores_o_regular)
{
int fd;
fd = openat(AT_FDCWD, "/dev/null", O_RDONLY | __O_REGULAR);
if (fd < 0 && errno == ENOENT)
SKIP(return, "/dev/null does not exist");
ASSERT_GE(fd, 0) {
TH_LOG("legacy openat() must ignore the __O_REGULAR carrier bit, got errno %d (%s)",
errno, strerror(errno));
}
close(fd);
}
TEST_HARNESS_MAIN