Skip to content

Send wrapped errnos to the kernel and treat ENOENT reply writes as benign - #203

Open
dsfaccini wants to merge 2 commits into
jacobsa:masterfrom
dsfaccini:upstream/errno-passthrough
Open

dsfaccini wants to merge 2 commits into
jacobsa:masterfrom
dsfaccini:upstream/errno-passthrough

Conversation

@dsfaccini

Copy link
Copy Markdown

This pull request was posted by Claude Code using claude-opus-5-5 on behalf of David.

Summary

kernelResponse sends an op error's errno only when the error is a bare syscall.Errno. An error that wraps one, such as fmt.Errorf("lookup: %w", syscall.ENOENT) or an *os.PathError, reaches the kernel as EIO. This PR sends the first syscall.Errno in the error's chain.

  • kernelResponse sends the first syscall.Errno in the op error's chain. An error with no errno still reaches the kernel as EIO.
  • shouldLogError judges the same errno. A wrapped ENOENT from LookUpInode, or a wrapped ENOSYS, ENODATA or ERANGE from the xattr ops, no longer goes to the error log. An error that holds EIO before ENOENT is still logged, because the kernel receives EIO.
  • Reply neither logs nor returns an ENOENT error from the reply write. The next section says when the kernel returns it. This is the case that Avoid printing the error message related to 'no such file or directory' #128 asked about.
  • fuseutil falls back to ForgetInode when BatchForget returns a wrapped ENOSYS. The fallback moves into a batchForget method, so a test can call it.
  • A nil *os.PathError no longer panics. errors.As panics when it unwraps one. The new internal/operror.Errno recovers and reports no errno, so the kernel gets EIO, as on master. On master, a nil *os.PathError already panics in formatWireLogEntry when a wire logger is set, and in the Reply debug log when no error logger is set. The debug log now passes the error to %q instead of calling Error(), because fmt recovers from a panic in Error only when it calls Error itself.

When the reply write returns ENOENT

#128 asked when the reply write fails with ENOENT. On Linux, fuse_dev_do_write looks up the reply's unique among the requests that wait for a reply. When it finds none, the write returns ENOENT (dev.c L1999-L2008). It also returns ENOENT when the connection is aborted while the reply is copied (L2043-L2046).

A request that the file system has read stays in that list until it gets a reply, even when its caller is interrupted (request_wait_answer, L460-L464). It leaves the list without a reply only when fuse_abort_conn ends every request in flight. The kernel aborts the connection in these cases:

So the error comes from an op that is still running when the mount goes away. For example, the kernel sends RELEASE in the background on a mount that is not fuseblk (file.c L359), so an unmount right after the last close can end a RELEASE that the file system still handles. The request is gone, so nothing can retry it. ServeOps ignores the error that Reply returns, so on master the only effect is a writeMessage: no such file or directory line in the error log.

libfuse also leaves ENOENT out of its error output (fuse_write_msg_dev).

I have not reproduced this on a mount for this PR. The steps follow from the kernel code: make LookUpInode in samples/hellofs sleep for a few seconds, run stat on a missing name, and write 1 to the connection's abort file while the op sleeps. The reply write then returns ENOENT.

Tests

None of the new tests mounts. Run them with:

go test -run '^(TestKernelResponseKeepsWrappedErrno|TestShouldLogErrorMatchesWrappedErrno|TestReplyTreatsENOENTWriteAsBenign|TestFormatWireLogEntryStatus|TestReplyDebugLogsOpError|TestBatchForgetFallsBackToForgetInodeOnENOSYS)$' . ./fuseutil
  • TestKernelResponseKeepsWrappedErrno: the errno the kernel receives for a bare, wrapped, joined or errno-less error, and for a nil *os.PathError.
  • TestShouldLogErrorMatchesWrappedErrno: wrapped expected errnos are not logged. Errors whose first errno is not the expected one, and errors with no errno, are logged.
  • TestReplyTreatsENOENTWriteAsBenign: drives ReadOp and Reply. An ENOENT write error is neither logged nor returned. An EIO write error still is.
  • TestFormatWireLogEntryStatus: the wire-log Status for a wrapped errno, an error with no errno, and a nil *os.PathError.
  • TestReplyDebugLogsOpError: the exact debug-log line for an ordinary error, a nil *os.PathError, and a joined error whose Error panics.
  • TestBatchForgetFallsBackToForgetInodeOnENOSYS in fuseutil: the fallback runs for a bare or wrapped ENOSYS and for no other error.

Each test fails on master (a124548) with only the syscallWrite variable and the batchForget method added, so that the tests compile. On that tree the wrapped-errno rows fail, except in TestFormatWireLogEntryStatus, because formatWireLogEntry already uses errors.As. The nil *os.PathError rows of TestFormatWireLogEntryStatus and TestReplyDebugLogsOpError panic. Rows that pass on both trees pin behavior this PR keeps, such as EIO for an error with no errno. The other nil *os.PathError rows panic if operror.Errno stops recovering.

writeMessage now calls syscall.Write through a package variable, syscallWrite. A test cannot make a write to a real file descriptor fail with ENOENT, so TestReplyTreatsENOENTWriteAsBenign replaces it. If you prefer no test seam, I can drop the variable and that test.

Not changed

  • A bare fs.ErrNotExist still reaches the kernel as EIO. It holds no syscall.Errno, and mapping io/fs errors to errnos is a separate change.
  • kernelResponse keeps its handled := false branch, to keep the diff small.

…nign

- kernelResponse sends the first syscall.Errno in an op error's chain.
  A wrapped errno, such as fmt.Errorf("lookup: %w", syscall.ENOENT) or an
  *os.PathError, used to reach the kernel as EIO.
- shouldLogError judges the same errno that kernelResponse sends.
- Reply neither logs nor returns an ENOENT reply-write error. The kernel
  returns ENOENT when it no longer tracks the request, for example after
  the connection was aborted. libfuse also leaves it out of its error output.
- fuseutil falls back to ForgetInode when BatchForget returns a wrapped
  ENOSYS.
- A nil *os.PathError no longer panics in the debug log or the wire log.

The new internal/operror package finds the errno and reports none when
unwrapping panics, as it does for a nil *os.PathError.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant