summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Brauner <brauner@kernel.org>2026-09-02 22:44:18 +0200
committerChristian Brauner <brauner@kernel.org>2026-09-02 22:44:18 +0200
commit0be7bef40fb20eb3cd8bcf023dc4da8ed3fbdc16 (patch)
tree131028299ffbbabdabf63ca4fd692881b70a03e3
parent6b2217f70f3f1aa40176765eafed1609a27a389e (diff)
parent0d1ea1532955fa881424cb01c4599d14b90a22f6 (diff)
downloadlinux-next-0be7bef40fb20eb3cd8bcf023dc4da8ed3fbdc16.tar.gz
linux-next-0be7bef40fb20eb3cd8bcf023dc4da8ed3fbdc16.zip
Merge branch 'vfs-7.4.lookup' into vfs.all
Signed-off-by: Christian Brauner <brauner@kernel.org>
-rw-r--r--fs/9p/vfs_inode.c3
-rw-r--r--fs/9p/vfs_inode_dotl.c3
-rw-r--r--fs/ceph/file.c3
-rw-r--r--fs/fuse/dir.c3
-rw-r--r--fs/gfs2/inode.c3
-rw-r--r--fs/internal.h1
-rw-r--r--fs/namei.c227
-rw-r--r--fs/nfs/dir.c6
-rw-r--r--fs/open.c59
-rw-r--r--fs/smb/client/dir.c3
-rw-r--r--fs/vboxsf/dir.c3
-rw-r--r--include/linux/fcntl.h6
-rw-r--r--tools/testing/selftests/filesystems/.gitignore1
-rw-r--r--tools/testing/selftests/filesystems/Makefile2
-rw-r--r--tools/testing/selftests/filesystems/open_o_creat_o_dir.c201
-rw-r--r--tools/testing/selftests/filesystems/wrappers.h11
16 files changed, 455 insertions, 80 deletions
diff --git a/fs/9p/vfs_inode.c b/fs/9p/vfs_inode.c
index 3829554ca369..b1e0823c87b5 100644
--- a/fs/9p/vfs_inode.c
+++ b/fs/9p/vfs_inode.c
@@ -776,6 +776,9 @@ v9fs_vfs_atomic_open(struct inode *dir, struct dentry *dentry,
struct inode *inode;
int p9_omode;
+ if (O_IS_MKDIR(flags))
+ flags &= ~O_CREAT;
+
if (d_in_lookup(dentry)) {
struct dentry *res = v9fs_vfs_lookup(dir, dentry, 0);
if (res || d_really_is_positive(dentry))
diff --git a/fs/9p/vfs_inode_dotl.c b/fs/9p/vfs_inode_dotl.c
index 116b29e95f21..64e0aba08da1 100644
--- a/fs/9p/vfs_inode_dotl.c
+++ b/fs/9p/vfs_inode_dotl.c
@@ -238,6 +238,9 @@ v9fs_vfs_atomic_open_dotl(struct inode *dir, struct dentry *dentry,
struct v9fs_session_info *v9ses;
struct posix_acl *pacl = NULL, *dacl = NULL;
+ if (O_IS_MKDIR(flags))
+ flags &= ~O_CREAT;
+
if (d_in_lookup(dentry)) {
struct dentry *res = v9fs_vfs_lookup(dir, dentry, 0);
if (res || d_really_is_positive(dentry))
diff --git a/fs/ceph/file.c b/fs/ceph/file.c
index bd3e3f5c269e..78a0b2540f10 100644
--- a/fs/ceph/file.c
+++ b/fs/ceph/file.c
@@ -812,6 +812,9 @@ int ceph_atomic_open(struct inode *dir, struct dentry *dentry,
dir, ceph_vinop(dir), dentry, dentry,
d_unhashed(dentry) ? "unhashed" : "hashed", flags, mode);
+ if (O_IS_MKDIR(flags))
+ flags &= ~O_CREAT;
+
if (dentry->d_name.len > NAME_MAX)
return -ENAMETOOLONG;
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index e49b4e874b15..17217d97b9e7 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -944,6 +944,9 @@ static int fuse_atomic_open(struct inode *dir, struct dentry *entry,
struct mnt_idmap *idmap = file_mnt_idmap(file);
struct fuse_conn *fc = get_fuse_conn(dir);
+ if (O_IS_MKDIR(flags))
+ flags &= ~O_CREAT;
+
if (fuse_is_bad(dir))
return -EIO;
diff --git a/fs/gfs2/inode.c b/fs/gfs2/inode.c
index f361876c5583..3ee1360f1bc2 100644
--- a/fs/gfs2/inode.c
+++ b/fs/gfs2/inode.c
@@ -1386,6 +1386,9 @@ static int gfs2_atomic_open(struct inode *dir, struct dentry *dentry,
{
bool excl = !!(flags & O_EXCL);
+ if (O_IS_MKDIR(flags))
+ flags &= ~O_CREAT;
+
if (d_in_lookup(dentry)) {
struct dentry *d = __gfs2_lookup(dir, dentry, file);
if (file->f_mode & FMODE_OPENED) {
diff --git a/fs/internal.h b/fs/internal.h
index c658c8a5ebd5..9632239036ac 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -205,6 +205,7 @@ int do_fchownat(int dfd, const char __user *filename, uid_t user, gid_t group,
int flag);
int chown_common(const struct path *path, uid_t user, gid_t group);
extern int vfs_open(const struct path *, struct file *);
+int vfs_open_consume(struct path *, struct file *);
/*
* inode.c
diff --git a/fs/namei.c b/fs/namei.c
index 20a6534ea3ef..9da900f6c113 100644
--- a/fs/namei.c
+++ b/fs/namei.c
@@ -1382,13 +1382,13 @@ int may_linkat(struct mnt_idmap *idmap, const struct path *link)
/**
* may_create_in_sticky - Check whether an O_CREAT open in a sticky directory
- * should be allowed, or not, on files that already
- * exist.
+ * should be allowed, or not, on files/directories that
+ * already exist.
* @idmap: idmap of the mount the inode was found from
* @nd: nameidata pathwalk data
* @inode: the inode of the file to open
*
- * Block an O_CREAT open of a FIFO (or a regular file) when:
+ * Block an O_CREAT open of a FIFO (or a regular file/directory) when:
* - sysctl_protected_fifos (or sysctl_protected_regular) is enabled
* - the file already exists
* - we are in a sticky directory
@@ -1416,6 +1416,14 @@ static int may_create_in_sticky(struct mnt_idmap *idmap, struct nameidata *nd,
if (likely(!(dir_mode & S_ISVTX)))
return 0;
+ /*
+ * There is no separate sysctl for directory creation in sticky
+ * folders. Therefore, for the S_ISDIR case, disabling
+ * sysctl_protected_regular is not enough to allow creating a
+ * directory in a sticky folder, because that may surprise users
+ * not expecting that O_CREAT|O_DIRECTORY is possible on newer
+ * kernels.
+ */
if (S_ISREG(inode->i_mode) && !sysctl_protected_regular)
return 0;
@@ -1447,6 +1455,12 @@ static int may_create_in_sticky(struct mnt_idmap *idmap, struct nameidata *nd,
"sticky_create_regular");
return -EACCES;
}
+
+ if (S_ISDIR(inode->i_mode)) {
+ audit_log_path_denied(AUDIT_ANOM_CREAT,
+ "sticky_create_dir");
+ return -EACCES;
+ }
}
return 0;
@@ -2781,9 +2795,16 @@ static const char *path_init(struct nameidata *nd, unsigned flags)
return s;
}
+static inline bool trailing_slashes(const struct qstr *last)
+{
+ /* last->len is set by hash_name() to the length of the current
+ * component ->name, terminating with '/' or a NUL character. */
+ return (bool)last->name[last->len];
+}
+
static inline const char *lookup_last(struct nameidata *nd)
{
- if (nd->last_type == LAST_NORM && nd->last.name[nd->last.len])
+ if (nd->last_type == LAST_NORM && trailing_slashes(&nd->last))
nd->flags |= LOOKUP_FOLLOW | LOOKUP_DIRECTORY;
return walk_component(nd, WALK_TRAILING);
@@ -4159,6 +4180,24 @@ static inline umode_t vfs_prepare_mode(struct mnt_idmap *idmap,
return mode;
}
+static inline
+int vfs_create_no_perm(struct mnt_idmap *idmap, struct dentry *dentry,
+ umode_t mode, struct delegated_inode *di)
+{
+ struct inode *dir = d_inode(dentry->d_parent);
+ int error;
+
+ error = try_break_deleg(dir, LEASE_BREAK_DIR_CREATE, di);
+ if (error)
+ return error;
+
+ error = dir->i_op->create(idmap, dir, dentry, mode);
+ if (!error)
+ fsnotify_create(dir, dentry);
+
+ return error;
+}
+
/**
* vfs_create - create new file
* @idmap: idmap of the mount the inode was found from
@@ -4191,13 +4230,8 @@ int vfs_create(struct mnt_idmap *idmap, struct dentry *dentry, umode_t mode,
error = security_inode_create(dir, dentry, mode);
if (error)
return error;
- error = try_break_deleg(dir, LEASE_BREAK_DIR_CREATE, di);
- if (error)
- return error;
- error = dir->i_op->create(idmap, dir, dentry, mode);
- if (!error)
- fsnotify_create(dir, dentry);
- return error;
+
+ return vfs_create_no_perm(idmap, dentry, mode, di);
}
EXPORT_SYMBOL(vfs_create);
@@ -4314,21 +4348,41 @@ static inline int open_to_namei_flags(int flag)
static int may_o_create(struct mnt_idmap *idmap,
const struct path *dir, struct dentry *dentry,
- umode_t mode)
+ int open_flag, umode_t mode)
{
- int error = security_path_mknod(dir, dentry, mode, 0);
+ struct inode *dir_inode = dir->dentry->d_inode;
+ bool create_dir = O_IS_MKDIR(open_flag);
+ int error;
+
+ if (create_dir)
+ error = security_path_mkdir(dir, dentry, mode);
+ else
+ error = security_path_mknod(dir, dentry, mode, 0);
if (error)
return error;
if (!fsuidgid_has_mapping(dir->dentry->d_sb, idmap))
return -EOVERFLOW;
- error = inode_permission(idmap, dir->dentry->d_inode,
- MAY_WRITE | MAY_EXEC);
+ error = inode_permission(idmap, dir_inode, MAY_WRITE | MAY_EXEC);
if (error)
return error;
- return security_inode_create(dir->dentry->d_inode, dentry, mode);
+ if (create_dir)
+ error = security_inode_mkdir(dir_inode, dentry, mode);
+ else
+ error = security_inode_create(dir_inode, dentry, mode);
+
+ return error;
+}
+
+static inline umode_t o_create_mode(struct mnt_idmap *idmap,
+ const struct inode *dir, int open_flag, umode_t mode)
+{
+ if (O_IS_MKDIR(open_flag))
+ return vfs_prepare_mode(idmap, dir, mode, S_IRWXUGO | S_ISVTX, S_IFDIR);
+ else
+ return vfs_prepare_mode(idmap, dir, mode, S_IALLUGO, S_IFREG);
}
/**
@@ -4364,8 +4418,9 @@ static struct dentry *atomic_open(const struct path *path, struct dentry *dentry
file->__f_path.dentry = DENTRY_NOT_SET;
file->__f_path.mnt = path->mnt;
+
error = dir_inode->i_op->atomic_open(dir_inode, dentry, file,
- open_to_namei_flags(open_flag), mode);
+ open_to_namei_flags(open_flag), mode);
d_lookup_done(dentry);
if (!error) {
@@ -4410,10 +4465,21 @@ static struct dentry *atomic_open(const struct path *path, struct dentry *dentry
}
dput(dentry);
dentry = ERR_PTR(error);
+ } else {
+ if (file->f_mode & FMODE_CREATED)
+ fsnotify_create(dir_inode, dentry);
+ if (file->f_mode & FMODE_OPENED)
+ fsnotify_open(file);
}
+
+
return dentry;
}
+static inline
+struct dentry *vfs_mkdir_no_perm(struct mnt_idmap *, struct inode *,
+ struct dentry *, umode_t,
+ struct delegated_inode *);
/*
* Look up and maybe create and open the last component.
*
@@ -4435,6 +4501,7 @@ static struct dentry *lookup_open(struct nameidata *nd, struct file *file,
struct mnt_idmap *idmap;
struct dentry *dir = nd->path.dentry;
struct inode *dir_inode = dir->d_inode;
+ bool create_dir = O_IS_MKDIR(op->mode);
int open_flag;
struct dentry *dentry;
int error, create_error;
@@ -4455,7 +4522,7 @@ retry:
*/
}
if (open_flag & O_CREAT)
- inode_lock(dir_inode);
+ inode_lock_nested(dir_inode, I_MUTEX_PARENT);
else
inode_lock_shared(dir_inode);
@@ -4507,12 +4574,17 @@ retry:
if (open_flag & O_CREAT) {
if (open_flag & O_EXCL)
open_flag &= ~O_TRUNC;
- mode = vfs_prepare_mode(idmap, dir_inode, mode, mode, mode);
+ mode = o_create_mode(idmap, dir_inode, open_flag, mode);
if (likely(got_write))
create_error = may_o_create(idmap, &nd->path,
- dentry, mode);
+ dentry, open_flag, mode);
else
create_error = -EROFS;
+ /* Refuse to create a directory through a dangling (trailing)
+ * symlink. For regular files this has been allowed historically
+ * on O_CREAT without O_EXCL. */
+ if (unlikely(nd->depth) && create_dir && !create_error)
+ create_error = -EEXIST;
}
if (create_error)
open_flag &= ~O_CREAT;
@@ -4537,6 +4609,7 @@ retry:
dentry = res;
}
}
+
if (dentry->d_inode || !(op->open_flag & O_CREAT)) {
/*
* No need to create a file. If lookup returned a positive
@@ -4554,26 +4627,26 @@ retry:
goto out_dput;
}
- error = try_break_deleg(dir_inode, LEASE_BREAK_DIR_CREATE, &delegated_inode);
- if (error)
- goto out_dput;
-
- file->f_mode |= FMODE_CREATED;
- if (!dir_inode->i_op->create) {
+ if ((create_dir && !dir_inode->i_op->mkdir)
+ || (!create_dir && !dir_inode->i_op->create)) {
error = -EACCES;
goto out_dput;
}
- error = dir_inode->i_op->create(idmap, dir_inode, dentry, mode);
+ if (create_dir) {
+ struct dentry *res = vfs_mkdir_no_perm(idmap, dir_inode, dentry,
+ mode, &delegated_inode);
+ error = PTR_ERR_OR_ZERO(res);
+ if (!error)
+ dentry = res;
+ } else {
+ error = vfs_create_no_perm(idmap, dentry, mode, &delegated_inode);
+ }
if (error)
goto out_dput;
+
+ file->f_mode |= FMODE_CREATED;
out:
- if (!IS_ERR(dentry)) {
- if (file->f_mode & FMODE_CREATED)
- fsnotify_create(dir_inode, dentry);
- if (file->f_mode & FMODE_OPENED)
- fsnotify_open(file);
- }
if ((open_flag & O_CREAT) || create_error)
inode_unlock(dir_inode);
else
@@ -4695,17 +4768,12 @@ struct file *vfs_lookup_open(struct path *parent, struct qstr *last,
}
EXPORT_SYMBOL_FOR_MODULES(vfs_lookup_open, "nfsd");
-static inline bool trailing_slashes(struct nameidata *nd)
-{
- return (bool)nd->last.name[nd->last.len];
-}
-
static struct dentry *lookup_fast_for_open(struct nameidata *nd, int open_flag)
{
struct dentry *dentry;
if (open_flag & O_CREAT) {
- if (trailing_slashes(nd))
+ if (trailing_slashes(&nd->last) && !(open_flag & O_DIRECTORY))
return ERR_PTR(-EISDIR);
/* Don't bother on an O_EXCL create */
@@ -4713,7 +4781,7 @@ static struct dentry *lookup_fast_for_open(struct nameidata *nd, int open_flag)
return NULL;
}
- if (trailing_slashes(nd))
+ if (trailing_slashes(&nd->last))
nd->flags |= LOOKUP_FOLLOW | LOOKUP_DIRECTORY;
dentry = lookup_fast(nd);
@@ -4789,6 +4857,7 @@ finish_lookup:
static int do_open(struct nameidata *nd,
struct file *file, const struct open_flags *op)
{
+ struct vfsmount *mnt;
struct mnt_idmap *idmap;
int open_flag = op->open_flag;
bool do_truncate;
@@ -4806,8 +4875,9 @@ static int do_open(struct nameidata *nd,
if (open_flag & O_CREAT) {
if ((open_flag & O_EXCL) && !(file->f_mode & FMODE_CREATED))
return -EEXIST;
- if (d_is_dir(nd->path.dentry))
+ if (!(open_flag & O_DIRECTORY) && d_is_dir(nd->path.dentry))
return -EISDIR;
+
error = may_create_in_sticky(idmap, nd,
d_backing_inode(nd->path.dentry));
if (unlikely(error))
@@ -4830,11 +4900,17 @@ static int do_open(struct nameidata *nd,
error = mnt_want_write(nd->path.mnt);
if (error)
return error;
+ /*
+ * A dedicated reference is needed because after the call to
+ * vfs_open_consume() we no longer own the reference in nd->path.mnt
+ * while we need to undo write acess below.
+ */
+ mnt = mntget(nd->path.mnt);
do_truncate = true;
}
error = may_open(idmap, &nd->path, acc_mode, open_flag);
if (!error && !(file->f_mode & FMODE_OPENED))
- error = vfs_open(&nd->path, file);
+ error = vfs_open_consume(&nd->path, file);
if (!error)
error = security_file_post_open(file, op->acc_mode);
if (!error && do_truncate)
@@ -4843,8 +4919,10 @@ static int do_open(struct nameidata *nd,
WARN_ON(1);
error = -EINVAL;
}
- if (do_truncate)
- mnt_drop_write(nd->path.mnt);
+ if (do_truncate) {
+ mnt_drop_write(mnt);
+ mntput(mnt);
+ }
return error;
}
@@ -5087,7 +5165,7 @@ static struct dentry *filename_create(int dfd, struct filename *name,
* Do the final lookup. Suppress 'create' if there is a trailing
* '/', and a directory wasn't requested.
*/
- if (last.name[last.len] && !want_dir)
+ if (trailing_slashes(&last) && !want_dir)
create_flags &= ~LOOKUP_CREATE;
dentry = start_dirop(path->dentry, &last, reval_flag | create_flags);
if (IS_ERR(dentry))
@@ -5182,7 +5260,7 @@ struct file *dentry_create(struct path *path, int flags, umode_t mode,
path->dentry = dir;
mode = vfs_prepare_mode(idmap, dir_inode, mode, S_IALLUGO, S_IFREG);
- create_error = may_o_create(idmap, path, dentry, mode);
+ create_error = may_o_create(idmap, path, dentry, flags, mode);
if (create_error)
flags &= ~O_CREAT;
@@ -5356,6 +5434,34 @@ SYSCALL_DEFINE3(mknod, const char __user *, filename, umode_t, mode, unsigned, d
return filename_mknodat(AT_FDCWD, name, mode, dev);
}
+/* Returns the dentry to use (not NULL) or -E on error */
+static inline
+struct dentry *vfs_mkdir_no_perm(struct mnt_idmap *idmap, struct inode *dir,
+ struct dentry *dentry, umode_t mode,
+ struct delegated_inode *di)
+{
+ int error;
+ struct dentry *de;
+ unsigned max_links = dir->i_sb->s_max_links;
+
+ if (max_links && dir->i_nlink >= max_links)
+ return ERR_PTR(-EMLINK);
+
+ error = try_break_deleg(dir, LEASE_BREAK_DIR_CREATE, di);
+ if (error)
+ return ERR_PTR(error);
+
+ de = dir->i_op->mkdir(idmap, dir, dentry, mode);
+ if (IS_ERR(de))
+ return de;
+ if (de) {
+ dput(dentry);
+ dentry = de;
+ }
+ fsnotify_mkdir(dir, dentry);
+ return dentry;
+}
+
/**
* vfs_mkdir - create directory returning correct dentry if possible
* @idmap: idmap of the mount the inode was found from
@@ -5383,7 +5489,6 @@ struct dentry *vfs_mkdir(struct mnt_idmap *idmap, struct inode *dir,
struct delegated_inode *delegated_inode)
{
int error;
- unsigned max_links = dir->i_sb->s_max_links;
struct dentry *de;
error = may_create_dentry(idmap, dir, dentry);
@@ -5399,24 +5504,12 @@ struct dentry *vfs_mkdir(struct mnt_idmap *idmap, struct inode *dir,
if (error)
goto err;
- error = -EMLINK;
- if (max_links && dir->i_nlink >= max_links)
+ de = vfs_mkdir_no_perm(idmap, dir, dentry, mode, delegated_inode);
+ if (IS_ERR(de)) {
+ error = PTR_ERR(de);
goto err;
-
- error = try_break_deleg(dir, LEASE_BREAK_DIR_CREATE, delegated_inode);
- if (error)
- goto err;
-
- de = dir->i_op->mkdir(idmap, dir, dentry, mode);
- error = PTR_ERR(de);
- if (IS_ERR(de))
- goto err;
- if (de) {
- dput(dentry);
- dentry = de;
}
- fsnotify_mkdir(dir, dentry);
- return dentry;
+ return de;
err:
end_creating(dentry);
@@ -5703,7 +5796,7 @@ retry_deleg:
goto exit_drop_write;
/* Why not before? Because we want correct error value */
- if (unlikely(last.name[last.len])) {
+ if (unlikely(trailing_slashes(&last))) {
if (d_is_dir(dentry))
error = -EISDIR;
else
@@ -6305,16 +6398,16 @@ retry_deleg:
if (flags & RENAME_EXCHANGE) {
if (!d_is_dir(rd.new_dentry)) {
error = -ENOTDIR;
- if (new_last.name[new_last.len])
+ if (trailing_slashes(&new_last))
goto exit_unlock;
}
}
/* unless the source is a directory trailing slashes give -ENOTDIR */
if (!d_is_dir(rd.old_dentry)) {
error = -ENOTDIR;
- if (old_last.name[old_last.len])
+ if (trailing_slashes(&old_last))
goto exit_unlock;
- if (!(flags & RENAME_EXCHANGE) && new_last.name[new_last.len])
+ if (!(flags & RENAME_EXCHANGE) && trailing_slashes(&new_last))
goto exit_unlock;
}
diff --git a/fs/nfs/dir.c b/fs/nfs/dir.c
index 49394123bd09..f01f9013202d 100644
--- a/fs/nfs/dir.c
+++ b/fs/nfs/dir.c
@@ -2121,6 +2121,9 @@ int nfs_atomic_open(struct inode *dir, struct dentry *dentry,
dfprintk(VFS, "NFS: atomic_open(%s/%llu), %pd\n",
dir->i_sb->s_id, dir->i_ino, dentry);
+ if (O_IS_MKDIR(open_flags))
+ open_flags &= ~O_CREAT;
+
err = nfs_check_flags(open_flags);
if (err)
return err;
@@ -2317,6 +2320,9 @@ int nfs_atomic_open_v23(struct inode *dir, struct dentry *dentry,
*/
int error = 0;
+ if (O_IS_MKDIR(open_flags))
+ open_flags &= ~O_CREAT;
+
if (dentry->d_name.len > NFS_SERVER(dir)->namelen)
return -ENAMETOOLONG;
diff --git a/fs/open.c b/fs/open.c
index 6b1c14e684a9..a84b55301719 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -931,6 +931,11 @@ cleanup_inode:
return error;
}
+/*
+ * Populate struct file
+ *
+ * NOTE: it assumes f_path is populated and consumes the caller's reference.
+ */
static int do_dentry_open(struct file *f,
int (*open)(struct inode *, struct file *))
{
@@ -938,7 +943,6 @@ static int do_dentry_open(struct file *f,
struct inode *inode = f->f_path.dentry->d_inode;
int error;
- path_get(&f->f_path);
f->f_inode = inode;
f->f_mapping = inode->i_mapping;
f->f_wb_err = filemap_sample_wb_err(f->f_mapping);
@@ -1055,6 +1059,7 @@ int finish_open(struct file *file, struct dentry *dentry,
BUG_ON(file->f_mode & FMODE_OPENED); /* once it's opened, it's opened */
file->__f_path.dentry = dentry;
+ path_get(&file->f_path);
return do_dentry_open(file, open);
}
EXPORT_SYMBOL(finish_open);
@@ -1098,6 +1103,7 @@ int vfs_open(const struct path *path, struct file *file)
int ret;
file->__f_path = *path;
+ path_get(&file->f_path);
ret = do_dentry_open(file, NULL);
if (!ret) {
/*
@@ -1110,6 +1116,25 @@ int vfs_open(const struct path *path, struct file *file)
return ret;
}
+/**
+ * vfs_open_consume - open the file at the given path and consume the reference
+ * @path: path to open
+ * @file: newly allocated file with f_flag initialized
+ */
+int vfs_open_consume(struct path *path, struct file *file)
+{
+ int ret;
+
+ file->__f_path = *path;
+ path->mnt = NULL;
+ path->dentry = NULL;
+ ret = do_dentry_open(file, NULL);
+ if (!ret) {
+ fsnotify_open(file);
+ }
+ return ret;
+}
+
struct file *dentry_open(const struct path *path, int flags,
const struct cred *cred)
{
@@ -1239,29 +1264,30 @@ inline int build_open_flags(const struct open_how *how, struct open_flags *op)
if (WILL_CREATE(flags)) {
if (how->mode & ~S_IALLUGO)
return -EINVAL;
- op->mode = how->mode | S_IFREG;
+ if (O_IS_MKDIR(flags))
+ op->mode = how->mode | S_IFDIR;
+ else
+ op->mode = how->mode | S_IFREG;
} else {
if (how->mode != 0)
return -EINVAL;
op->mode = 0;
}
- /*
- * Block bugs where O_DIRECTORY | O_CREAT created regular files.
- * Note, that blocking O_DIRECTORY | O_CREAT here also protects
- * O_TMPFILE below which requires O_DIRECTORY being raised.
- */
- if ((flags & (O_DIRECTORY | O_CREAT)) == (O_DIRECTORY | O_CREAT))
- return -EINVAL;
-
/* Now handle the creative implementation of O_TMPFILE. */
if (flags & __O_TMPFILE) {
/*
* In order to ensure programs get explicit errors when trying
* to use O_TMPFILE on old kernels we enforce that O_DIRECTORY
- * is raised alongside __O_TMPFILE.
+ * is raised alongside __O_TMPFILE, but without O_CREAT. The
+ * reason for disallowing O_CREAT|O_TMPFILE is that
+ * O_DIRECTORY|O_CREAT used to work and created a regular file
+ * if nothing existed at the open path. Hence, allowing the
+ * combination would have caused O_CREAT|O_TMPFILE to create a
+ * regular (non-temporary) file on old kernels, while the caller
+ * would believe they created an actual O_TMPFILE.
*/
- if (!(flags & O_DIRECTORY))
+ if (!(flags & O_DIRECTORY) || (flags & O_CREAT))
return -EINVAL;
if (!(acc_mode & MAY_WRITE))
return -EINVAL;
@@ -1318,6 +1344,15 @@ inline int build_open_flags(const struct open_how *how, struct open_flags *op)
op->intent = flags & O_PATH ? 0 : LOOKUP_OPEN;
+ /*
+ * Requesting write access on a directory can never succeed. Rather
+ * than performing a path-walk to determine whether the target is
+ * actually a directory (-EISDIR) or not (-ENOTDIR), we short-circuit
+ * to -ENOTDIR.
+ */
+ if ((flags & O_DIRECTORY) && !(flags & __O_TMPFILE) && (acc_mode & MAY_WRITE))
+ return -ENOTDIR;
+
if (flags & O_CREAT) {
op->intent |= LOOKUP_CREATE;
if (flags & O_EXCL) {
diff --git a/fs/smb/client/dir.c b/fs/smb/client/dir.c
index 6fa6d48fdfd3..f44defb64354 100644
--- a/fs/smb/client/dir.c
+++ b/fs/smb/client/dir.c
@@ -534,6 +534,9 @@ int cifs_atomic_open(struct inode *dir, struct dentry *direntry,
if (unlikely(cifs_forced_shutdown(cifs_sb)))
return smb_EIO(smb_eio_trace_forced_shutdown);
+ if (O_IS_MKDIR(oflags))
+ oflags &= ~O_CREAT;
+
/*
* Posix open is only called (at lookup time) for file create now. For
* opens (rather than creates), because we do not know if it is a file
diff --git a/fs/vboxsf/dir.c b/fs/vboxsf/dir.c
index 0b9eab157432..6e306ddd722b 100644
--- a/fs/vboxsf/dir.c
+++ b/fs/vboxsf/dir.c
@@ -318,6 +318,9 @@ static int vboxsf_dir_atomic_open(struct inode *parent, struct dentry *dentry,
u64 handle;
int err;
+ if (O_IS_MKDIR(flags))
+ flags &= ~O_CREAT;
+
if (d_in_lookup(dentry)) {
struct dentry *res = vboxsf_dir_lookup(parent, dentry, 0);
if (res || d_really_is_positive(dentry))
diff --git a/include/linux/fcntl.h b/include/linux/fcntl.h
index 6ad6b9e7a226..204e16bbe263 100644
--- a/include/linux/fcntl.h
+++ b/include/linux/fcntl.h
@@ -30,6 +30,12 @@
*/
#define __O_REGULAR (1 << 30)
+#define O_MKDIR_MASK (O_CREAT | O_DIRECTORY)
+static inline bool O_IS_MKDIR(unsigned int flags)
+{
+ return (flags & O_MKDIR_MASK) == O_MKDIR_MASK;
+}
+
/* List of all valid flags for the how->resolve argument: */
#define VALID_RESOLVE_FLAGS \
(RESOLVE_NO_XDEV | RESOLVE_NO_MAGICLINKS | RESOLVE_NO_SYMLINKS | \
diff --git a/tools/testing/selftests/filesystems/.gitignore b/tools/testing/selftests/filesystems/.gitignore
index 9eb185fb2f9d..01c588d4c84f 100644
--- a/tools/testing/selftests/filesystems/.gitignore
+++ b/tools/testing/selftests/filesystems/.gitignore
@@ -1,4 +1,5 @@
# SPDX-License-Identifier: GPL-2.0-only
+open_o_creat_o_dir
dnotify_test
devpts_pts
fclog
diff --git a/tools/testing/selftests/filesystems/Makefile b/tools/testing/selftests/filesystems/Makefile
index 03be337c1f35..0959bd26875a 100644
--- a/tools/testing/selftests/filesystems/Makefile
+++ b/tools/testing/selftests/filesystems/Makefile
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: GPL-2.0
CFLAGS += $(KHDR_INCLUDES)
-TEST_GEN_PROGS := devpts_pts file_stressor anon_inode_test kernfs_test fclog ustat_test
+TEST_GEN_PROGS := open_o_creat_o_dir devpts_pts file_stressor anon_inode_test kernfs_test fclog ustat_test
TEST_GEN_PROGS += idmapped_tmpfile
TEST_GEN_PROGS_EXTENDED := dnotify_test
diff --git a/tools/testing/selftests/filesystems/open_o_creat_o_dir.c b/tools/testing/selftests/filesystems/open_o_creat_o_dir.c
new file mode 100644
index 000000000000..be0ab34267e1
--- /dev/null
+++ b/tools/testing/selftests/filesystems/open_o_creat_o_dir.c
@@ -0,0 +1,201 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <sys/stat.h>
+#include <errno.h>
+#include <limits.h>
+#include <fcntl.h>
+
+#include "kselftest_harness.h"
+#include "wrappers.h"
+
+#define openat_o_mkdir_checked_flags(dfd, pathname, flags) ({ \
+ struct stat __st; \
+ int __fd = openat_o_mkdir(dfd, pathname, flags, S_IRWXU); \
+ ASSERT_GE(__fd, 0); \
+ ASSERT_EQ(fstat(__fd, &__st), 0); \
+ EXPECT_TRUE(S_ISDIR(__st.st_mode)); \
+ __fd; \
+})
+
+#define openat_o_mkdir_checked(dfd, pathname) \
+ openat_o_mkdir_checked_flags(dfd, pathname, O_RDONLY)
+
+FIXTURE(open_o_creat_o_dir) {
+ char dirpath[PATH_MAX];
+ int dfd;
+};
+
+FIXTURE_SETUP(open_o_creat_o_dir)
+{
+ strcpy(self->dirpath, "/tmp/open_o_creat_o_dir_test.XXXXXX");
+ ASSERT_NE(mkdtemp(self->dirpath), NULL);
+ self->dfd = open(self->dirpath, O_DIRECTORY);
+ ASSERT_GE(self->dfd, 0);
+}
+
+FIXTURE_TEARDOWN(open_o_creat_o_dir)
+{
+ close(self->dfd);
+ rmdir(self->dirpath);
+}
+
+/* Does open_o_creat_o_dir return a fd at all? */
+TEST_F(open_o_creat_o_dir, returns_fd)
+{
+ int fd = openat_o_mkdir_checked(self->dfd, "newdir");
+ EXPECT_EQ(close(fd), 0);
+ EXPECT_EQ(unlinkat(self->dfd, "newdir", AT_REMOVEDIR), 0);
+}
+
+/* The fd must refer to the directory that was just created. */
+TEST_F(open_o_creat_o_dir, fd_is_created_dir)
+{
+ int fd;
+ struct stat st_via_fd, st_via_path;
+ char path[PATH_MAX];
+
+ fd = openat_o_mkdir_checked(self->dfd, "checkdir");
+
+ ASSERT_EQ(fstat(fd, &st_via_fd), 0);
+
+ snprintf(path, sizeof(path), "%s/checkdir", self->dirpath);
+ ASSERT_EQ(stat(path, &st_via_path), 0);
+
+ EXPECT_EQ(st_via_fd.st_ino, st_via_path.st_ino);
+ EXPECT_EQ(st_via_fd.st_dev, st_via_path.st_dev);
+
+ EXPECT_EQ(close(fd), 0);
+ EXPECT_EQ(rmdir(path), 0);
+}
+
+/* Missing parent component must fail with ENOENT. */
+TEST_F(open_o_creat_o_dir, enoent_missing_parent)
+{
+ EXPECT_EQ(openat_o_mkdir(self->dfd, "nonexistent/child", O_RDONLY, S_IRWXU), -1);
+ EXPECT_EQ(errno, ENOENT);
+}
+
+/* An invalid dfd must fail with EBADF. */
+TEST_F(open_o_creat_o_dir, ebadf)
+{
+ EXPECT_EQ(openat_o_mkdir(FD_INVALID, "badfdir", O_RDONLY, S_IRWXU), -1);
+ EXPECT_EQ(errno, EBADF);
+}
+
+/* A dfd that points to a file (not a directory) must fail with ENOTDIR. */
+TEST_F(open_o_creat_o_dir, enotdir_dfd)
+{
+ int file_fd;
+
+ file_fd = openat(self->dfd, "file",
+ O_CREAT | O_RDONLY, S_IRWXU);
+ ASSERT_GE(file_fd, 0);
+
+ EXPECT_EQ(openat_o_mkdir(file_fd, "subdir", O_RDONLY, S_IRWXU), -1);
+ EXPECT_EQ(errno, ENOTDIR);
+
+ EXPECT_EQ(close(file_fd), 0);
+ EXPECT_EQ(unlinkat(self->dfd, "file", 0), 0);
+}
+
+/*
+ * O_EXCL together with O_CREAT|O_DIRECTORY should succeed if the target
+ * directory does not yet exist. After directory creation, repeating this
+ * call must fail with EEXIST.
+ */
+TEST_F(open_o_creat_o_dir, o_excl_eexist)
+{
+ int excldir_fd;
+
+ excldir_fd = openat_o_mkdir_checked_flags(self->dfd, "excldir", O_EXCL);
+
+ EXPECT_EQ(openat_o_mkdir(excldir_fd, ".", O_EXCL, S_IRWXU), -1);
+ EXPECT_EQ(errno, EEXIST);
+
+ EXPECT_EQ(close(excldir_fd), 0);
+ EXPECT_EQ(unlinkat(self->dfd, "excldir", AT_REMOVEDIR), 0);
+}
+
+/*
+ * O_CREAT|O_DIRECTORY on a path that already exists as a regular file
+ * must fail with ENOTDIR.
+ */
+TEST_F(open_o_creat_o_dir, existing_file_enotdir)
+{
+ int file_fd;
+
+ file_fd = openat(self->dfd, "regfile",
+ O_CREAT | O_RDONLY, S_IRWXU);
+ ASSERT_GE(file_fd, 0);
+ EXPECT_EQ(close(file_fd), 0);
+
+ EXPECT_EQ(openat_o_mkdir(self->dfd, "regfile", O_RDONLY, S_IRWXU), -1);
+ EXPECT_EQ(errno, ENOTDIR);
+
+ EXPECT_EQ(unlinkat(self->dfd, "regfile", 0), 0);
+}
+
+/*
+ * O_CREAT|O_DIRECTORY combined with a writable access mode must be
+ * rejected: a directory cannot be opened for writing.
+ */
+TEST_F(open_o_creat_o_dir, rejects_writable_acc_mode)
+{
+ EXPECT_EQ(openat_o_mkdir(self->dfd, "rdwrdir", O_RDWR, S_IRWXU), -1);
+ EXPECT_EQ(errno, ENOTDIR);
+ /* Clean up if the kernel created the directory anyway. */
+ unlinkat(self->dfd, "rdwrdir", AT_REMOVEDIR);
+}
+
+/*
+ * openat(O_CREAT|O_DIRECTORY) with a trailing slash should work.
+ */
+TEST_F(open_o_creat_o_dir, trailing_slash)
+{
+ int fd = openat_o_mkdir_checked(self->dfd, "newdir/");
+ EXPECT_EQ(close(fd), 0);
+ EXPECT_EQ(unlinkat(self->dfd, "newdir", AT_REMOVEDIR), 0);
+}
+
+/*
+ * openat(O_CREAT) with a trailing slash but without O_DIRECTORY
+ * must fail with EISDIR and must not create anything at the path.
+ */
+TEST_F(open_o_creat_o_dir, trailing_slash_no_o_dir)
+{
+ int fd;
+ struct stat st;
+
+ fd = openat(self->dfd, "trailing/", O_CREAT | O_RDONLY, S_IRWXU);
+ EXPECT_EQ(fd, -1);
+ EXPECT_EQ(errno, EISDIR);
+
+ EXPECT_EQ(fstatat(self->dfd, "trailing", &st, 0), -1);
+ EXPECT_EQ(errno, ENOENT);
+
+ /* Best-effort cleanup in case the kernel left a file behind. */
+ if (fd >= 0)
+ close(fd);
+ unlinkat(self->dfd, "trailing", 0);
+}
+
+/*
+ * The returned fd must be usable as a dfd for further *at() calls.
+ */
+TEST_F(open_o_creat_o_dir, fd_usable_as_dfd)
+{
+ int parent_fd, child_fd;
+ char path[PATH_MAX];
+
+ parent_fd = openat_o_mkdir_checked(self->dfd, "parent");
+ child_fd = openat_o_mkdir_checked(parent_fd, "child");
+
+ EXPECT_EQ(close(child_fd), 0);
+ EXPECT_EQ(close(parent_fd), 0);
+
+ snprintf(path, sizeof(path), "%s/parent/child", self->dirpath);
+ EXPECT_EQ(rmdir(path), 0);
+ snprintf(path, sizeof(path), "%s/parent", self->dirpath);
+ EXPECT_EQ(rmdir(path), 0);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/wrappers.h b/tools/testing/selftests/filesystems/wrappers.h
index 420ae4f908cf..abe5b85cebdc 100644
--- a/tools/testing/selftests/filesystems/wrappers.h
+++ b/tools/testing/selftests/filesystems/wrappers.h
@@ -13,6 +13,10 @@
#define STATX_MNT_ID_UNIQUE 0x00004000U /* Want/got extended stx_mount_id */
#endif
+#ifndef FD_INVALID
+#define FD_INVALID -10009
+#endif
+
static inline int sys_fsopen(const char *fsname, unsigned int flags)
{
return syscall(__NR_fsopen, fsname, flags);
@@ -105,4 +109,11 @@ static inline int sys_open_tree(int dfd, const char *filename, unsigned int flag
return syscall(__NR_open_tree, dfd, filename, flags);
}
+static inline int openat_o_mkdir(int dfd, const char *pathname,
+ unsigned int flags, mode_t mode)
+{
+ return syscall(__NR_openat, dfd, pathname,
+ flags | O_DIRECTORY | O_CREAT, mode);
+}
+
#endif