-
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
Changes from 8 commits
09694c4
ecb3050
c8fc565
f750e22
f8a0b1c
b2acfbc
2c74ddb
3707f10
bdbdcf8
a46c157
0fa70db
d23c3b8
2fd9b16
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,74 +1,25 @@ | ||
| /** | ||
| * @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 cause data loss or resource leaks. | ||
| * @kind problem | ||
| * @tags efficiency | ||
| * correctness | ||
| * resources | ||
| * 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| /** 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 `e`. */ | ||
|
||
| 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 { | ||
| With w; | ||
|
||
|
|
||
| WithStatement() { this.asExpr() = 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 `node`; 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 fileLocalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { | ||
| DataFlow::localFlowStep(nodeFrom, nodeTo) | ||
| or | ||
| exists(FileWrapperCall fw | nodeFrom = fw.getWrapped() and nodeTo = fw) | ||
| } | ||
|
||
|
|
||
| /** Holds if data flows from `source` to `sink`, including file wrapper classes. */ | ||
| private predicate fileLocalFlow(DataFlow::Node source, DataFlow::Node sink) { | ||
| fileLocalFlowStep*(source, 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) | ||
| ) | ||
| ) | ||
| } | ||
| 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. | ||
|
|
Uh oh!
There was an error while loading. Please reload this page.