-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Should EINVAL always be unreachable? #6925
Comments
Related: #6389 |
It should be handled on a case-by-case basis. For some syscalls, EINVAL should be unreachable because it can only mean something like an invalid pointer address being passed to the kernel, or invalid flags. However, as you pointed out, some kernel APIs unfortunately have decided to dual-purpose this error code to mean other things, in which case we must to handle the ambiguity by mapping it to an error code. We had to make this change with EBADF recently for some syscalls, for example. I do want to keep the error sets clean, however, so I am opposed to a blanket modification of all EINVAL code sites without cause. |
Ah I see... so returning an error would be less safe in that particular case compared to unreachable (or panic) because the program state is likely corrupted. If the intention is to crash the program and fail fast, would there be a reason not to use |
This is necessary to handle file systems that do not support O_DIRECT, and other common file system features. Fixes: ziglang#6925
@jorangreef @andrewrk I recently I came across this issue. I was writing a script to do a few things, including setting my screen brightness. I'm on an AMD system so I'm writing to The man page describes EINVAL as:
So I think this is mainly the fault of my amdgpu driver misbehaving, but I feel like this kind of error, which is handed to the program, should be returned.
Maybe adding EINVAL to the error sets isn't the right solution. I think the errors currently marked as unreachable should return unexpected since that error result is unexpected (it shouldn't happen) but not necessarily unreachable (because it can occur, either with a driver like amdgpu, or, as #6389 mentions, changing kernel behaviour).
|
Yes. However what we can do is decide that the code path is not, in fact, unreachable, and that leads us to...
I would support a change to make We may want to consider doing this carefully - in a debug build currently, hitting that |
Agreed, I would just add that even in debug builds, there are situations where an error should be thrown for |
@jorangreef also note that it's pretty easy to call a system call directly in Zig. If you're doing a platform-specific test like testing for |
Thanks @marler8997 , yes we're using |
Often, it is necessary to accept a file descriptor and perform some operation on it. Crashing when that file descriptor is invalid leaves a program with no way to prevent the UB without reimplementing or pasting standard library code. The similarity this bears to memory allocation failure is remarkable. The main argument for handling EBADF as unreachable is that it's "always a race condition". Not only is this obviously not true as Andrew himself demonstrates here, but it is the very same argument as "memory allocation failure never happens because overcommit exists". Handling memory allocation failure by crashing limits the reusability of the code because there are use cases in which the user needs to handle it. Handling EBADF by crashing limits the reusability of the code because there are use cases in which the user needs to handle it. |
Saying it "limits the reusability" is too strong IMO, it just puts an extra requirement on the caller to verify the file descriptor is valid. Handling "memory allocation failure" isn't analogous to "requiring valid file descriptors". The equivalent in terms of file desciptors would be handling "file descriptor creation failure", which you can and should always do. The equivalent of "requiring valid file descriptors" in terms of memory allocation would be "requiring valid pointers". This is such a common occurrence that Zig incorporates it into its type system, via optional and non-optional pointers. The majority of Zig functions require valid pointers (not optional). This doesn't mean the code can't be reused, it just puts a requirement on the caller to verify the pointer is valid before calling it, this is the same requirement Zig's std library puts on file descriptors. In fact on systems that use pointers for handles (like Windows), you can use "optional vs non-optional" on the handles themselves to declare whether they are required to be "valid". The zigwin32 library leverages this which enforces handle validity at compile time instead of runtime, pretty neat :) P.S. Actually it's more accurate to say that this enables libraries to accept invalid handles on Windows and have that protected by the type system. The different with std is that by convention it pretty much requires all file descriptors to be valid. |
I'm sorry, but "verifying the file descriptor is valid" is system-dependent and extra code that is not necessary, violating Zig's goal of optimality (for instance:
You misunderstand. File descriptors are not always acquired by the same code that uses them. Often, a file descriptor even originates from the user directly, as is the case with
This is a terrible analogy. Unlike with file descriptors on many systems, it is categorically impossible to check whether a pointer is valid. The only case in which it is possible is with null pointers. It is analogous to checking whether a file descriptor is |
Ah this does bring up an example I hadn't thought of. I think the requirement for valid file descriptors is fine within the context of a single program where there's an established API, but file descriptors can cross process boundaries. Triggering "unreachable" because a process received an invalid file descriptor from another process doesn't seem like a good approach :) Here are solutions that come to mind
Each solution has tradeoffs. I don't particularly like the first solution (what I think is being proposed here) since I think the use cases that require this are much more rare compared to code that is able to verify a file descriptor is valid and wouldn't need to handle this error (i.e. code that would be doing |
With option 3, you unfortunately run into the whole reusability problem. My .02 is just that EBADF should be made handleable in |
I had another use-case today where I'd need to handle |
If I'm remembering correctly, you can accomplish this by calling P.S. Calling close while you are calling accept might be considered a misuse of the posix socket API. If so, it's interesting to note that the std library's choice to make this unhandlable will have resulted in forcing you to fix this misuse rather than adding a "band-aid" solution where you catch/swallow an EBADF error. |
Yep! It looks like the problem was indeed calling |
Are these situations enumerable? |
The std lib handles
EINVAL
asunreachable
foros.openatZ()
.However, there are some legitimate runtime use cases that could trigger
EINVAL
, e.g. trying to open a file descriptor inO_DIRECT
on Linux, but on a file system that does not supportO_DIRECT
.This then leads to undefined behavior rather than a safe error.
It also means you would have to reimplement
os.openatZ()
if you wanted to write something to detect fs support forO_DIRECT
.There are also other scenarios that could trigger
EINVAL
, e.g. the system does not support synchronized I/O for this file, or the O_XATTR flag was supplied and the underlying file system does not support extended file attributes.These are all fairly common failure modes for storage/backup/sync applications that need to probe file system characteristics.
Should
EINVAL
always beunreachable
? Could the std lib start to move away fromunreachable
for this and other syscalls?The text was updated successfully, but these errors were encountered: