From nobody Mon May 25 04:35:26 2026 Received: from ewsoutbound.kpnmail.nl (ewsoutbound.kpnmail.nl [195.121.94.170]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 7EADA494A00 for ; Mon, 18 May 2026 16:52:53 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=195.121.94.170 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1779123175; cv=none; b=acPtASfsKLJJz6VdbvuEc/KL2xrQN0GFunlNKhWQqEFBWlduEQjXZJi7cnoLEvxLLHIvJ7oSw7CzOmN3h6/xmScmRuu9FZImy0lLLUBifWWCY/6wQUVmhB4L0pdmHW4256DQNTcxDANuoVSVWn/vDOyggXF6GoH8fMz/cUhW8yU= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1779123175; c=relaxed/simple; bh=nw/RolQjIyBvd1xt/1XaDApOpdU0y3npyeRM6T1PR44=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=lTBeJ3Nm1SfISjzkQ01NaMgChoQ7fd2gaEa7M+E9p8RMaL1EehZdLeDQc1XMt5zHhPvJX5PfNqbcfsMGxCbtliRSaigboU6HANE+tNsFuOTH7MaWX4svSMow7m3lqWAq1o7Vo7DYw0tEHCXYu7MH3T5NKovidH6jrZ8c1fHUMHw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=xs4all.nl; spf=pass smtp.mailfrom=xs4all.nl; dkim=pass (2048-bit key) header.d=xs4all.nl header.i=@xs4all.nl header.b=l0FkQd8/; arc=none smtp.client-ip=195.121.94.170 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=xs4all.nl Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=xs4all.nl Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=xs4all.nl header.i=@xs4all.nl header.b="l0FkQd8/" X-KPN-MessageId: da29dd43-52d9-11f1-8aa3-005056ab378f Received: from smtp.kpnmail.nl (unknown [10.31.155.39]) by ewsoutbound.so.kpn.org (Halon) with ESMTPS id da29dd43-52d9-11f1-8aa3-005056ab378f; Mon, 18 May 2026 18:51:44 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=xs4all.nl; s=xs4all01; h=mime-version:message-id:date:subject:to:from; bh=YmNz6PCNED8niV5po3A0S/+XAtyMuhNizpv57+y4j6E=; b=l0FkQd8/bME0zr7VBCnpr/cZJmxqSEqkmultkZYZF4Sj0wVp+Bt8HFWoklt8PesoUGHqEVOp6meBy gSyc1i9JAthx5RKwQ0XijLiVAiymY69sizDMHRmBi0fYKJ4KkJs0X4/dyqKLttYm4yM91+gubl8jXZ Hb/5vr2mu61o02lvtXdzC16ID8UPF+rMHTFahXzYDF2BNjbveQ+tW7N1MHZz9g1ZGJzPhUoHGY6Zpb NZlE582QXCguCGoWf11l+/aIq6XT9G7i++oic2HyIVrb2uDIaA1wtPneHehyBOtFdHUsXSxL1a5/dD 62bhsptaevGJhMx/zaO+nor6L7wryvA== X-KPN-MID: 33|ddaHYbaA2GZjUyOdzlZQZY5Tac8dZCTunQfymfZQGAEcmkUfdioZr8rpnW7wHEo ozR7Ka7EC3s+T7UcBpPBXGMLlbFWsiAdnIpIvA4Z2sGw= X-KPN-VerifiedSender: Yes X-CMASSUN: 33|vrDSk1fFEd+IAl8pM1SeiR9CF9iCQPQZyw4gPnzEW7h4cq9kmDBMaQS0QOq5NU0 1veRjYu5h3qEsmJ8jrDP61w== Received: from daedalus.home (unknown [178.227.141.244]) by smtp.xs4all.nl (Halon) with ESMTPSA id d9dce838-52d9-11f1-8011-005056ab7447; Mon, 18 May 2026 18:51:44 +0200 (CEST) From: Jori Koolstra To: Alexander Viro , Christian Brauner , Jan Kara , Aleksa Sarai Cc: Jori Koolstra , linux-kernel@vger.kernel.org, linux-fsdevel@vger.kernel.org, cmirabil@redhat.com, Jori Koolstra Subject: [RFC PATCH v4 1/2] vfs: add O_CREAT|O_DIRECTORY to open*(2) Date: Mon, 18 May 2026 18:52:36 +0200 Message-ID: <20260518165237.2084042-2-jkoolstra@xs4all.nl> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260518165237.2084042-1-jkoolstra@xs4all.nl> References: <20260518165237.2084042-1-jkoolstra@xs4all.nl> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Currently there is no way to race-freely create and open a directory. For regular files we have open(O_CREAT) for creating a new file inode, and returning a pinning fd to it. The lack of such functionality for directories means that when populating a directory tree there's always a race involved: the inodes first need to be created, and then opened to adjust their permissions/ownership/labels/timestamps/acls/xattrs/..., but in the time window between the creation and the opening they might be replaced by something else. Addressing this race without proper APIs is possible (by immediately fstat()ing what was opened, to verify that it has the right inode type), but difficult to get right. Hence, adding support for a new flag combo O_CREAT|O_DIRECTORY to open*(2) that creates a directory (if it does not exist already) and returns an O_DIRECTORY fd is very useful. Historically, the O_CREAT|O_DIRECTORY behaviour was to return ENOTDIR if a regular file exists at the open path; EISDIR if a directory exists at the path; and to create a regular file if no file exists at the path. This behaviour changed accidentally with 973d4b73fbaf ("do_last(): rejoin the common path even earlier in FMODE_{OPENED,CREATED} case") causing ENOTDIR to return in the last case while still creating the file. As this change was not detected for a long time, Brauner proposed to adopt the more consistent NetBSD behaviour, i.e. to return EINVAL on the the O_CREAT|O_DIRECTORY combination. This change was applied in 43b450632676 ("open: return EINVAL for O_DIRECTORY | O_CREAT") in March, 2023. As the EINVAL behaviour has been in the kernel for about 3 year now, no rollback is expected as a result of userspace reliance on old behaviour, leaving us free to reassign the O_CREAT|O_DIRECTORY semantics. This commit also changes the error returned when a filesystem operation is unsupported (i_op->mkdir/creat) to EOPNOTSUPP. Current error values are inconsistent (both EPERM and EACCES are used) and confusing. This feature idea (and some of its description) is taken from the UAPI group: https://github.com/uapi-group/kernel-features?tab=3Dreadme-ov-file#race-fre= e-creation-and-opening-of-non-file-inodes Signed-off-by: Jori Koolstra --- fs/namei.c | 180 +++++++++++++++++++++---------- fs/open.c | 25 +++-- include/uapi/asm-generic/fcntl.h | 2 + 3 files changed, 138 insertions(+), 69 deletions(-) diff --git a/fs/namei.c b/fs/namei.c index c7fac83c9a85..60223278f9ec 100644 --- a/fs/namei.c +++ b/fs/namei.c @@ -2777,9 +2777,14 @@ static const char *path_init(struct nameidata *nd, u= nsigned flags) return s; } =20 +static inline bool trailing_slashes(struct nameidata *nd) +{ + return (bool)nd->last.name[nd->last.len]; +} + static inline const char *lookup_last(struct nameidata *nd) { - if (nd->last_type =3D=3D LAST_NORM && nd->last.name[nd->last.len]) + if (nd->last_type =3D=3D LAST_NORM && trailing_slashes(nd)) nd->flags |=3D LOOKUP_FOLLOW | LOOKUP_DIRECTORY; =20 return walk_component(nd, WALK_TRAILING); @@ -4166,6 +4171,16 @@ static inline umode_t vfs_prepare_mode(struct mnt_id= map *idmap, return mode; } =20 +static int __vfs_create(struct mnt_idmap *idmap, struct dentry *dentry, um= ode_t mode, + struct delegated_inode *di, bool excl) +{ + struct inode *dir =3D d_inode(dentry->d_parent); + int error =3D try_break_deleg(dir, di); + if (error) + return error; + return dir->i_op->create(idmap, dir, dentry, mode, excl); +} + /** * vfs_create - create new file * @idmap: idmap of the mount the inode was found from @@ -4192,16 +4207,14 @@ int vfs_create(struct mnt_idmap *idmap, struct dent= ry *dentry, umode_t mode, return error; =20 if (!dir->i_op->create) - return -EACCES; /* shouldn't it be ENOSYS? */ + return -EOPNOTSUPP; =20 mode =3D vfs_prepare_mode(idmap, dir, mode, S_IALLUGO, S_IFREG); error =3D security_inode_create(dir, dentry, mode); if (error) return error; - error =3D try_break_deleg(dir, di); - if (error) - return error; - error =3D dir->i_op->create(idmap, dir, dentry, mode, true); + + error =3D __vfs_create(idmap, dentry, mode, di, true); if (!error) fsnotify_create(dir, dentry); return error; @@ -4321,21 +4334,32 @@ static inline int open_to_namei_flags(int flag) =20 static int may_o_create(struct mnt_idmap *idmap, const struct path *dir, struct dentry *dentry, - umode_t mode) + umode_t mode, bool create_dir) { - int error =3D security_path_mknod(dir, dentry, mode, 0); + struct inode *dir_inode =3D dir->dentry->d_inode; + int error; + + error =3D create_dir ? security_path_mkdir(dir, dentry, mode) + : security_path_mknod(dir, dentry, mode, 0); if (error) return error; =20 if (!fsuidgid_has_mapping(dir->dentry->d_sb, idmap)) return -EOVERFLOW; =20 - error =3D inode_permission(idmap, dir->dentry->d_inode, - MAY_WRITE | MAY_EXEC); + error =3D inode_permission(idmap, dir_inode, MAY_WRITE | MAY_EXEC); if (error) return error; =20 - return security_inode_create(dir->dentry->d_inode, dentry, mode); + return create_dir ? security_inode_mkdir(dir_inode, dentry, mode) + : security_inode_create(dir_inode, dentry, mode); +} + +static inline umode_t o_create_mode(struct mnt_idmap *idmap, + const struct inode *dir, umode_t mode, bool create_dir) +{ + return create_dir ? vfs_prepare_mode(idmap, dir, mode, S_IRWXUGO | S_ISVT= X, 0) + : vfs_prepare_mode(idmap, dir, mode, S_IALLUGO, S_IFREG); } =20 /* @@ -4359,6 +4383,11 @@ static struct dentry *atomic_open(const struct path = *path, struct dentry *dentry struct inode *dir =3D path->dentry->d_inode; int error; =20 + if ((open_flag & O_MKDIR_MASK) =3D=3D O_MKDIR_MASK) { + error =3D -EOPNOTSUPP; + goto out; + } + file->__f_path.dentry =3D DENTRY_NOT_SET; file->__f_path.mnt =3D path->mnt; error =3D dir->i_op->atomic_open(dir, dentry, file, @@ -4381,6 +4410,7 @@ static struct dentry *atomic_open(const struct path *= path, struct dentry *dentry error =3D -ENOENT; } } +out: if (error) { dput(dentry); dentry =3D ERR_PTR(error); @@ -4388,6 +4418,9 @@ static struct dentry *atomic_open(const struct path *= path, struct dentry *dentry return dentry; } =20 +static struct dentry *__vfs_mkdir(struct mnt_idmap *, struct inode *, + struct dentry *, umode_t, + struct delegated_inode *); /* * Look up and maybe create and open the last component. * @@ -4412,8 +4445,9 @@ static struct dentry *lookup_open(struct nameidata *n= d, struct file *file, struct inode *dir_inode =3D dir->d_inode; int open_flag =3D op->open_flag; struct dentry *dentry; - int error, create_error =3D 0; + int error =3D 0, create_error =3D 0; umode_t mode =3D op->mode; + bool create_dir =3D (open_flag & O_MKDIR_MASK) =3D=3D O_MKDIR_MASK; DECLARE_WAIT_QUEUE_HEAD_ONSTACK(wq); =20 if (unlikely(IS_DEADDIR(dir_inode))) @@ -4462,10 +4496,10 @@ static struct dentry *lookup_open(struct nameidata = *nd, struct file *file, if (open_flag & O_CREAT) { if (open_flag & O_EXCL) open_flag &=3D ~O_TRUNC; - mode =3D vfs_prepare_mode(idmap, dir->d_inode, mode, mode, mode); + mode =3D o_create_mode(idmap, dir_inode, mode, create_dir); if (likely(got_write)) create_error =3D may_o_create(idmap, &nd->path, - dentry, mode); + dentry, mode, create_dir); else create_error =3D -EROFS; } @@ -4494,29 +4528,37 @@ static struct dentry *lookup_open(struct nameidata = *nd, struct file *file, } } =20 + if (unlikely(create_error) && !dentry->d_inode) { + error =3D create_error; + goto out_dput; + } + /* Negative dentry, just create the file */ if (!dentry->d_inode && (open_flag & O_CREAT)) { - /* but break the directory lease first! */ - error =3D try_break_deleg(dir_inode, delegated_inode); - if (error) - goto out_dput; =20 file->f_mode |=3D FMODE_CREATED; audit_inode_child(dir_inode, dentry, AUDIT_TYPE_CHILD_CREATE); - if (!dir_inode->i_op->create) { - error =3D -EACCES; + if ((create_dir && !dir_inode->i_op->mkdir) + || (!create_dir && !dir_inode->i_op->create)) { + error =3D -EOPNOTSUPP; goto out_dput; } =20 - error =3D dir_inode->i_op->create(idmap, dir_inode, dentry, - mode, open_flag & O_EXCL); + if (create_dir) { + struct dentry *res =3D __vfs_mkdir(idmap, dir_inode, dentry, mode, + delegated_inode); + if (IS_ERR(res)) + error =3D PTR_ERR(res); + else + dentry =3D res; + } else { + error =3D __vfs_create(idmap, dentry, mode, delegated_inode, + open_flag & O_EXCL); + } if (error) goto out_dput; } - if (unlikely(create_error) && !dentry->d_inode) { - error =3D create_error; - goto out_dput; - } + return dentry; =20 out_dput: @@ -4524,17 +4566,12 @@ static struct dentry *lookup_open(struct nameidata = *nd, struct file *file, return ERR_PTR(error); } =20 -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; =20 if (open_flag & O_CREAT) { - if (trailing_slashes(nd)) + if (trailing_slashes(nd) && !(open_flag & O_DIRECTORY)) return ERR_PTR(-EISDIR); =20 /* Don't bother on an O_EXCL create */ @@ -4610,8 +4647,12 @@ static const char *open_last_lookups(struct nameidat= a *nd, inode_lock_shared(dir->d_inode); dentry =3D lookup_open(nd, file, op, got_write, &delegated_inode); if (!IS_ERR(dentry)) { - if (file->f_mode & FMODE_CREATED) - fsnotify_create(dir->d_inode, dentry); + if (file->f_mode & FMODE_CREATED) { + if (open_flag & O_DIRECTORY) + fsnotify_mkdir(dir->d_inode, dentry); + else + fsnotify_create(dir->d_inode, dentry); + } if (file->f_mode & FMODE_OPENED) fsnotify_open(file); } @@ -4672,12 +4713,15 @@ 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)) - return -EISDIR; - error =3D may_create_in_sticky(idmap, nd, - d_backing_inode(nd->path.dentry)); - if (unlikely(error)) - return error; + if (!(open_flag & O_DIRECTORY)) { + if (d_is_dir(nd->path.dentry)) + return -EISDIR; + + error =3D may_create_in_sticky(idmap, nd, + d_backing_inode(nd->path.dentry)); + if (unlikely(error)) + return error; + } } if ((nd->flags & LOOKUP_DIRECTORY) && !d_can_lookup(nd->path.dentry)) return -ENOTDIR; @@ -5039,7 +5083,7 @@ struct file *dentry_create(struct path *path, int fla= gs, umode_t mode, path->dentry =3D dir; mode =3D vfs_prepare_mode(idmap, dir_inode, mode, S_IALLUGO, S_IFREG); =20 - create_error =3D may_o_create(idmap, path, dentry, mode); + create_error =3D may_o_create(idmap, path, dentry, mode, false); if (create_error) flags &=3D ~O_CREAT; =20 @@ -5207,6 +5251,37 @@ SYSCALL_DEFINE3(mknod, const char __user *, filename= , umode_t, mode, unsigned, d return filename_mknodat(AT_FDCWD, name, mode, dev); } =20 +static struct dentry *__vfs_mkdir(struct mnt_idmap *idmap, struct inode *d= ir, + struct dentry *dentry, umode_t mode, + struct delegated_inode *di) +{ + int error; + unsigned max_links =3D dir->i_sb->s_max_links; + struct dentry *de; + + error =3D -EMLINK; + if (max_links && dir->i_nlink >=3D max_links) + goto err; + + error =3D try_break_deleg(dir, di); + if (error) + goto err; + + de =3D dir->i_op->mkdir(idmap, dir, dentry, mode); + if (IS_ERR(de)) { + error =3D PTR_ERR(de); + goto err; + } + if (de) { + dput(dentry); + dentry =3D de; + } + return dentry; + +err: + return ERR_PTR(error); +} + /** * vfs_mkdir - create directory returning correct dentry if possible * @idmap: idmap of the mount the inode was found from @@ -5231,17 +5306,16 @@ SYSCALL_DEFINE3(mknod, const char __user *, filenam= e, umode_t, mode, unsigned, d */ struct dentry *vfs_mkdir(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode, - struct delegated_inode *delegated_inode) + struct delegated_inode *di) { int error; - unsigned max_links =3D dir->i_sb->s_max_links; struct dentry *de; =20 error =3D may_create_dentry(idmap, dir, dentry); if (error) goto err; =20 - error =3D -EPERM; + error =3D -EOPNOTSUPP; if (!dir->i_op->mkdir) goto err; =20 @@ -5250,22 +5324,12 @@ struct dentry *vfs_mkdir(struct mnt_idmap *idmap, s= truct inode *dir, if (error) goto err; =20 - error =3D -EMLINK; - if (max_links && dir->i_nlink >=3D max_links) - goto err; - - error =3D try_break_deleg(dir, delegated_inode); - if (error) + de =3D __vfs_mkdir(idmap, dir, dentry, mode, di); + if (IS_ERR(de)) { + error =3D PTR_ERR(de); goto err; - - de =3D dir->i_op->mkdir(idmap, dir, dentry, mode); - error =3D PTR_ERR(de); - if (IS_ERR(de)) - goto err; - if (de) { - dput(dentry); - dentry =3D de; } + dentry =3D de; fsnotify_mkdir(dir, dentry); return dentry; =20 diff --git a/fs/open.c b/fs/open.c index 681d405bc61e..68b694ae1843 100644 --- a/fs/open.c +++ b/fs/open.c @@ -1209,29 +1209,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 =3D how->mode | S_IFREG; + if (flags & O_DIRECTORY) + op->mode =3D how->mode | S_IFDIR; + else + op->mode =3D how->mode | S_IFREG; } else { if (how->mode !=3D 0) return -EINVAL; op->mode =3D 0; } =20 - /* - * 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)) =3D=3D (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; @@ -1268,6 +1269,8 @@ inline int build_open_flags(const struct open_how *ho= w, struct open_flags *op) op->intent =3D flags & O_PATH ? 0 : LOOKUP_OPEN; =20 if (flags & O_CREAT) { + if ((flags & O_DIRECTORY) && (acc_mode & MAY_WRITE)) + return -EISDIR; op->intent |=3D LOOKUP_CREATE; if (flags & O_EXCL) { op->intent |=3D LOOKUP_EXCL; diff --git a/include/uapi/asm-generic/fcntl.h b/include/uapi/asm-generic/fc= ntl.h index 613475285643..40ab8bbe668b 100644 --- a/include/uapi/asm-generic/fcntl.h +++ b/include/uapi/asm-generic/fcntl.h @@ -95,6 +95,8 @@ #define O_NDELAY O_NONBLOCK #endif =20 +#define O_MKDIR_MASK (O_CREAT | O_DIRECTORY) + #define F_DUPFD 0 /* dup */ #define F_GETFD 1 /* get close_on_exec */ #define F_SETFD 2 /* set/clear close_on_exec */ --=20 2.54.0 From nobody Mon May 25 04:35:26 2026 Received: from ewsoutbound.kpnmail.nl (ewsoutbound.kpnmail.nl [195.121.94.167]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C619C3F88A8 for ; Mon, 18 May 2026 16:52:55 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=195.121.94.167 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1779123177; cv=none; b=SRx9kxaNSNdGad+ITv+GnLEnruYwilHll+kZFTW6o03KJ7nz0P0GrER3cOo/ZOM0rq9kBfR3TB4MQMNmhtMt3g5VHfTWshY3nJ/52Cwb9s8gWFP5tzElePoSWYcEfnPFTHE36JLA8ttE17pYXD8Bk89uF/hW/OjkebGGfLesJwg= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1779123177; c=relaxed/simple; bh=cYXb9tzVz/VHufYzBiQa9Tgo040kolVK90OknxNhBRA=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=f4RDytVyWPeU20mUnMjWmMOnZk6QvQDmhB0dFmY+GpkQoThRnGtL6mnesRShcUOqfrCY95D/EP0Sg/ylmeoiuOV8U7hv2rgEJC48ezcK48CClppDQcnn7F1gyaEA2y3I3KBZ9xsfb5CqRFPFXK3rz3x7OAXFliADiSY49bUBBkg= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=xs4all.nl; spf=pass smtp.mailfrom=xs4all.nl; dkim=pass (2048-bit key) header.d=xs4all.nl header.i=@xs4all.nl header.b=WSp2jWq/; arc=none smtp.client-ip=195.121.94.167 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=xs4all.nl Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=xs4all.nl Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=xs4all.nl header.i=@xs4all.nl header.b="WSp2jWq/" X-KPN-MessageId: dbbda82e-52d9-11f1-96a9-005056abbe64 Received: from smtp.kpnmail.nl (unknown [10.31.155.39]) by ewsoutbound.so.kpn.org (Halon) with ESMTPS id dbbda82e-52d9-11f1-96a9-005056abbe64; Mon, 18 May 2026 18:51:47 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=xs4all.nl; s=xs4all01; h=mime-version:message-id:date:subject:to:from; bh=IH1gtlgWGlfJooKfwRoEN3A05j7btI9kUPmL/7nJ1hU=; b=WSp2jWq/YratPJVti/h4qKAUxHTZcQofTFTq7vPSNXOvCFLgEoxYV8bZDez6bJDEij+V++FHUaYDf RGzx+URPw6wGFHniMkgkaY5CgZq/q4hpKXig7aUOnwJzEsyBwynMe1CTv760s7yTEGQkMw1ssOwrru ZKDSBunqF2d3X6nrZdvKSyQmD+NT4yL+APBOcG03FjT6deFOeh5+eqhma30YLFfRXB7xoDZyJA6hNj 0FL7YZ4Bv23iOKCogEQyW9TM+SSe6kKAWxAmZGQPmPVtBUOK7+HoI9tVJoHGjAJFyE01/8/hJr49X8 DQT9D8iZrsd5FfUlMA+dir46HNglKYA== X-KPN-MID: 33|KsEPqyqI5QpkqbUELhaT08E3/lo9wjExjUI+UXGg5hKU42q1aaiq0NzJqWZ9yfV 7VKoOugOUfunloqgE9TSjxkyVyO0TuOY2uA7i2ERfvA0= X-KPN-VerifiedSender: Yes X-CMASSUN: 33|35KU9xZge9zr8SWwdDqZ1f3jyQnBlK3uMlbMwAYdTmSQKLx4hp0Wu4j4tJjq+AQ q39fl3IoN3rjsrWxx+B+Buw== Received: from daedalus.home (unknown [178.227.141.244]) by smtp.xs4all.nl (Halon) with ESMTPSA id db7dabd5-52d9-11f1-8011-005056ab7447; Mon, 18 May 2026 18:51:47 +0200 (CEST) From: Jori Koolstra To: Alexander Viro , Christian Brauner , Jan Kara , Aleksa Sarai Cc: Jori Koolstra , linux-kernel@vger.kernel.org, linux-fsdevel@vger.kernel.org, cmirabil@redhat.com, Jori Koolstra Subject: [RFC PATCH v4 2/2] selftest: add tests for open*(O_CREAT|O_DIRECTORY) Date: Mon, 18 May 2026 18:52:37 +0200 Message-ID: <20260518165237.2084042-3-jkoolstra@xs4all.nl> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260518165237.2084042-1-jkoolstra@xs4all.nl> References: <20260518165237.2084042-1-jkoolstra@xs4all.nl> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add some tests for the new valid O_CREAT|O_DIRECTORY flag combination for open*(2) to test compliance and to showcase its behaviour. Signed-off-by: Jori Koolstra --- .../testing/selftests/filesystems/.gitignore | 1 + tools/testing/selftests/filesystems/Makefile | 4 +- tools/testing/selftests/filesystems/fclog.c | 1 + .../filesystems/open_o_creat_o_dir.c | 200 ++++++++++++++++++ 4 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 tools/testing/selftests/filesystems/open_o_creat_o_dir.c diff --git a/tools/testing/selftests/filesystems/.gitignore b/tools/testing= /selftests/filesystems/.gitignore index 64ac0dfa46b7..f257b3ddb479 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/s= elftests/filesystems/Makefile index 85427d7f19b9..ec7f93b700d2 100644 --- a/tools/testing/selftests/filesystems/Makefile +++ b/tools/testing/selftests/filesystems/Makefile @@ -1,7 +1,7 @@ # SPDX-License-Identifier: GPL-2.0 =20 -CFLAGS +=3D $(KHDR_INCLUDES) -TEST_GEN_PROGS :=3D devpts_pts file_stressor anon_inode_test kernfs_test f= clog +CFLAGS +=3D $(KHDR_INCLUDES) $(TOOLS_INCLUDES) +TEST_GEN_PROGS :=3D open_o_creat_o_dir devpts_pts file_stressor anon_inode= _test kernfs_test fclog TEST_GEN_PROGS_EXTENDED :=3D dnotify_test =20 include ../lib.mk diff --git a/tools/testing/selftests/filesystems/fclog.c b/tools/testing/se= lftests/filesystems/fclog.c index 551c4a0f395a..33ed59286a2d 100644 --- a/tools/testing/selftests/filesystems/fclog.c +++ b/tools/testing/selftests/filesystems/fclog.c @@ -4,6 +4,7 @@ * Copyright (C) 2025 SUSE LLC. */ =20 +#include #include #include #include diff --git a/tools/testing/selftests/filesystems/open_o_creat_o_dir.c b/too= ls/testing/selftests/filesystems/open_o_creat_o_dir.c new file mode 100644 index 000000000000..96edca532e4c --- /dev/null +++ b/tools/testing/selftests/filesystems/open_o_creat_o_dir.c @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: GPL-2.0 +#include +#include +#include +#include +#include + +#include "kselftest_harness.h" + +static inline int open_o_creat_o_dir(int dfd, const char *pathname, + mode_t mode, unsigned int flags) +{ + return syscall(__NR_openat, dfd, pathname, + flags | O_DIRECTORY | O_CREAT, mode); +} + +#define open_o_creat_o_dir_checked_flags(dfd, pathname, flags) ({ \ + struct stat __st; \ + int __fd =3D open_o_creat_o_dir(dfd, pathname, S_IRWXU, flags); \ + ASSERT_GE(__fd, 0); \ + EXPECT_EQ(fstat(__fd, &__st), 0); \ + EXPECT_TRUE(S_ISDIR(__st.st_mode)); \ + __fd; \ +}) + +#define open_o_creat_o_dir_checked(dfd, pathname) \ + open_o_creat_o_dir_checked_flags(dfd, pathname, 0) + +FIXTURE(open_o_creat_o_dir) { + char dirpath[PATH_MAX]; + int dfd; +}; + +FIXTURE_SETUP(open_o_creat_o_dir) +{ + snprintf(self->dirpath, sizeof(self->dirpath), + "/tmp/open_o_creat_o_dir_test.%d", getpid()); + ASSERT_EQ(mkdir(self->dirpath, S_IRWXU), 0); + + self->dfd =3D 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 =3D open_o_creat_o_dir_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 =3D open_o_creat_o_dir_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(open_o_creat_o_dir(self->dfd, "nonexistent/child", S_IRWXU, 0),= -1); + EXPECT_EQ(errno, ENOENT); +} + +/* An invalid dfd must fail with EBADF. */ +TEST_F(open_o_creat_o_dir, ebadf) +{ + EXPECT_EQ(open_o_creat_o_dir(-42, "badfdir", S_IRWXU, 0), -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 =3D openat(self->dfd, "file", + O_CREAT | O_WRONLY, S_IRWXU); + ASSERT_GE(file_fd, 0); + + EXPECT_EQ(open_o_creat_o_dir(file_fd, "subdir", S_IRWXU, 0), -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 must fail with EEXIST when + * the target directory already exists. + */ +TEST_F(open_o_creat_o_dir, o_excl_eexist) +{ + int fd; + + fd =3D open_o_creat_o_dir_checked_flags(self->dfd, "excldir", O_EXCL); + EXPECT_EQ(close(fd), 0); + + EXPECT_EQ(open_o_creat_o_dir(self->dfd, "excldir", S_IRWXU, O_EXCL), -1); + EXPECT_EQ(errno, EEXIST); + + 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; + struct stat st; + + file_fd =3D openat(self->dfd, "regfile", + O_CREAT | O_WRONLY, S_IRWXU); + ASSERT_GE(file_fd, 0); + EXPECT_EQ(close(file_fd), 0); + + EXPECT_EQ(open_o_creat_o_dir(self->dfd, "regfile", S_IRWXU, 0), -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(open_o_creat_o_dir(self->dfd, "rdwrdir", S_IRWXU, O_RDWR), -1); + EXPECT_EQ(errno, EISDIR); + /* Clean up if the kernel created the directory anyway. */ + unlinkat(self->dfd, "rdwrdir", AT_REMOVEDIR); +} + +/* + * 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 =3D openat(self->dfd, "trailing/", O_CREAT | O_WRONLY, 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 >=3D 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 =3D open_o_creat_o_dir_checked(self->dfd, "parent"); + child_fd =3D open_o_creat_o_dir_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 --=20 2.54.0