On 2019-12-17, David Laight David.Laight@ACULAB.COM wrote:
From Aleksa Sarai
Sent: 17 December 2019 06:47
...
Just use u64 for all the fields.
That is an option (and is the one that clone3 went with), but it's a bit awkward because umode_t is a u16 -- and it would be a waste of 6 bytes to store it as a u64. Arguably it could be extended but I personally find that to be very unlikely (and lots of other syscalls would need be updated).
6 bytes on interface structure will make almost no difference. There is no reason to save more than 16 bits anywhere else.
You have a point, and clone3's way of dealing with it does make life easier. It also removes the need to care about explicit padding and padding holes entirely.
You could error values with high bits set.
Of course we'll give -EINVAL with invalid values, that's one of the reasons openat2(2) exists after all. :P
I'm just going to move the padding to the end and change the error for non-zero padding to -E2BIG.
The padding had to be after the u16 field.
Right, I was suggesting to move the u16 field later in the struct too. But after thinking about it some more, it doesn't help with extensibility at all (a subsequent non-u16 extension will leave holes). So I'm probably just going to go with either the -E2BIG patch or switch to u64s.
Use 'flags' bits to indicate whether the additional fields should be looked at. Error if a 'flags' bit requires a value that isn't passed in the structure.
Then you can add an extra field and old source code recompiled with the new headers will still work - because the 'junk' value isn't looked at.
This problem is already handled entirely by copy_struct_from_user().
It is true that for some new fields it will be necessary to add a new flag (such as passing fds -- where 0 is a valid value) but for most new fields (especially pointer or flag fields) it will not be necessary because the 0 value is equivalent to the old behaviour. It also allows us to entirely avoid accepting junk from userspace.
Only if userspace is guaranteed to memset the entire structure before making the call - rather than just fill in all the fields it knows about. If it doesn't use memset() then recompiling old code with new headers will pass garbage to the kernel. copy_struct_from_user() cannot solve that problem.
You don't need to /explicitly/ memset(), since
struct open_how how = { .flags = O_RDWR, .resolve = RESOLVE_IN_ROOT };
or even
struct open_how how = {}; /* or { 0 } if you prefer. */
will clear all of the unused fields.
But, I can add a NOTE to the man-page to clarify that this is how users should fill their structs (or rather, that they should zero-fill them somehow to avoid this problem).
While this might be a little annoying, I would argue that given the openat2(2) man page explains how extensions work (in great detail) and mentions several times that the structure may have new fields added to it in the future -- programs which don't zero-fill the struct should be simply seen as buggy. Note that those buggy programs *will still work* on new kernels -- until you recompile them with new headers (because they made an incorrect assumption about the structures they were using).
As an aside, the other downside from the uapi side is that we would probably have to spend flag bits *that are shared with openat(2)* for such extensions, so I'd like to avoid that as much as necessary.
You'll never be able to guarantee that all code actually clears the entire structure - so at some point extending it will break recompilations of old code - annoying.
Only if they're explicitly doing something like
struct open_how how; how.flags = O_RDWR; how.resolve = RESOLVE_IN_ROOT; memset(how.__padding, 0, sizeof(how.__padding));
As above, given the description of extensions in the man-page, I would consider that style of struct initialisation to be eyebrow-raising at best.
I'm sorry, but I'm simply against the idea of silently ignoring garbage that userspace passes to the kernel -- even if it's tied to a flag. That has proven to be an awful idea and in fact openat2(2) was written precisely to fix this problem. To be honest, this reminds me of (hypothetical) code like:
int flags; flags |= O_PATH | O_CLOEXEC; open("foo", flags); /* yay, mystery fds! */
IMHO that shouldn't have ever worked, and the only way to stop userspace from passing garbage is to always reject it.