|
12 | 12 | import go |
13 | 13 |
|
14 | 14 | /** |
15 | | - * A type that represents either `*Repository` or a Storage type that needs closing. |
| 15 | + * A function that creates a Repository |
16 | 16 | */ |
17 | | -class ResourceType extends Type { |
18 | | - ResourceType() { |
19 | | - // Repository type |
20 | | - this.(PointerType).getBaseType().hasQualifiedName("github.com/go-git/go-git/v6", "Repository") |
21 | | - or |
22 | | - // filesystem.Storage |
23 | | - this.(PointerType).getBaseType().hasQualifiedName("github.com/go-git/go-git/v6/storage/filesystem", "Storage") |
24 | | - or |
25 | | - // transactional.Storage |
26 | | - this.(PointerType).getBaseType().hasQualifiedName("github.com/go-git/go-git/v6/storage/transactional", "Storage") |
| 17 | +class RepositoryCreation extends DataFlow::CallNode { |
| 18 | + RepositoryCreation() { |
| 19 | + this.getTarget().hasQualifiedName("github.com/go-git/go-git/v6", [ |
| 20 | + "PlainOpen", "PlainOpenWithOptions", "PlainClone", "PlainCloneContext", |
| 21 | + "PlainInit", "Init", "Clone", "CloneContext", "Open" |
| 22 | + ]) |
27 | 23 | } |
28 | 24 | } |
29 | 25 |
|
30 | 26 | /** |
31 | | - * A function call that creates a resource (Repository or Storage). |
| 27 | + * A function that creates a Storage |
32 | 28 | */ |
33 | | -class ResourceCreation extends CallExpr { |
34 | | - ResourceCreation() { |
35 | | - exists(Function f | f = this.getTarget() | |
36 | | - // Repository creation functions |
37 | | - f.hasQualifiedName("github.com/go-git/go-git/v6", ["PlainOpen", "PlainOpenWithOptions", "PlainClone", "PlainCloneContext", "PlainInit", "Init", "Clone", "CloneContext", "Open"]) |
38 | | - or |
39 | | - // Storage creation functions |
40 | | - f.hasQualifiedName("github.com/go-git/go-git/v6/storage/filesystem", ["NewStorage", "NewStorageWithOptions"]) |
41 | | - or |
42 | | - f.hasQualifiedName("github.com/go-git/go-git/v6/storage/transactional", "NewStorage") |
43 | | - or |
44 | | - // Submodule.Repository() method |
45 | | - f.hasQualifiedName("github.com/go-git/go-git/v6", "Submodule", "Repository") |
46 | | - or |
47 | | - // Worktree.Repository() method |
48 | | - f.hasQualifiedName("github.com/go-git/go-git/v6", "Worktree", "Repository") |
49 | | - or |
50 | | - // Repository.Worktree() method returns a Worktree with repository field |
51 | | - f.hasQualifiedName("github.com/go-git/go-git/v6", "Repository", "Worktree") |
52 | | - ) |
| 29 | +class StorageCreation extends DataFlow::CallNode { |
| 30 | + StorageCreation() { |
| 31 | + this.getTarget().hasQualifiedName("github.com/go-git/go-git/v6/storage/filesystem", [ |
| 32 | + "NewStorage", "NewStorageWithOptions" |
| 33 | + ]) |
| 34 | + or |
| 35 | + this.getTarget().hasQualifiedName("github.com/go-git/go-git/v6/storage/transactional", "NewStorage") |
53 | 36 | } |
54 | 37 | } |
55 | 38 |
|
56 | 39 | /** |
57 | | - * A call to Close() method on a resource. |
| 40 | + * A function that returns a Repository or Storage (factory function). |
| 41 | + * Resources returned by factory functions are the caller's responsibility to close. |
58 | 42 | */ |
59 | | -class CloseCall extends MethodCall { |
| 43 | +predicate isFactoryFunction(FuncDef f) { |
| 44 | + exists(Type resultType | |
| 45 | + resultType = f.getType().getResultType(0) | |
| 46 | + resultType.getUnderlyingType().(PointerType).getBaseType().hasQualifiedName("github.com/go-git/go-git/v6", "Repository") |
| 47 | + or |
| 48 | + resultType.getUnderlyingType().(PointerType).getBaseType().hasQualifiedName("github.com/go-git/go-git/v6/storage/filesystem", "Storage") |
| 49 | + or |
| 50 | + resultType.getUnderlyingType().(PointerType).getBaseType().hasQualifiedName("github.com/go-git/go-git/v6/storage/transactional", "Storage") |
| 51 | + or |
| 52 | + // Storage-related interfaces |
| 53 | + resultType.getName() = ["Storer", "EncodedObjectStorer"] |
| 54 | + ) |
| 55 | +} |
| 56 | + |
| 57 | +/** |
| 58 | + * A call to Close() method |
| 59 | + */ |
| 60 | +class CloseCall extends DataFlow::MethodCallNode { |
60 | 61 | CloseCall() { |
61 | | - this.getTarget().getName() = "Close" and |
62 | | - this.getReceiver().getType() instanceof ResourceType |
| 62 | + this.getTarget().getName() = "Close" |
63 | 63 | } |
64 | 64 | } |
65 | 65 |
|
66 | 66 | /** |
67 | | - * Checks if a variable has a Close() call (direct or in defer) in the same function. |
| 67 | + * Checks if there's a direct Close() call using dataflow. |
68 | 68 | */ |
69 | | -predicate hasCloseCall(SsaVariable v) { |
| 69 | +predicate hasDirectClose(DataFlow::Node resource, FuncDef f) { |
70 | 70 | exists(CloseCall close | |
71 | | - close.getReceiver() = v.getAUse() |
| 71 | + DataFlow::localFlow(resource, close.getReceiver()) and |
| 72 | + close.asExpr().getEnclosingFunction() = f |
72 | 73 | ) |
73 | | - or |
74 | | - // Check for defer Close() patterns |
75 | | - exists(DeferStmt defer, CloseCall close | |
76 | | - defer.getCall() = close and |
77 | | - close.getReceiver() = v.getAUse() |
| 74 | +} |
| 75 | + |
| 76 | +/** |
| 77 | + * Checks if there's a Close() call on the same variable name. |
| 78 | + * This handles cases where dataflow doesn't track through embedded fields. |
| 79 | + */ |
| 80 | +predicate hasCloseOnSameVariable(DataFlow::CallNode create, FuncDef f) { |
| 81 | + exists(CallExpr closeCall, SelectorExpr sel, Ident closeVar, Ident createVar, string varName | |
| 82 | + // The Close() call |
| 83 | + closeCall.getEnclosingFunction() = f and |
| 84 | + sel.getParent() = closeCall and |
| 85 | + sel.getSelector().getName() = "Close" and |
| 86 | + closeVar = sel.getBase() and |
| 87 | + closeVar.getName() = varName and |
| 88 | + // The creation assignment (handles both := and =) |
| 89 | + ( |
| 90 | + create.asExpr().getParent().(DefineStmt).getLhs() = createVar or |
| 91 | + create.asExpr().getParent().(AssignStmt).getLhs() = createVar |
| 92 | + ) and |
| 93 | + createVar.getName() = varName |
78 | 94 | ) |
79 | | - or |
80 | | - // Check for defer func() { _ = x.Close() }() patterns |
81 | | - exists(DeferStmt defer, FuncLit fn, AssignStmt assign, CloseCall close | |
82 | | - defer.getCall().(CallExpr).getCalleeExpr() = fn and |
83 | | - fn.getBody().getAStmt() = assign and |
84 | | - assign.getRhs(0) = close and |
85 | | - close.getReceiver() = v.getAUse() |
| 95 | +} |
| 96 | + |
| 97 | +/** |
| 98 | + * Checks if there's a defer statement with Close() in the function. |
| 99 | + * This is a conservative check - if there's any defer Close() in the function, |
| 100 | + * we assume the resource might be cleaned up (to avoid false positives). |
| 101 | + */ |
| 102 | +predicate hasDeferWithClose(FuncDef f) { |
| 103 | + exists(DeferStmt defer, SelectorExpr sel | |
| 104 | + defer.getEnclosingFunction() = f and |
| 105 | + sel.getParent+() = defer and |
| 106 | + sel.getSelector().getName() = "Close" |
| 107 | + ) |
| 108 | +} |
| 109 | + |
| 110 | +/** |
| 111 | + * Checks if there's a testing.TB.Cleanup() call with Close() in the function. |
| 112 | + * This handles patterns like: t.Cleanup(func() { _ = r.Close() }) |
| 113 | + */ |
| 114 | +predicate hasTestingCleanupWithClose(FuncDef f) { |
| 115 | + exists(DataFlow::CallNode cleanup, FuncLit cleanupFunc, SelectorExpr sel | |
| 116 | + cleanup.getTarget().getName() = "Cleanup" and |
| 117 | + cleanup.asExpr().getEnclosingFunction() = f and |
| 118 | + cleanupFunc = cleanup.getArgument(0).asExpr() and |
| 119 | + sel.getParent+() = cleanupFunc and |
| 120 | + sel.getSelector().getName() = "Close" |
86 | 121 | ) |
87 | 122 | } |
88 | 123 |
|
89 | | -from ResourceCreation create, SsaVariable v |
| 124 | +/** |
| 125 | + * Checks if the resource is cleaned up. |
| 126 | + */ |
| 127 | +predicate hasCleanup(DataFlow::CallNode create, DataFlow::Node resource, FuncDef f) { |
| 128 | + hasDirectClose(resource, f) |
| 129 | + or |
| 130 | + hasDeferWithClose(f) |
| 131 | + or |
| 132 | + hasTestingCleanupWithClose(f) |
| 133 | + or |
| 134 | + hasCloseOnSameVariable(create, f) |
| 135 | +} |
| 136 | + |
| 137 | +from DataFlow::CallNode create, DataFlow::Node resource, FuncDef enclosingFunc |
90 | 138 | where |
91 | | - // The resource is assigned to a variable |
92 | | - v.getDefinition().(SsaExplicitDefinition).getInstruction().getNode() = create and |
93 | | - // The variable is not closed |
94 | | - not hasCloseCall(v) and |
95 | | - // The variable is not assigned to a field (which might be closed elsewhere) |
96 | | - not exists(Field f | v.getAUse() = f.getAWrite().getRhs()) and |
97 | | - // The variable is not returned (caller's responsibility) |
98 | | - not exists(ReturnStmt ret | v.getAUse() = ret.getExpr()) |
99 | | -select create, "This resource is created but never closed, which may cause file handle leaks." |
| 139 | + (create instanceof RepositoryCreation or create instanceof StorageCreation) and |
| 140 | + resource = create.getResult(0) and |
| 141 | + enclosingFunc = create.asExpr().getEnclosingFunction() and |
| 142 | + // Check if there's no cleanup for this resource |
| 143 | + not hasCleanup(create, resource, enclosingFunc) and |
| 144 | + // Exclude factory functions (return Repository/Storage to caller) |
| 145 | + not isFactoryFunction(enclosingFunc) and |
| 146 | + // Exclude resources assigned to struct fields (managed by struct lifecycle) |
| 147 | + not exists(StructLit lit | |
| 148 | + lit.getAnElement().(KeyValueExpr).getValue() = resource.asExpr() |
| 149 | + ) and |
| 150 | + // Exclude direct calls to memory.NewStorage (doesn't need closing) |
| 151 | + not create.getTarget().hasQualifiedName("github.com/go-git/go-git/v6/storage/memory", "NewStorage") |
| 152 | +select create.asExpr(), |
| 153 | + "Resource created but may not be closed. " + |
| 154 | + "Always call defer func() { _ = r.Close() }() after creating Repository or Storage instances." |
0 commit comments