|
| 1 | +/** |
| 2 | + * @name Writable file handle closed without error handling |
| 3 | + * @description Errors which occur when closing a writable file handle may result in data loss |
| 4 | + * if the data could not be successfully flushed. Such errors should be handled |
| 5 | + * explicitly. |
| 6 | + * @kind path-problem |
| 7 | + * @problem.severity warning |
| 8 | + * @precision high |
| 9 | + * @id go/unhandled-writable-file-close |
| 10 | + * @tags maintainability |
| 11 | + * correctness |
| 12 | + * call |
| 13 | + * defer |
| 14 | + */ |
| 15 | + |
| 16 | +import go |
| 17 | +import DataFlow::PathGraph |
| 18 | + |
| 19 | +/** |
| 20 | + * Holds if a `flag` for use with `os.OpenFile` implies that the resulting |
| 21 | + * file handle will be writable. |
| 22 | + */ |
| 23 | +predicate isWritable(Entity flag) { |
| 24 | + flag.hasQualifiedName("os", "O_WRONLY") or |
| 25 | + flag.hasQualifiedName("os", "O_RDWR") |
| 26 | +} |
| 27 | + |
| 28 | +/** |
| 29 | + * Gets constant names from `expr`. |
| 30 | + */ |
| 31 | +QualifiedName getConstants(ValueExpr expr) { |
| 32 | + result = expr or |
| 33 | + result = getConstants(expr.getAChild()) |
| 34 | +} |
| 35 | + |
| 36 | +/** |
| 37 | + * The `os.OpenFile` function. |
| 38 | + */ |
| 39 | +class OpenFileFun extends Function { |
| 40 | + OpenFileFun() { this.hasQualifiedName("os", "OpenFile") } |
| 41 | +} |
| 42 | + |
| 43 | +/** |
| 44 | + * The `os.File.Close` function. |
| 45 | + */ |
| 46 | +class CloseFileFun extends Method { |
| 47 | + CloseFileFun() { this.hasQualifiedName("os", "File", "Close") } |
| 48 | +} |
| 49 | + |
| 50 | +/** |
| 51 | + * The `os.File.Sync` function. |
| 52 | + */ |
| 53 | +class SyncFileFun extends Method { |
| 54 | + SyncFileFun() { this.hasQualifiedName("os", "File", "Sync") } |
| 55 | +} |
| 56 | + |
| 57 | +/** |
| 58 | + * Holds if a `call` to a function is "unhandled". That is, it is either |
| 59 | + * deferred or its result is not assigned to anything. |
| 60 | + * |
| 61 | + * TODO: maybe we should check that something is actually done with the result |
| 62 | + */ |
| 63 | +predicate unhandledCall(DataFlow::CallNode call) { |
| 64 | + exists(DeferStmt defer | defer.getCall() = call.asExpr()) or |
| 65 | + exists(ExprStmt stmt | stmt.getExpr() = call.asExpr()) |
| 66 | +} |
| 67 | + |
| 68 | +/** |
| 69 | + * Holds if `source` is a writable file handle returned by a `call` to the |
| 70 | + * `os.OpenFile` function. |
| 71 | + */ |
| 72 | +predicate isWritableFileHandle(DataFlow::Node source, DataFlow::CallNode call) { |
| 73 | + exists(OpenFileFun f, DataFlow::Node flags, QualifiedName flag | |
| 74 | + // check that the source is a result of the call |
| 75 | + source = call.getAResult() and |
| 76 | + // find a call to the os.OpenFile function |
| 77 | + f.getACall() = call and |
| 78 | + // get the flags expression used for opening the file |
| 79 | + call.getArgument(1) = flags and |
| 80 | + // extract individual flags from the argument |
| 81 | + // flag = flag.getAChild*() and |
| 82 | + flag = getConstants(flags.asExpr()) and |
| 83 | + // check for one which signals that the handle will be writable |
| 84 | + // note that we are underestimating here, since the flags may be |
| 85 | + // specified elsewhere |
| 86 | + isWritable(flag.getTarget()) |
| 87 | + ) |
| 88 | +} |
| 89 | + |
| 90 | +/** |
| 91 | + * Holds if `os.File.Close` is called on `sink`. |
| 92 | + */ |
| 93 | +predicate isCloseSink(DataFlow::Node sink, DataFlow::CallNode closeCall) { |
| 94 | + // find calls to the os.File.Close function |
| 95 | + closeCall = any(CloseFileFun f).getACall() and |
| 96 | + // that are unhandled |
| 97 | + unhandledCall(closeCall) and |
| 98 | + // where the function is called on the sink |
| 99 | + closeCall.getReceiver() = sink and |
| 100 | + // and check that it is not dominated by a call to `os.File.Sync`. |
| 101 | + not exists(IR::Instruction syncInstr, DataFlow::Node syncReceiver, DataFlow::CallNode syncCall | |
| 102 | + // match the instruction corresponding to an `os.File.Sync` call with the predecessor |
| 103 | + syncCall.asInstruction() = syncInstr and |
| 104 | + // check that the call to `os.File.Sync` is handled |
| 105 | + isHandledSync(syncReceiver, syncCall) and |
| 106 | + // find a predecessor to `closeCall` in the control flow graph which dominates the call to |
| 107 | + // `os.File.Close` |
| 108 | + syncInstr.dominatesNode(closeCall.asInstruction()) and |
| 109 | + // check that `os.File.Sync` is called on the same object as `os.File.Close` |
| 110 | + exists(DataFlow::SsaNode ssa | ssa.getAUse() = sink and ssa.getAUse() = syncReceiver) |
| 111 | + ) |
| 112 | +} |
| 113 | + |
| 114 | +/** |
| 115 | + * Holds if `os.File.Sync` is called on `sink` and the result of the call is neither |
| 116 | + * deferred nor discarded. |
| 117 | + */ |
| 118 | +predicate isHandledSync(DataFlow::Node sink, DataFlow::CallNode syncCall) { |
| 119 | + // find a call of the `os.File.Sync` function |
| 120 | + syncCall = any(SyncFileFun f).getACall() and |
| 121 | + // match the sink with the object on which the method is called |
| 122 | + syncCall.getReceiver() = sink and |
| 123 | + // check that the result is neither deferred nor discarded |
| 124 | + not unhandledCall(syncCall) |
| 125 | +} |
| 126 | + |
| 127 | +/** |
| 128 | + * A data flow configuration which traces writable file handles resulting from calls to |
| 129 | + * `os.OpenFile` to `os.File.Close` calls on them. |
| 130 | + */ |
| 131 | +class UnhandledFileCloseDataFlowConfiguration extends DataFlow::Configuration { |
| 132 | + UnhandledFileCloseDataFlowConfiguration() { this = "UnhandledCloseWritableHandle" } |
| 133 | + |
| 134 | + override predicate isSource(DataFlow::Node source) { isWritableFileHandle(source, _) } |
| 135 | + |
| 136 | + override predicate isSink(DataFlow::Node sink) { isCloseSink(sink, _) } |
| 137 | +} |
| 138 | + |
| 139 | +from |
| 140 | + UnhandledFileCloseDataFlowConfiguration cfg, DataFlow::PathNode source, |
| 141 | + DataFlow::CallNode openCall, DataFlow::PathNode sink, DataFlow::CallNode closeCall |
| 142 | +where |
| 143 | + // find data flow from an `os.OpenFile` call to an `os.File.Close` call |
| 144 | + // where the handle is writable |
| 145 | + cfg.hasFlowPath(source, sink) and |
| 146 | + isWritableFileHandle(source.getNode(), openCall) and |
| 147 | + // get the `CallNode` corresponding to the sink |
| 148 | + isCloseSink(sink.getNode(), closeCall) |
| 149 | +select sink, source, sink, |
| 150 | + "File handle may be writable as a result of data flow from a $@ and closing it may result in data loss upon failure, which is not handled explicitly.", |
| 151 | + openCall, openCall.toString() |
0 commit comments