[PATCH v2 1/5] fcntl: Cast commands with int args explicitly

Luca Vizzarro posted 5 patches 2 years, 8 months ago
[PATCH v2 1/5] fcntl: Cast commands with int args explicitly
Posted by Luca Vizzarro 2 years, 8 months ago
According to the fcntl API specification commands that expect an
integer, hence not a pointer, always take an int and not long. In
order to avoid access to undefined bits, we should explicitly cast
the argument to int.

Cc: Alexander Viro <viro@zeniv.linux.org.uk>
Cc: Christian Brauner <brauner@kernel.org>
Cc: Jeff Layton <jlayton@kernel.org>
Cc: Chuck Lever <chuck.lever@oracle.com>
Cc: Kevin Brodsky <Kevin.Brodsky@arm.com>
Cc: Vincenzo Frascino <Vincenzo.Frascino@arm.com>
Cc: Szabolcs Nagy <Szabolcs.Nagy@arm.com>
Cc: "Theodore Ts'o" <tytso@mit.edu>
Cc: David Laight <David.Laight@ACULAB.com>
Cc: Mark Rutland <Mark.Rutland@arm.com>
Cc: linux-fsdevel@vger.kernel.org
Cc: linux-morello@op-lists.linaro.org
Signed-off-by: Luca Vizzarro <Luca.Vizzarro@arm.com>
---
 fs/fcntl.c         | 29 +++++++++++++++--------------
 include/linux/fs.h |  2 +-
 2 files changed, 16 insertions(+), 15 deletions(-)

diff --git a/fs/fcntl.c b/fs/fcntl.c
index b622be119706..e871009f6c88 100644
--- a/fs/fcntl.c
+++ b/fs/fcntl.c
@@ -34,7 +34,7 @@
 
 #define SETFL_MASK (O_APPEND | O_NONBLOCK | O_NDELAY | O_DIRECT | O_NOATIME)
 
-static int setfl(int fd, struct file * filp, unsigned long arg)
+static int setfl(int fd, struct file * filp, unsigned int arg)
 {
 	struct inode * inode = file_inode(filp);
 	int error = 0;
@@ -112,11 +112,11 @@ void __f_setown(struct file *filp, struct pid *pid, enum pid_type type,
 }
 EXPORT_SYMBOL(__f_setown);
 
-int f_setown(struct file *filp, unsigned long arg, int force)
+int f_setown(struct file *filp, int who, int force)
 {
 	enum pid_type type;
 	struct pid *pid = NULL;
-	int who = arg, ret = 0;
+	int ret = 0;
 
 	type = PIDTYPE_TGID;
 	if (who < 0) {
@@ -317,28 +317,29 @@ static long do_fcntl(int fd, unsigned int cmd, unsigned long arg,
 		struct file *filp)
 {
 	void __user *argp = (void __user *)arg;
+	int argi = (int)arg;
 	struct flock flock;
 	long err = -EINVAL;
 
 	switch (cmd) {
 	case F_DUPFD:
-		err = f_dupfd(arg, filp, 0);
+		err = f_dupfd(argi, filp, 0);
 		break;
 	case F_DUPFD_CLOEXEC:
-		err = f_dupfd(arg, filp, O_CLOEXEC);
+		err = f_dupfd(argi, filp, O_CLOEXEC);
 		break;
 	case F_GETFD:
 		err = get_close_on_exec(fd) ? FD_CLOEXEC : 0;
 		break;
 	case F_SETFD:
 		err = 0;
-		set_close_on_exec(fd, arg & FD_CLOEXEC);
+		set_close_on_exec(fd, argi & FD_CLOEXEC);
 		break;
 	case F_GETFL:
 		err = filp->f_flags;
 		break;
 	case F_SETFL:
-		err = setfl(fd, filp, arg);
+		err = setfl(fd, filp, argi);
 		break;
 #if BITS_PER_LONG != 32
 	/* 32-bit arches must use fcntl64() */
@@ -375,7 +376,7 @@ static long do_fcntl(int fd, unsigned int cmd, unsigned long arg,
 		force_successful_syscall_return();
 		break;
 	case F_SETOWN:
-		err = f_setown(filp, arg, 1);
+		err = f_setown(filp, argi, 1);
 		break;
 	case F_GETOWN_EX:
 		err = f_getown_ex(filp, arg);
@@ -391,28 +392,28 @@ static long do_fcntl(int fd, unsigned int cmd, unsigned long arg,
 		break;
 	case F_SETSIG:
 		/* arg == 0 restores default behaviour. */
-		if (!valid_signal(arg)) {
+		if (!valid_signal(argi)) {
 			break;
 		}
 		err = 0;
-		filp->f_owner.signum = arg;
+		filp->f_owner.signum = argi;
 		break;
 	case F_GETLEASE:
 		err = fcntl_getlease(filp);
 		break;
 	case F_SETLEASE:
-		err = fcntl_setlease(fd, filp, arg);
+		err = fcntl_setlease(fd, filp, argi);
 		break;
 	case F_NOTIFY:
-		err = fcntl_dirnotify(fd, filp, arg);
+		err = fcntl_dirnotify(fd, filp, argi);
 		break;
 	case F_SETPIPE_SZ:
 	case F_GETPIPE_SZ:
-		err = pipe_fcntl(filp, cmd, arg);
+		err = pipe_fcntl(filp, cmd, argi);
 		break;
 	case F_ADD_SEALS:
 	case F_GET_SEALS:
-		err = memfd_fcntl(filp, cmd, arg);
+		err = memfd_fcntl(filp, cmd, argi);
 		break;
 	case F_GET_RW_HINT:
 	case F_SET_RW_HINT:
diff --git a/include/linux/fs.h b/include/linux/fs.h
index c85916e9f7db..8da79822dbba 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -1050,7 +1050,7 @@ extern void fasync_free(struct fasync_struct *);
 extern void kill_fasync(struct fasync_struct **, int, int);
 
 extern void __f_setown(struct file *filp, struct pid *, enum pid_type, int force);
-extern int f_setown(struct file *filp, unsigned long arg, int force);
+extern int f_setown(struct file *filp, int who, int force);
 extern void f_delown(struct file *filp);
 extern pid_t f_getown(struct file *filp);
 extern int send_sigurg(struct fown_struct *fown);
-- 
2.34.1
Re: [PATCH v2 1/5] fcntl: Cast commands with int args explicitly
Posted by Al Viro 2 years, 8 months ago
On Fri, Apr 14, 2023 at 04:24:55PM +0100, Luca Vizzarro wrote:
>  	void __user *argp = (void __user *)arg;
> +	int argi = (int)arg;

Strictly speaking, conversion from unsigned long to int is
an undefined behaviour, unless the value fits into the
range representable by int ;-)

>  	case F_SETFD:
>  		err = 0;
> -		set_close_on_exec(fd, arg & FD_CLOEXEC);
> +		set_close_on_exec(fd, argi & FD_CLOEXEC);

Why?

>  	case F_SETSIG:
>  		/* arg == 0 restores default behaviour. */
> -		if (!valid_signal(arg)) {
> +		if (!valid_signal(argi)) {

Why???

>  			break;
>  		}
>  		err = 0;
> -		filp->f_owner.signum = arg;
> +		filp->f_owner.signum = argi;
>  		break;

These two are clearly bogus and I'd like to see more details
on the series rationale, please.
Re: [PATCH v2 1/5] fcntl: Cast commands with int args explicitly
Posted by Luca Vizzarro 2 years, 8 months ago
On 14/04/2023 16:46, Al Viro wrote

> Why?
> ...
> Why???
 > ...
> These two are clearly bogus and I'd like to see more details
> on the series rationale, please.

Mark preceded me with his reply, which is perfectly summarising
the whole point of this series. – Thank you Mark!

As for FD_SETFD, yes it's not necessary. The only reason I
changed the variable was to denote the argument as being
classified as an `int` as per the man page. If I were not to
change it, it would have been the only command with an `int`
argument not to use `argi`. Therefore it's also for
consistency's sake.

Hope this helps.

Best,
Luca
Re: [PATCH v2 1/5] fcntl: Cast commands with int args explicitly
Posted by Mark Rutland 2 years, 8 months ago
On Fri, Apr 14, 2023 at 04:46:31PM +0100, Al Viro wrote:
> On Fri, Apr 14, 2023 at 04:24:55PM +0100, Luca Vizzarro wrote:
> >  	void __user *argp = (void __user *)arg;
> > +	int argi = (int)arg;
> 
> Strictly speaking, conversion from unsigned long to int is
> an undefined behaviour, unless the value fits into the
> range representable by int ;-)
> 
> >  	case F_SETFD:
> >  		err = 0;
> > -		set_close_on_exec(fd, arg & FD_CLOEXEC);
> > +		set_close_on_exec(fd, argi & FD_CLOEXEC);
> 
> Why?
> 
> >  	case F_SETSIG:
> >  		/* arg == 0 restores default behaviour. */
> > -		if (!valid_signal(arg)) {
> > +		if (!valid_signal(argi)) {
> 
> Why???
> 
> >  			break;
> >  		}
> >  		err = 0;
> > -		filp->f_owner.signum = arg;
> > +		filp->f_owner.signum = argi;
> >  		break;
> 
> These two are clearly bogus and I'd like to see more details
> on the series rationale, please.

I agree the first isn't necessary, but I don't think the second is bogus, since
valid_signal() takes an unsigned long and the man page for F_SETSIG says that
the argument is an int:

  https://man7.org/linux/man-pages/man2/fcntl.2.html

... though arguably that could be a bug in the man page.

The cover letter really should have quoted the description that Szabolcs wote
at:

  https://lore.kernel.org/linux-api/Y1%2FDS6uoWP7OSkmd@arm.com/

The gist being that where the calling convention leaves narrowing to callees
(as is the case on arm64 with our "AAPCS64" calling convention), if the caller
passes a type which is narrower than a register, the upper bits of that
register may contain junk.

So e.g. for F_SETSIG, if the userspace will try to pass some 32-bit value,
leaving bits 63:32 of the argument register containing arbitrary junk. Then
here we interprert the value as an unsigned long, considering that junk as part
of the argument. Then valid_signal(arg) may end up rejecting the argument due
to the junk uper bits, which is surprising to the caller as from its PoV it
passed a 32-bit value in the correct way.

So either:

* That's a documentation bug, and userspce needs to treat the agument to
  F_SETSIG as an unsigned long.

* The kernel needs to narrow the argument to an int (if required by the calling
  convention) to prevent that.

Does that make sense, or have I missed the point you were making?

Thanks,
Mark.