Re: [PATCH v4 2/4] samples: rust: Provide example using the new Rust MiscDevice abstraction

Arnd Bergmann
2024-12-06T11:15:57Z

On Fri, Dec 6, 2024, at 11:40, Alice Ryhl wrote:
> On Fri, Dec 6, 2024 at 11:31 AM Arnd Bergmann <arnd@arndb.de> wrote:

>> I wonder if we should change the prototype of the ioctl
>> callback to always pass a __user pointer and just not allow
>> the few commands that pass an integer in rust drivers, and
>> worry about it only when it's absolutely needed.
>
> One option is to let the Rust Miscdevice trait have three ioctl methods:
>
> fn ioctl(cmd: u32, arg: UserPtr);
> fn ioctl_raw(cmd: u32, arg: usize);
> fn compat_ioctl(cmd: u32, arg: usize);
>
> Then when building the fops vtable, we do one of:
>
> 1. If `ioctl` is specified, use that implementation with compat_ptr_ioctl().
> 2. If `ioctl_raw` and `compat_ioctl` are specified, use those two
> implementations.
> 3. If none of the above are specified, use null pointers.
> 4. All other cases trigger an error at build time.
>
> Thoughts?

I think we can combine the latter two into

fn ioctl_raw(cmd: u32, arg: usize, compat: bool);

and only need two cases: either all arguments are pointers to
structures with compatible layout and you can use the simple
ioctl() callback, or there is some special case (incompatible
struct layout or integer arguments) and you use the raw
version. I think this would work just fine.

Or we could take it one step further (going back to the
discussion we had the last time this came up) and make
the default version copy the data as well and pass it
as a kernel pointer. In C code this would look roughly
like

long file_ioctl_wrapper(struct file *f, u32 cmd, unsigned long arg)
{
void *argp;
void __user *uarg;
bool compat = in_compat_syscall());
size_t size = _IOC_SIZE(cmd);
int ret = -ENOIOCTLCMD;

/* Get a pointer argument for both native and compat mode */
if (compat)
uarg = compat_ptr(arg);
else
uarg = (void __user*)arg;

/*
* allow .ioctl_raw to provide a custom version for
* commands that take an integer argument, have an
* incompatible compat layout or fail to encode size
* and/or direction correctly in cmd
* This can return ENOIOCTLCMD to fall back to the
* simple handler for other commands.
*/
if (f->ops->ioctl_raw)
ret = f->ops->ioctl_raw(f, cmd, uarg, arg, compat);
if (ret != -ENOIOCTLCMD)
return ret;

/* No data, so skip the allocation */
if (_IOC_DIR(cmd) == _IOC_NONE || size = 0)
return f->ops->ioctl(f, cmd, NULL);

argp = kzalloc(size, GFP_KERNEL);
if (!argp)
return -ENOMEM;

/* _IOW or _IOWR, so copy data into the kernel */
if (_IOC_DIR(cmd) & _IOC_WRITE) {
if (copy_from_user(argp, uarg, size))
return -EFAULT;
}

ret = f->ops->ioctl(f, cmd, arg);

/* _IOR or _IOWR, copy back even after command failure */
if (_IOC_DIR(cmd) & _IOC_READ) {
if (copy_to_user(uarg, arg, size))
return -EFAULT;
}

return ret;
}

With this, every driver that has a properly designed
set of ioctl commands can just use the simple .ioctl()
callback, and any commands that need some special case
can be put in the .ioctl_raw() callback.

If this works out for Rust, we can actually put that exact
code into vfs_ioctl() and add back a .ioctl() callback
into struct file_operations next to the C .unlocked_ioctl
and .compat_ioctl handlers.

Arnd