-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Python: Modernize File Not Always Closed query #18845
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
joefarebrother
merged 13 commits into
github:main
from
joefarebrother:python-qual-file-not-closed
Mar 28, 2025
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
09694c4
Rewrite file not closed simple case using dataflow
joefarebrother ecb3050
Update tests
joefarebrother c8fc565
Check for wrapper classes
joefarebrother f750e22
Add case for exception flow
joefarebrother f8a0b1c
Update docs, precision, and deprecate old library
joefarebrother b2acfbc
Simplify handling of wrapper classes and exception flow + improve qld…
joefarebrother 2c74ddb
Add django FileRsponse as a wrapper
joefarebrother 3707f10
Fix tests + add more tests
joefarebrother bdbdcf8
Clean up charpred of WithStatement + fix a comment
joefarebrother a46c157
Add quality tag + tweak description
joefarebrother 0fa70db
Review suggestions - update comment and introduce manual magic to fil…
joefarebrother d23c3b8
Revert manual magic
joefarebrother 2fd9b16
Attempt performance improvement for fileLocalFlow
joefarebrother File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,74 +1,26 @@ | ||
| /** | ||
| * @name File is not always closed | ||
| * @description Opening a file without ensuring that it is always closed may cause resource leaks. | ||
| * @description Opening a file without ensuring that it is always closed may lead to data loss or resource leaks. | ||
| * @kind problem | ||
| * @tags efficiency | ||
| * correctness | ||
| * resources | ||
| * quality | ||
| * external/cwe/cwe-772 | ||
| * @problem.severity warning | ||
| * @sub-severity high | ||
| * @precision medium | ||
| * @precision high | ||
| * @id py/file-not-closed | ||
| */ | ||
|
|
||
| import python | ||
| import FileOpen | ||
| import FileNotAlwaysClosedQuery | ||
|
|
||
| /** | ||
| * Whether resource is opened and closed in in a matched pair of methods, | ||
| * either `__enter__` and `__exit__` or `__init__` and `__del__` | ||
| */ | ||
| predicate opened_in_enter_closed_in_exit(ControlFlowNode open) { | ||
| file_not_closed_at_scope_exit(open) and | ||
| exists(FunctionValue entry, FunctionValue exit | | ||
| open.getScope() = entry.getScope() and | ||
| exists(ClassValue cls | | ||
| cls.declaredAttribute("__enter__") = entry and cls.declaredAttribute("__exit__") = exit | ||
| or | ||
| cls.declaredAttribute("__init__") = entry and cls.declaredAttribute("__del__") = exit | ||
| ) and | ||
| exists(AttrNode attr_open, AttrNode attrclose | | ||
| attr_open.getScope() = entry.getScope() and | ||
| attrclose.getScope() = exit.getScope() and | ||
| expr_is_open(attr_open.(DefinitionNode).getValue(), open) and | ||
| attr_open.getName() = attrclose.getName() and | ||
| close_method_call(_, attrclose) | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| predicate file_not_closed_at_scope_exit(ControlFlowNode open) { | ||
| exists(EssaVariable v | | ||
| BaseFlow::reaches_exit(v) and | ||
| var_is_open(v, open) and | ||
| not file_is_returned(v, open) | ||
| ) | ||
| or | ||
| call_to_open(open) and | ||
| not exists(AssignmentDefinition def | def.getValue() = open) and | ||
| not exists(Return r | r.getValue() = open.getNode()) | ||
| } | ||
|
|
||
| predicate file_not_closed_at_exception_exit(ControlFlowNode open, ControlFlowNode exit) { | ||
| exists(EssaVariable v | | ||
| exit.(RaisingNode).viableExceptionalExit(_, _) and | ||
| not closes_arg(exit, v.getSourceVariable()) and | ||
| not close_method_call(exit, v.getAUse().(NameNode)) and | ||
| var_is_open(v, open) and | ||
| v.getAUse() = exit.getAChild*() | ||
| ) | ||
| } | ||
|
|
||
| /* Check to see if a file is opened but not closed or returned */ | ||
| from ControlFlowNode defn, string message | ||
| from FileOpen fo, string msg | ||
| where | ||
| not opened_in_enter_closed_in_exit(defn) and | ||
| ( | ||
| file_not_closed_at_scope_exit(defn) and message = "File is opened but is not closed." | ||
| or | ||
| not file_not_closed_at_scope_exit(defn) and | ||
| file_not_closed_at_exception_exit(defn, _) and | ||
| message = "File may not be closed if an exception is raised." | ||
| ) | ||
| select defn.getNode(), message | ||
| fileNotClosed(fo) and | ||
| msg = "File is opened but is not closed." | ||
| or | ||
| fileMayNotBeClosedOnException(fo, _) and | ||
| msg = "File may not be closed if an exception is raised." | ||
| select fo, msg |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| /** Definitions for reasoning about whether files are closed. */ | ||
|
|
||
| import python | ||
| import semmle.python.dataflow.new.internal.DataFlowDispatch | ||
| import semmle.python.ApiGraphs | ||
|
|
||
| /** A CFG node where a file is opened. */ | ||
| abstract class FileOpenSource extends DataFlow::CfgNode { } | ||
|
|
||
| /** A call to the builtin `open` or `os.open`. */ | ||
| class FileOpenCall extends FileOpenSource { | ||
| FileOpenCall() { | ||
| this = [API::builtin("open").getACall(), API::moduleImport("os").getMember("open").getACall()] | ||
| } | ||
| } | ||
|
|
||
| private DataFlow::TypeTrackingNode fileOpenInstance(DataFlow::TypeTracker t) { | ||
| t.start() and | ||
| result instanceof FileOpenSource | ||
| or | ||
| exists(DataFlow::TypeTracker t2 | result = fileOpenInstance(t2).track(t2, t)) | ||
| } | ||
|
|
||
| /** | ||
| * A call that returns an instance of an open file object. | ||
| * This includes calls to methods that transitively call `open` or similar. | ||
| */ | ||
| class FileOpen extends DataFlow::CallCfgNode { | ||
| FileOpen() { fileOpenInstance(DataFlow::TypeTracker::end()).flowsTo(this) } | ||
| } | ||
|
|
||
| /** A call that may wrap a file object in a wrapper class or `os.fdopen`. */ | ||
| class FileWrapperCall extends DataFlow::CallCfgNode { | ||
| DataFlow::Node wrapped; | ||
|
|
||
| FileWrapperCall() { | ||
| wrapped = this.getArg(_).getALocalSource() and | ||
| this.getFunction() = classTracker(_) | ||
tausbn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| or | ||
| wrapped = this.getArg(0) and | ||
| this = API::moduleImport("os").getMember("fdopen").getACall() | ||
| or | ||
| wrapped = this.getArg(0) and | ||
| this = API::moduleImport("django").getMember("http").getMember("FileResponse").getACall() | ||
| } | ||
|
|
||
| /** Gets the file that this call wraps. */ | ||
| DataFlow::Node getWrapped() { result = wrapped } | ||
| } | ||
|
|
||
| /** A node where a file is closed. */ | ||
| abstract class FileClose extends DataFlow::CfgNode { | ||
| /** Holds if this file close will occur if an exception is thrown at `raises`. */ | ||
| predicate guardsExceptions(DataFlow::CfgNode raises) { | ||
| this.asCfgNode() = raises.asCfgNode().getAnExceptionalSuccessor().getASuccessor*() | ||
| or | ||
| // The expression is after the close call. | ||
| // This also covers the body of a `with` statement. | ||
| raises.asCfgNode() = this.asCfgNode().getASuccessor*() | ||
| } | ||
| } | ||
|
|
||
| /** A call to the `.close()` method of a file object. */ | ||
| class FileCloseCall extends FileClose { | ||
| FileCloseCall() { exists(DataFlow::MethodCallNode mc | mc.calls(this, "close")) } | ||
| } | ||
|
|
||
| /** A call to `os.close`. */ | ||
| class OsCloseCall extends FileClose { | ||
| OsCloseCall() { this = API::moduleImport("os").getMember("close").getACall().getArg(0) } | ||
| } | ||
|
|
||
| /** A `with` statement. */ | ||
| class WithStatement extends FileClose { | ||
| WithStatement() { this.asExpr() = any(With w).getContextExpr() } | ||
| } | ||
|
|
||
| /** Holds if an exception may be raised at `raises` if `file` is a file object. */ | ||
| private predicate mayRaiseWithFile(DataFlow::CfgNode file, DataFlow::CfgNode raises) { | ||
| // Currently just consider any method called on `file`; e.g. `file.write()`; as potentially raising an exception | ||
| raises.(DataFlow::MethodCallNode).getObject() = file and | ||
| not file instanceof FileOpen and | ||
| not file instanceof FileClose | ||
| } | ||
|
|
||
| /** Holds if data flows from `nodeFrom` to `nodeTo` in one step that also includes file wrapper classes. */ | ||
| private predicate fileAdditionalLocalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { | ||
| exists(FileWrapperCall fw | nodeFrom = fw.getWrapped() and nodeTo = fw) | ||
| } | ||
|
|
||
| private predicate fileLocalFlowHelper0( | ||
| DataFlow::LocalSourceNode nodeFrom, DataFlow::LocalSourceNode nodeTo | ||
| ) { | ||
| exists(DataFlow::Node nodeMid | | ||
| nodeFrom.flowsTo(nodeMid) and fileAdditionalLocalFlowStep(nodeMid, nodeTo) | ||
| ) | ||
| } | ||
|
|
||
| private predicate fileLocalFlowHelper1( | ||
| DataFlow::LocalSourceNode nodeFrom, DataFlow::LocalSourceNode nodeTo | ||
| ) { | ||
| fileLocalFlowHelper0*(nodeFrom, nodeTo) | ||
| } | ||
|
|
||
| /** Holds if data flows from `source` to `sink`, including file wrapper classes. */ | ||
| pragma[inline] | ||
| private predicate fileLocalFlow(FileOpen source, DataFlow::Node sink) { | ||
| exists(DataFlow::LocalSourceNode mid | fileLocalFlowHelper1(source, mid) and mid.flowsTo(sink)) | ||
| } | ||
|
|
||
| /** Holds if the file opened at `fo` is closed. */ | ||
| predicate fileIsClosed(FileOpen fo) { exists(FileClose fc | fileLocalFlow(fo, fc)) } | ||
|
|
||
| /** Holds if the file opened at `fo` is returned to the caller. This makes the caller responsible for closing the file. */ | ||
| predicate fileIsReturned(FileOpen fo) { | ||
| exists(Return ret, Expr retVal | | ||
| ( | ||
| retVal = ret.getValue() | ||
| or | ||
| retVal = ret.getValue().(List).getAnElt() | ||
| or | ||
| retVal = ret.getValue().(Tuple).getAnElt() | ||
| ) and | ||
tausbn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| fileLocalFlow(fo, DataFlow::exprNode(retVal)) | ||
| ) | ||
| } | ||
|
|
||
| /** Holds if the file opened at `fo` is stored in a field. We assume that another method is then responsible for closing the file. */ | ||
| predicate fileIsStoredInField(FileOpen fo) { | ||
| exists(DataFlow::AttrWrite aw | fileLocalFlow(fo, aw.getValue())) | ||
| } | ||
|
|
||
| /** Holds if the file opened at `fo` is not closed, and is expected to be closed. */ | ||
| predicate fileNotClosed(FileOpen fo) { | ||
| not fileIsClosed(fo) and | ||
| not fileIsReturned(fo) and | ||
| not fileIsStoredInField(fo) | ||
| } | ||
|
|
||
| predicate fileMayNotBeClosedOnException(FileOpen fo, DataFlow::Node raises) { | ||
| fileIsClosed(fo) and | ||
| exists(DataFlow::CfgNode fileRaised | | ||
tausbn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| mayRaiseWithFile(fileRaised, raises) and | ||
| fileLocalFlow(fo, fileRaised) and | ||
| not exists(FileClose fc | | ||
| fileLocalFlow(fo, fc) and | ||
| fc.guardsExceptions(raises) | ||
| ) | ||
| ) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| def bad(): | ||
| f = open("filename", "w") | ||
| f.write("could raise exception") # BAD: This call could raise an exception, leading to the file not being closed. | ||
| f.close() | ||
|
|
||
|
|
||
| def good1(): | ||
| with open("filename", "w") as f: | ||
| f.write("always closed") # GOOD: The `with` statement ensures the file is always closed. | ||
|
|
||
| def good2(): | ||
| f = open("filename", "w") | ||
| try: | ||
| f.write("always closed") | ||
| finally: | ||
| f.close() # GOOD: The `finally` block always ensures the file is closed. | ||
|
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.