Skip to content

Go: Add query to detect lack of error handling for os.File.Close on writable handles #11496

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

Merged
merged 20 commits into from
Feb 8, 2023

Conversation

mbg
Copy link
Member

@mbg mbg commented Nov 30, 2022

Based on discussions following from #11410, this adds a new query which looks for calls to os.File.Close on writable file handles, where resulting errors are not handled.

This is useful, because data written to a file may not be flushed to the underlying storage medium until os.File.Close is called (or possibly even later than that). Therefore, any errors resulting from issues with the storage medium may not occur until that point. Deferring calls to os.File.Close or ignoring errors returned by it may lead to silent data loss.

@mbg mbg added the Go label Nov 30, 2022
@mbg mbg requested a review from a team as a code owner November 30, 2022 12:40
@mbg mbg self-assigned this Nov 30, 2022
@github-actions
Copy link
Contributor

github-actions bot commented Nov 30, 2022

QHelp previews:

go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.qhelp

Writable file handle closed without error handling

Data written to a file handle may not immediately be flushed to the underlying storage medium by the operating system. Often, the data may be cached in memory until the handle is closed, or until a later point after that. Only calling os.File.Sync gives a reasonable guarantee that the data is flushed. Therefore, write errors may not occur until os.File.Close or os.File.Sync are called. If either is called and any errors returned by them are discarded, then the program may be unaware that data loss occurred.

Recommendation

Always check whether os.File.Close returned an error and handle it appropriately.

Example

In this first example, two calls to os.File.Close are made with the intention of closing the file after all work on it has been done or if writing to it fails. However, while errors that may arise during the call to os.File.WriteString are handled, any errors arising from the calls to os.File.Close are discarded silently:

package main

import (
	"os"
)

func example() error {
	f, err := os.OpenFile("file.txt", os.O_WRONLY|os.O_CREATE, 0666)

	if err != nil {
		return err
	}

	if _, err := f.WriteString("Hello"); err != nil {
		f.Close()
		return err
	}

	f.Close()

	return nil
}

In the second example, a call to os.File.Close is deferred in order to accomplish the same behaviour as in the first example. However, while errors that may arise during the call to os.File.WriteString are handled, any errors arising from os.File.Close are again discarded silently:

package main

import (
	"os"
)

func example() error {
	f, err := os.OpenFile("file.txt", os.O_WRONLY|os.O_CREATE, 0666)

	if err != nil {
		return err
	}

	defer f.Close()

	if _, err := f.WriteString("Hello"); err != nil {
		return err
	}

	return nil
}

To correct this issue, handle errors arising from calls to os.File.Close explicitly:

package main

import (
	"os"
)

func example() (exampleError error) {
	f, err := os.OpenFile("file.txt", os.O_WRONLY|os.O_CREATE, 0666)

	if err != nil {
		return err
	}

	defer func() {
		// try to close the file; if an error occurs, set the error returned by `example`
		// to that error, but only if `WriteString` didn't already set it to something first
		if err := f.Close(); exampleError == nil && err != nil {
			exampleError = err
		}
	}()

	if _, err := f.WriteString("Hello"); err != nil {
		exampleError = err
	}

	return
}

References

  • The Go Programming Language Specification: Defer statements.
  • The Go Programming Language Specification: Errors.

Copy link
Contributor

@owen-mc owen-mc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry it took me so long to review this. Lots of minor niggles, plus a few questions.

@mbg mbg force-pushed the mbg/add/writable-file-closed-error-query branch from e969526 to 41c4cd5 Compare January 5, 2023 17:09
@owen-mc
Copy link
Contributor

owen-mc commented Feb 3, 2023

All my review comments have been addressed. I haven't looked through it thoroughly, but from memory I think that means it can be merged. But the tests are failing because rebasing changed the format of the test output slightly (I remember merging the PR which did that).

@mbg mbg force-pushed the mbg/add/writable-file-closed-error-query branch from 41c4cd5 to 25f9078 Compare February 6, 2023 08:52
@mbg
Copy link
Member Author

mbg commented Feb 6, 2023

@owen-mc there was one unresolved review comment about adding another example which doesn't use defer to the QHelp documentation. I have done that now, updated the expected test output, and rebased this branch.

@mbg mbg merged commit 3abf321 into main Feb 8, 2023
@mbg mbg deleted the mbg/add/writable-file-closed-error-query branch February 8, 2023 10:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants