circle-ir 3.148.0 → 3.150.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/passes/cli-main-reflection-suppress-pass.d.ts +94 -0
- package/dist/analysis/passes/cli-main-reflection-suppress-pass.d.ts.map +1 -0
- package/dist/analysis/passes/cli-main-reflection-suppress-pass.js +337 -0
- package/dist/analysis/passes/cli-main-reflection-suppress-pass.js.map +1 -0
- package/dist/analysis/passes/resource-leak-pass.d.ts +26 -0
- package/dist/analysis/passes/resource-leak-pass.d.ts.map +1 -1
- package/dist/analysis/passes/resource-leak-pass.js +138 -0
- package/dist/analysis/passes/resource-leak-pass.js.map +1 -1
- package/dist/analysis/passes/sink-semantics-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-semantics-pass.js +10 -1
- package/dist/analysis/passes/sink-semantics-pass.js.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +10 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +315 -5
- package/package.json +1 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CliMainReflectionSuppressPass — cognium-dev #162 Option B
|
|
3
|
+
*
|
|
4
|
+
* Drops Java reflection `code_injection` sinks in files that look like
|
|
5
|
+
* a fat-jar CLI tool's own artifact — files that ship a `main(String[])`
|
|
6
|
+
* entry point AND carry no web-framework entry-point signal anywhere
|
|
7
|
+
* in the file. The `main(String[])` on the same compilation unit *is*
|
|
8
|
+
* the trust boundary; the caller is the OS shell that invoked the jar.
|
|
9
|
+
*
|
|
10
|
+
* Motivation (from the ticket):
|
|
11
|
+
*
|
|
12
|
+
* // antlr/tool/.../TestRig.java — 5 reflection sinks reported HIGH
|
|
13
|
+
* public static void main(String[] args) throws Exception {
|
|
14
|
+
* TestRig testRig = new TestRig(args);
|
|
15
|
+
* ...
|
|
16
|
+
* }
|
|
17
|
+
* public void process() throws Exception {
|
|
18
|
+
* String lexerName = grammarName + "Lexer";
|
|
19
|
+
* ClassLoader cl = Thread.currentThread().getContextClassLoader();
|
|
20
|
+
* Class<? extends Lexer> lexerClass =
|
|
21
|
+
* cl.loadClass(lexerName).asSubclass(Lexer.class); // sink
|
|
22
|
+
* Constructor<? extends Lexer> lexerCtor =
|
|
23
|
+
* lexerClass.getConstructor(CharStream.class);
|
|
24
|
+
* Lexer lexer = lexerCtor.newInstance((CharStream)null); // sink
|
|
25
|
+
* ...
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* This is documented behaviour of ANTLR's `TestRig` developer CLI —
|
|
29
|
+
* same shape as `javac`, `java -jar`, `python -m`. The tool's whole
|
|
30
|
+
* purpose is "give me a grammar name from the command line and I'll
|
|
31
|
+
* run it." Blocked 5/49 HIGH FPs in the top-100 Java harness.
|
|
32
|
+
*
|
|
33
|
+
* ## Signal
|
|
34
|
+
*
|
|
35
|
+
* Language MUST be Java. Per-file signal (all must hold):
|
|
36
|
+
*
|
|
37
|
+
* 1. Some type in the file declares a `main(String[])` method
|
|
38
|
+
* (matches `looksLikeMainMethod` in `entry-point-detection.ts`).
|
|
39
|
+
* 2. NO type in the file carries a Tier-1 class-level web-framework
|
|
40
|
+
* annotation (`@RestController`, `@Controller`, `@Service`,
|
|
41
|
+
* `@Component`, `@Path`, `@WebServlet`, `@ServerEndpoint`,
|
|
42
|
+
* `@FeignClient`, `@Repository`).
|
|
43
|
+
* 3. NO method in the file carries a Tier-1 method-level
|
|
44
|
+
* web-framework annotation (Spring `@*Mapping`, `@KafkaListener`,
|
|
45
|
+
* `@JmsListener`, JAX-RS `@GET/@POST/…`, `@Scheduled`, `@Path`,
|
|
46
|
+
* `@DataBoundConstructor`/`@DataBoundSetter`).
|
|
47
|
+
* 4. NO type in the file extends a Tier-1 supertype (HttpServlet,
|
|
48
|
+
* GenericServlet, Filter, HandlerInterceptor, CommandLineRunner,
|
|
49
|
+
* SimpleChannelInboundHandler, etc.).
|
|
50
|
+
*
|
|
51
|
+
* ## Action
|
|
52
|
+
*
|
|
53
|
+
* When the signal fires, drop from the `SinkFilterResult.sinks` list
|
|
54
|
+
* (the authoritative sink array consumed by `TaintPropagationPass` and
|
|
55
|
+
* folded into `taint.sinks` by `analyzer.ts`) every sink whose
|
|
56
|
+
* `type === 'code_injection'` AND `method` is a Java reflection
|
|
57
|
+
* / ClassLoader surface method. The gate does NOT touch other
|
|
58
|
+
* `code_injection` shapes: ScriptEngine.eval / GroovyShell.evaluate /
|
|
59
|
+
* SpEL `parseExpression` remain flagged in CLI tools because they
|
|
60
|
+
* evaluate scripts (a genuinely dangerous CLI misuse), not just class
|
|
61
|
+
* names.
|
|
62
|
+
*
|
|
63
|
+
* ## Pipeline slot
|
|
64
|
+
*
|
|
65
|
+
* Runs after `SinkSemanticsPass` (so its curated-registry drops have
|
|
66
|
+
* already fired) and before `TaintPropagationPass` (so the flow
|
|
67
|
+
* generators never see the dropped sinks). Emits no findings.
|
|
68
|
+
*
|
|
69
|
+
* ## Pillar I / safety
|
|
70
|
+
*
|
|
71
|
+
* - Pure per-file heuristic; no LLM, no filesystem, no config knobs.
|
|
72
|
+
* - False-negative-safe: any web-framework signal in the file
|
|
73
|
+
* (annotation OR supertype OR Tier-1 method annotation) disables
|
|
74
|
+
* the gate. Recall preserved for `@RestController` classes,
|
|
75
|
+
* `HttpServlet` subclasses, Netty handlers, Jenkins plugins, and
|
|
76
|
+
* Kafka/JMS listeners that happen to also expose a `main`.
|
|
77
|
+
* - Reflection-only: `Runtime.exec`, `ProcessBuilder.start`,
|
|
78
|
+
* `Statement.execute`, `ObjectInputStream.readObject`, SpEL,
|
|
79
|
+
* Groovy, ScriptEngine — all unaffected.
|
|
80
|
+
* - Java-only: non-Java sources fall through untouched.
|
|
81
|
+
*/
|
|
82
|
+
import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
|
|
83
|
+
export interface CliMainReflectionSuppressResult {
|
|
84
|
+
/** Whether the fat-jar CLI signal fired for this file. */
|
|
85
|
+
cliMainSignal: boolean;
|
|
86
|
+
/** Number of reflection `code_injection` sinks dropped. */
|
|
87
|
+
droppedCount: number;
|
|
88
|
+
}
|
|
89
|
+
export declare class CliMainReflectionSuppressPass implements AnalysisPass<CliMainReflectionSuppressResult> {
|
|
90
|
+
readonly name = "cli-main-reflection-suppress";
|
|
91
|
+
readonly category: "security";
|
|
92
|
+
run(ctx: PassContext): CliMainReflectionSuppressResult;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=cli-main-reflection-suppress-pass.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-main-reflection-suppress-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/cli-main-reflection-suppress-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgFG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAG9E,MAAM,WAAW,+BAA+B;IAC9C,0DAA0D;IAC1D,aAAa,EAAE,OAAO,CAAC;IACvB,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;CACtB;AAiKD,qBAAa,6BACX,YAAW,YAAY,CAAC,+BAA+B,CAAC;IAExD,QAAQ,CAAC,IAAI,kCAAkC;IAC/C,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,+BAA+B;CA+FvD"}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CliMainReflectionSuppressPass — cognium-dev #162 Option B
|
|
3
|
+
*
|
|
4
|
+
* Drops Java reflection `code_injection` sinks in files that look like
|
|
5
|
+
* a fat-jar CLI tool's own artifact — files that ship a `main(String[])`
|
|
6
|
+
* entry point AND carry no web-framework entry-point signal anywhere
|
|
7
|
+
* in the file. The `main(String[])` on the same compilation unit *is*
|
|
8
|
+
* the trust boundary; the caller is the OS shell that invoked the jar.
|
|
9
|
+
*
|
|
10
|
+
* Motivation (from the ticket):
|
|
11
|
+
*
|
|
12
|
+
* // antlr/tool/.../TestRig.java — 5 reflection sinks reported HIGH
|
|
13
|
+
* public static void main(String[] args) throws Exception {
|
|
14
|
+
* TestRig testRig = new TestRig(args);
|
|
15
|
+
* ...
|
|
16
|
+
* }
|
|
17
|
+
* public void process() throws Exception {
|
|
18
|
+
* String lexerName = grammarName + "Lexer";
|
|
19
|
+
* ClassLoader cl = Thread.currentThread().getContextClassLoader();
|
|
20
|
+
* Class<? extends Lexer> lexerClass =
|
|
21
|
+
* cl.loadClass(lexerName).asSubclass(Lexer.class); // sink
|
|
22
|
+
* Constructor<? extends Lexer> lexerCtor =
|
|
23
|
+
* lexerClass.getConstructor(CharStream.class);
|
|
24
|
+
* Lexer lexer = lexerCtor.newInstance((CharStream)null); // sink
|
|
25
|
+
* ...
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* This is documented behaviour of ANTLR's `TestRig` developer CLI —
|
|
29
|
+
* same shape as `javac`, `java -jar`, `python -m`. The tool's whole
|
|
30
|
+
* purpose is "give me a grammar name from the command line and I'll
|
|
31
|
+
* run it." Blocked 5/49 HIGH FPs in the top-100 Java harness.
|
|
32
|
+
*
|
|
33
|
+
* ## Signal
|
|
34
|
+
*
|
|
35
|
+
* Language MUST be Java. Per-file signal (all must hold):
|
|
36
|
+
*
|
|
37
|
+
* 1. Some type in the file declares a `main(String[])` method
|
|
38
|
+
* (matches `looksLikeMainMethod` in `entry-point-detection.ts`).
|
|
39
|
+
* 2. NO type in the file carries a Tier-1 class-level web-framework
|
|
40
|
+
* annotation (`@RestController`, `@Controller`, `@Service`,
|
|
41
|
+
* `@Component`, `@Path`, `@WebServlet`, `@ServerEndpoint`,
|
|
42
|
+
* `@FeignClient`, `@Repository`).
|
|
43
|
+
* 3. NO method in the file carries a Tier-1 method-level
|
|
44
|
+
* web-framework annotation (Spring `@*Mapping`, `@KafkaListener`,
|
|
45
|
+
* `@JmsListener`, JAX-RS `@GET/@POST/…`, `@Scheduled`, `@Path`,
|
|
46
|
+
* `@DataBoundConstructor`/`@DataBoundSetter`).
|
|
47
|
+
* 4. NO type in the file extends a Tier-1 supertype (HttpServlet,
|
|
48
|
+
* GenericServlet, Filter, HandlerInterceptor, CommandLineRunner,
|
|
49
|
+
* SimpleChannelInboundHandler, etc.).
|
|
50
|
+
*
|
|
51
|
+
* ## Action
|
|
52
|
+
*
|
|
53
|
+
* When the signal fires, drop from the `SinkFilterResult.sinks` list
|
|
54
|
+
* (the authoritative sink array consumed by `TaintPropagationPass` and
|
|
55
|
+
* folded into `taint.sinks` by `analyzer.ts`) every sink whose
|
|
56
|
+
* `type === 'code_injection'` AND `method` is a Java reflection
|
|
57
|
+
* / ClassLoader surface method. The gate does NOT touch other
|
|
58
|
+
* `code_injection` shapes: ScriptEngine.eval / GroovyShell.evaluate /
|
|
59
|
+
* SpEL `parseExpression` remain flagged in CLI tools because they
|
|
60
|
+
* evaluate scripts (a genuinely dangerous CLI misuse), not just class
|
|
61
|
+
* names.
|
|
62
|
+
*
|
|
63
|
+
* ## Pipeline slot
|
|
64
|
+
*
|
|
65
|
+
* Runs after `SinkSemanticsPass` (so its curated-registry drops have
|
|
66
|
+
* already fired) and before `TaintPropagationPass` (so the flow
|
|
67
|
+
* generators never see the dropped sinks). Emits no findings.
|
|
68
|
+
*
|
|
69
|
+
* ## Pillar I / safety
|
|
70
|
+
*
|
|
71
|
+
* - Pure per-file heuristic; no LLM, no filesystem, no config knobs.
|
|
72
|
+
* - False-negative-safe: any web-framework signal in the file
|
|
73
|
+
* (annotation OR supertype OR Tier-1 method annotation) disables
|
|
74
|
+
* the gate. Recall preserved for `@RestController` classes,
|
|
75
|
+
* `HttpServlet` subclasses, Netty handlers, Jenkins plugins, and
|
|
76
|
+
* Kafka/JMS listeners that happen to also expose a `main`.
|
|
77
|
+
* - Reflection-only: `Runtime.exec`, `ProcessBuilder.start`,
|
|
78
|
+
* `Statement.execute`, `ObjectInputStream.readObject`, SpEL,
|
|
79
|
+
* Groovy, ScriptEngine — all unaffected.
|
|
80
|
+
* - Java-only: non-Java sources fall through untouched.
|
|
81
|
+
*/
|
|
82
|
+
/**
|
|
83
|
+
* Java reflection + ClassLoader methods that produce `code_injection`
|
|
84
|
+
* sinks via `configs/sinks/code_injection.yaml`. Simple-name match on
|
|
85
|
+
* `sink.method`. Kept intentionally narrow — script-engine /
|
|
86
|
+
* expression-language sinks (`eval`, `evaluate`, `parseExpression`,
|
|
87
|
+
* `getBeanInfo`, `getEngineByName`) are NOT in this set, so those
|
|
88
|
+
* still fire in CLI artifacts.
|
|
89
|
+
*/
|
|
90
|
+
const REFLECTION_SINK_METHODS = new Set([
|
|
91
|
+
'forName',
|
|
92
|
+
'newInstance',
|
|
93
|
+
'invoke',
|
|
94
|
+
'getMethod',
|
|
95
|
+
'getDeclaredMethod',
|
|
96
|
+
'getConstructor',
|
|
97
|
+
'getDeclaredConstructor',
|
|
98
|
+
'loadClass',
|
|
99
|
+
'defineClass',
|
|
100
|
+
]);
|
|
101
|
+
/**
|
|
102
|
+
* Tier-1 class-level web-framework annotations. Simple-name match
|
|
103
|
+
* (annotation strings in `TypeInfo.annotations` may include the `@`
|
|
104
|
+
* prefix and generic args — we strip both). Any single match kills
|
|
105
|
+
* the gate for this file.
|
|
106
|
+
*/
|
|
107
|
+
const TIER_1_CLASS_ANNOTATIONS = new Set([
|
|
108
|
+
// Spring MVC
|
|
109
|
+
'RestController',
|
|
110
|
+
'Controller',
|
|
111
|
+
// Spring stereotype beans
|
|
112
|
+
'Service',
|
|
113
|
+
'Repository',
|
|
114
|
+
'Component',
|
|
115
|
+
// JAX-RS resource class
|
|
116
|
+
'Path',
|
|
117
|
+
// Servlet 3.0 annotation-based servlet
|
|
118
|
+
'WebServlet',
|
|
119
|
+
// JSR-356 WebSocket endpoint
|
|
120
|
+
'ServerEndpoint',
|
|
121
|
+
// Declarative HTTP client
|
|
122
|
+
'FeignClient',
|
|
123
|
+
]);
|
|
124
|
+
/**
|
|
125
|
+
* Tier-1 method-level web-framework annotations. Any single match
|
|
126
|
+
* kills the gate.
|
|
127
|
+
*/
|
|
128
|
+
const TIER_1_METHOD_ANNOTATIONS = new Set([
|
|
129
|
+
// Spring MVC
|
|
130
|
+
'RequestMapping',
|
|
131
|
+
'GetMapping',
|
|
132
|
+
'PostMapping',
|
|
133
|
+
'PutMapping',
|
|
134
|
+
'DeleteMapping',
|
|
135
|
+
'PatchMapping',
|
|
136
|
+
// Spring messaging / WebSocket
|
|
137
|
+
'MessageMapping',
|
|
138
|
+
'SubscribeMapping',
|
|
139
|
+
// Spring messaging listeners
|
|
140
|
+
'KafkaListener',
|
|
141
|
+
'KafkaHandler',
|
|
142
|
+
'RabbitListener',
|
|
143
|
+
'RabbitHandler',
|
|
144
|
+
'JmsListener',
|
|
145
|
+
'StreamListener',
|
|
146
|
+
// Spring Cloud AWS
|
|
147
|
+
'SqsListener',
|
|
148
|
+
'SqsHandler',
|
|
149
|
+
// Spring application events
|
|
150
|
+
'EventListener',
|
|
151
|
+
// CRON / scheduled
|
|
152
|
+
'Scheduled',
|
|
153
|
+
// JAX-RS
|
|
154
|
+
'Path',
|
|
155
|
+
'GET',
|
|
156
|
+
'POST',
|
|
157
|
+
'PUT',
|
|
158
|
+
'DELETE',
|
|
159
|
+
'PATCH',
|
|
160
|
+
'HEAD',
|
|
161
|
+
'OPTIONS',
|
|
162
|
+
// Jenkins Stapler form-binding
|
|
163
|
+
'DataBoundConstructor',
|
|
164
|
+
'DataBoundSetter',
|
|
165
|
+
]);
|
|
166
|
+
/**
|
|
167
|
+
* Tier-1 supertypes whose subclasses declare framework entry points
|
|
168
|
+
* via lifecycle methods (HttpServlet.doGet, ChannelHandler.channelRead,
|
|
169
|
+
* CommandLineRunner.run, etc.). Simple-name match on
|
|
170
|
+
* `TypeInfo.extends` and each `TypeInfo.implements[]` entry (generics
|
|
171
|
+
* stripped).
|
|
172
|
+
*/
|
|
173
|
+
const TIER_1_SUPERTYPES = new Set([
|
|
174
|
+
'HttpServlet',
|
|
175
|
+
'GenericServlet',
|
|
176
|
+
'Filter',
|
|
177
|
+
'HandlerInterceptor',
|
|
178
|
+
'AsyncHandlerInterceptor',
|
|
179
|
+
'CommandLineRunner',
|
|
180
|
+
'ApplicationRunner',
|
|
181
|
+
'SimpleChannelInboundHandler',
|
|
182
|
+
'ChannelInboundHandler',
|
|
183
|
+
'ChannelInboundHandlerAdapter',
|
|
184
|
+
'ChannelDuplexHandler',
|
|
185
|
+
'NettyRequestProcessor',
|
|
186
|
+
'Converter',
|
|
187
|
+
'SingleValueConverter',
|
|
188
|
+
'ConverterMatcher',
|
|
189
|
+
'AbstractReflectionConverter',
|
|
190
|
+
'AbstractSingleValueConverter',
|
|
191
|
+
'AbstractCollectionConverter',
|
|
192
|
+
]);
|
|
193
|
+
/**
|
|
194
|
+
* Normalize an annotation string to its simple name. Handles `@Foo`,
|
|
195
|
+
* `Foo`, `@Foo(...)`, `pkg.Foo`, and `Foo<T>` shapes. Returns the
|
|
196
|
+
* bare identifier (`Foo`) for the lookup.
|
|
197
|
+
*/
|
|
198
|
+
function normalizeAnnotation(raw) {
|
|
199
|
+
let s = raw.trim();
|
|
200
|
+
if (s.startsWith('@'))
|
|
201
|
+
s = s.slice(1);
|
|
202
|
+
const parenIdx = s.indexOf('(');
|
|
203
|
+
if (parenIdx >= 0)
|
|
204
|
+
s = s.slice(0, parenIdx);
|
|
205
|
+
const genericIdx = s.indexOf('<');
|
|
206
|
+
if (genericIdx >= 0)
|
|
207
|
+
s = s.slice(0, genericIdx);
|
|
208
|
+
const dotIdx = s.lastIndexOf('.');
|
|
209
|
+
if (dotIdx >= 0)
|
|
210
|
+
s = s.slice(dotIdx + 1);
|
|
211
|
+
return s.trim();
|
|
212
|
+
}
|
|
213
|
+
/** Strip generics + package qualifier from a supertype reference. */
|
|
214
|
+
function normalizeSupertype(raw) {
|
|
215
|
+
let s = raw.trim();
|
|
216
|
+
const genericIdx = s.indexOf('<');
|
|
217
|
+
if (genericIdx >= 0)
|
|
218
|
+
s = s.slice(0, genericIdx);
|
|
219
|
+
const dotIdx = s.lastIndexOf('.');
|
|
220
|
+
if (dotIdx >= 0)
|
|
221
|
+
s = s.slice(dotIdx + 1);
|
|
222
|
+
return s.trim();
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Detect the `main(String[] args)` entry-point shape. Static isn't
|
|
226
|
+
* required by the JVM launcher spec strictly (some launchers accept
|
|
227
|
+
* instance mains), but every real Java CLI uses `public static void
|
|
228
|
+
* main(String[])`. We match on the parameter shape only — one string
|
|
229
|
+
* array — matching `looksLikeMainMethod` in
|
|
230
|
+
* `src/analysis/entry-point-detection.ts`.
|
|
231
|
+
*/
|
|
232
|
+
function isMainMethod(name, paramTypes) {
|
|
233
|
+
if (name !== 'main')
|
|
234
|
+
return false;
|
|
235
|
+
if (paramTypes.length !== 1)
|
|
236
|
+
return false;
|
|
237
|
+
const t = paramTypes[0];
|
|
238
|
+
if (!t)
|
|
239
|
+
return false;
|
|
240
|
+
const bare = t.replace(/\s+/g, '');
|
|
241
|
+
return bare === 'String[]' || bare === 'java.lang.String[]';
|
|
242
|
+
}
|
|
243
|
+
export class CliMainReflectionSuppressPass {
|
|
244
|
+
name = 'cli-main-reflection-suppress';
|
|
245
|
+
category = 'security';
|
|
246
|
+
run(ctx) {
|
|
247
|
+
const { graph, language } = ctx;
|
|
248
|
+
// Java-only gate. Non-Java IR falls through untouched.
|
|
249
|
+
if (language !== 'java') {
|
|
250
|
+
return { cliMainSignal: false, droppedCount: 0 };
|
|
251
|
+
}
|
|
252
|
+
const types = graph.ir.types;
|
|
253
|
+
if (!types || types.length === 0) {
|
|
254
|
+
return { cliMainSignal: false, droppedCount: 0 };
|
|
255
|
+
}
|
|
256
|
+
// Signal condition (1): main(String[]) present anywhere in file.
|
|
257
|
+
let hasMain = false;
|
|
258
|
+
// Disqualifiers (2), (3), (4): any Tier-1 web-framework signal.
|
|
259
|
+
let hasFrameworkSignal = false;
|
|
260
|
+
for (const type of types) {
|
|
261
|
+
// (2) class-level annotations
|
|
262
|
+
for (const ann of type.annotations) {
|
|
263
|
+
if (TIER_1_CLASS_ANNOTATIONS.has(normalizeAnnotation(ann))) {
|
|
264
|
+
hasFrameworkSignal = true;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (hasFrameworkSignal)
|
|
269
|
+
break;
|
|
270
|
+
// (4) supertype match — `extends` slot + every `implements` slot.
|
|
271
|
+
if (type.extends && TIER_1_SUPERTYPES.has(normalizeSupertype(type.extends))) {
|
|
272
|
+
hasFrameworkSignal = true;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
for (const impl of type.implements) {
|
|
276
|
+
if (TIER_1_SUPERTYPES.has(normalizeSupertype(impl))) {
|
|
277
|
+
hasFrameworkSignal = true;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (hasFrameworkSignal)
|
|
282
|
+
break;
|
|
283
|
+
for (const method of type.methods) {
|
|
284
|
+
// (3) method-level annotations
|
|
285
|
+
for (const ann of method.annotations) {
|
|
286
|
+
if (TIER_1_METHOD_ANNOTATIONS.has(normalizeAnnotation(ann))) {
|
|
287
|
+
hasFrameworkSignal = true;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (hasFrameworkSignal)
|
|
292
|
+
break;
|
|
293
|
+
// (1) main(String[]) shape
|
|
294
|
+
if (!hasMain) {
|
|
295
|
+
const paramTypes = method.parameters.map((p) => p.type);
|
|
296
|
+
if (isMainMethod(method.name, paramTypes)) {
|
|
297
|
+
hasMain = true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (hasFrameworkSignal)
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
const cliMainSignal = hasMain && !hasFrameworkSignal;
|
|
305
|
+
if (!cliMainSignal) {
|
|
306
|
+
return { cliMainSignal: false, droppedCount: 0 };
|
|
307
|
+
}
|
|
308
|
+
// Drop reflection code_injection sinks in place. The authoritative
|
|
309
|
+
// sink list in the real pipeline is `SinkFilterResult.sinks` — that
|
|
310
|
+
// is what `analyzer.ts` assembles the final `taint.sinks` from and
|
|
311
|
+
// what `TaintPropagationPass` / `InterproceduralPass` consume.
|
|
312
|
+
// `graph.ir.taint.sinks` starts empty and is never populated by the
|
|
313
|
+
// pipeline, so mutating it alone would be a no-op. Prefer the
|
|
314
|
+
// sink-filter result; fall back to `graph.ir.taint.sinks` for
|
|
315
|
+
// stand-alone unit-test harnesses that don't run `SinkFilterPass`.
|
|
316
|
+
const sinks = ctx.hasResult('sink-filter')
|
|
317
|
+
? ctx.getResult('sink-filter').sinks
|
|
318
|
+
: graph.ir.taint.sinks;
|
|
319
|
+
let droppedCount = 0;
|
|
320
|
+
const kept = sinks.filter((sink) => {
|
|
321
|
+
if (sink.type !== 'code_injection')
|
|
322
|
+
return true;
|
|
323
|
+
if (!sink.method)
|
|
324
|
+
return true;
|
|
325
|
+
if (!REFLECTION_SINK_METHODS.has(sink.method))
|
|
326
|
+
return true;
|
|
327
|
+
droppedCount++;
|
|
328
|
+
return false;
|
|
329
|
+
});
|
|
330
|
+
if (droppedCount > 0) {
|
|
331
|
+
sinks.length = 0;
|
|
332
|
+
sinks.push(...kept);
|
|
333
|
+
}
|
|
334
|
+
return { cliMainSignal: true, droppedCount };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
//# sourceMappingURL=cli-main-reflection-suppress-pass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-main-reflection-suppress-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/cli-main-reflection-suppress-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgFG;AAYH;;;;;;;GAOG;AACH,MAAM,uBAAuB,GAAwB,IAAI,GAAG,CAAC;IAC3D,SAAS;IACT,aAAa;IACb,QAAQ;IACR,WAAW;IACX,mBAAmB;IACnB,gBAAgB;IAChB,wBAAwB;IACxB,WAAW;IACX,aAAa;CACd,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,wBAAwB,GAAwB,IAAI,GAAG,CAAC;IAC5D,aAAa;IACb,gBAAgB;IAChB,YAAY;IACZ,0BAA0B;IAC1B,SAAS;IACT,YAAY;IACZ,WAAW;IACX,wBAAwB;IACxB,MAAM;IACN,uCAAuC;IACvC,YAAY;IACZ,6BAA6B;IAC7B,gBAAgB;IAChB,0BAA0B;IAC1B,aAAa;CACd,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,yBAAyB,GAAwB,IAAI,GAAG,CAAC;IAC7D,aAAa;IACb,gBAAgB;IAChB,YAAY;IACZ,aAAa;IACb,YAAY;IACZ,eAAe;IACf,cAAc;IACd,+BAA+B;IAC/B,gBAAgB;IAChB,kBAAkB;IAClB,6BAA6B;IAC7B,eAAe;IACf,cAAc;IACd,gBAAgB;IAChB,eAAe;IACf,aAAa;IACb,gBAAgB;IAChB,mBAAmB;IACnB,aAAa;IACb,YAAY;IACZ,4BAA4B;IAC5B,eAAe;IACf,mBAAmB;IACnB,WAAW;IACX,SAAS;IACT,MAAM;IACN,KAAK;IACL,MAAM;IACN,KAAK;IACL,QAAQ;IACR,OAAO;IACP,MAAM;IACN,SAAS;IACT,+BAA+B;IAC/B,sBAAsB;IACtB,iBAAiB;CAClB,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,iBAAiB,GAAwB,IAAI,GAAG,CAAC;IACrD,aAAa;IACb,gBAAgB;IAChB,QAAQ;IACR,oBAAoB;IACpB,yBAAyB;IACzB,mBAAmB;IACnB,mBAAmB;IACnB,6BAA6B;IAC7B,uBAAuB;IACvB,8BAA8B;IAC9B,sBAAsB;IACtB,uBAAuB;IACvB,WAAW;IACX,sBAAsB;IACtB,kBAAkB;IAClB,6BAA6B;IAC7B,8BAA8B;IAC9B,6BAA6B;CAC9B,CAAC,CAAC;AAEH;;;;GAIG;AACH,SAAS,mBAAmB,CAAC,GAAW;IACtC,IAAI,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACnB,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACtC,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,QAAQ,IAAI,CAAC;QAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IAC5C,MAAM,UAAU,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,UAAU,IAAI,CAAC;QAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,MAAM,IAAI,CAAC;QAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACzC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;AAClB,CAAC;AAED,qEAAqE;AACrE,SAAS,kBAAkB,CAAC,GAAW;IACrC,IAAI,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACnB,MAAM,UAAU,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,UAAU,IAAI,CAAC;QAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,MAAM,IAAI,CAAC;QAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACzC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;AAClB,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,YAAY,CAAC,IAAY,EAAE,UAAsC;IACxE,IAAI,IAAI,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC;IAClC,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1C,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;IACxB,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACrB,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACnC,OAAO,IAAI,KAAK,UAAU,IAAI,IAAI,KAAK,oBAAoB,CAAC;AAC9D,CAAC;AAED,MAAM,OAAO,6BAA6B;IAG/B,IAAI,GAAG,8BAA8B,CAAC;IACtC,QAAQ,GAAG,UAAmB,CAAC;IAExC,GAAG,CAAC,GAAgB;QAClB,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,GAAG,CAAC;QAEhC,uDAAuD;QACvD,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;YACxB,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;QACnD,CAAC;QAED,MAAM,KAAK,GAAG,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC;QAC7B,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;QACnD,CAAC;QAED,iEAAiE;QACjE,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,gEAAgE;QAChE,IAAI,kBAAkB,GAAG,KAAK,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,8BAA8B;YAC9B,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;gBACnC,IAAI,wBAAwB,CAAC,GAAG,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;oBAC3D,kBAAkB,GAAG,IAAI,CAAC;oBAC1B,MAAM;gBACR,CAAC;YACH,CAAC;YACD,IAAI,kBAAkB;gBAAE,MAAM;YAE9B,kEAAkE;YAClE,IAAI,IAAI,CAAC,OAAO,IAAI,iBAAiB,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;gBAC5E,kBAAkB,GAAG,IAAI,CAAC;gBAC1B,MAAM;YACR,CAAC;YACD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;gBACnC,IAAI,iBAAiB,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;oBACpD,kBAAkB,GAAG,IAAI,CAAC;oBAC1B,MAAM;gBACR,CAAC;YACH,CAAC;YACD,IAAI,kBAAkB;gBAAE,MAAM;YAE9B,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBAClC,+BAA+B;gBAC/B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;oBACrC,IAAI,yBAAyB,CAAC,GAAG,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;wBAC5D,kBAAkB,GAAG,IAAI,CAAC;wBAC1B,MAAM;oBACR,CAAC;gBACH,CAAC;gBACD,IAAI,kBAAkB;oBAAE,MAAM;gBAE9B,2BAA2B;gBAC3B,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;oBACxD,IAAI,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC;wBAC1C,OAAO,GAAG,IAAI,CAAC;oBACjB,CAAC;gBACH,CAAC;YACH,CAAC;YACD,IAAI,kBAAkB;gBAAE,MAAM;QAChC,CAAC;QAED,MAAM,aAAa,GAAG,OAAO,IAAI,CAAC,kBAAkB,CAAC;QACrD,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;QACnD,CAAC;QAED,mEAAmE;QACnE,oEAAoE;QACpE,mEAAmE;QACnE,+DAA+D;QAC/D,oEAAoE;QACpE,8DAA8D;QAC9D,8DAA8D;QAC9D,mEAAmE;QACnE,MAAM,KAAK,GAA+C,GAAG,CAAC,SAAS,CAAC,aAAa,CAAC;YACpF,CAAC,CAAC,GAAG,CAAC,SAAS,CAAmB,aAAa,CAAC,CAAC,KAAK;YACtD,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC;QAEzB,IAAI,YAAY,GAAG,CAAC,CAAC;QACrB,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;YACjC,IAAI,IAAI,CAAC,IAAI,KAAK,gBAAgB;gBAAE,OAAO,IAAI,CAAC;YAChD,IAAI,CAAC,IAAI,CAAC,MAAM;gBAAE,OAAO,IAAI,CAAC;YAC9B,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC;gBAAE,OAAO,IAAI,CAAC;YAC3D,YAAY,EAAE,CAAC;YACf,OAAO,KAAK,CAAC;QACf,CAAC,CAAC,CAAC;QAEH,IAAI,YAAY,GAAG,CAAC,EAAE,CAAC;YACrB,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;YACjB,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QACtB,CAAC;QAED,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;IAC/C,CAAC;CACF"}
|
|
@@ -68,5 +68,31 @@ export declare class ResourceLeakPass implements AnalysisPass<ResourceLeakResult
|
|
|
68
68
|
* `void openFoo()` continue to fire.
|
|
69
69
|
*/
|
|
70
70
|
private isFactoryMethod;
|
|
71
|
+
/**
|
|
72
|
+
* #226 — true if `variable` is passed as a constructor argument to a
|
|
73
|
+
* known Closeable wrapper call within the enclosing method's line
|
|
74
|
+
* range. Ownership of the inner stream transfers to the wrapper.
|
|
75
|
+
*
|
|
76
|
+
* The check is deliberately per-method: cross-method wrapping does
|
|
77
|
+
* not apply because the inner reference has already escaped the
|
|
78
|
+
* scope by then.
|
|
79
|
+
*/
|
|
80
|
+
private isWrappedByCloseableCtor;
|
|
81
|
+
/**
|
|
82
|
+
* #227 — true if `variable` refers to a field of the enclosing class
|
|
83
|
+
* that is closed inside a worker-literal method (Runnable#run,
|
|
84
|
+
* Callable#call, Consumer#accept, ...) declared in the same enclosing
|
|
85
|
+
* method. Ownership transfers to the executor thread.
|
|
86
|
+
*
|
|
87
|
+
* The heuristic is intentionally conservative: it requires both
|
|
88
|
+
* (a) the resource variable to match a declared field name on the
|
|
89
|
+
* enclosing class (so a stray local named `selector` cannot
|
|
90
|
+
* accidentally opt in to the suppression), AND
|
|
91
|
+
* (b) a `<field>.close()` (or CLOSE_METHODS) call to appear on a
|
|
92
|
+
* line inside a method whose name is one of WORKER_METHODS and
|
|
93
|
+
* whose start_line is strictly within the enclosing method's
|
|
94
|
+
* body (nested literal indicator).
|
|
95
|
+
*/
|
|
96
|
+
private isClosedInNestedWorker;
|
|
71
97
|
}
|
|
72
98
|
//# sourceMappingURL=resource-leak-pass.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource-leak-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/resource-leak-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"resource-leak-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/resource-leak-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAgF9E,MAAM,WAAW,kBAAkB;IACjC,iDAAiD;IACjD,KAAK,EAAE,KAAK,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,UAAU,GAAG,WAAW,CAAC;KAChC,CAAC,CAAC;CACJ;AAED,qBAAa,gBAAiB,YAAW,YAAY,CAAC,kBAAkB,CAAC;IACvE,QAAQ,CAAC,IAAI,mBAAmB;IAChC,QAAQ,CAAC,QAAQ,EAAG,aAAa,CAAU;IAE3C,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,kBAAkB;IAkJzC,kFAAkF;IAClF,OAAO,CAAC,eAAe;IAOvB;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAQ3B;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAa1B;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAgBvB;;;;;;OAMG;IACH,OAAO,CAAC,sBAAsB;IAa9B;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IAKvB;;;;;;;;OAQG;IACH,OAAO,CAAC,wBAAwB;IAmBhC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,sBAAsB;CAqD/B"}
|
|
@@ -56,6 +56,37 @@ const CLOSE_METHODS = new Set([
|
|
|
56
56
|
'close', 'dispose', 'shutdown', 'disconnect', 'release', 'destroy', 'free',
|
|
57
57
|
'shutdownNow', 'terminate',
|
|
58
58
|
]);
|
|
59
|
+
/**
|
|
60
|
+
* #226 — Closeable wrapper constructors that document ownership transfer
|
|
61
|
+
* of their underlying stream/reader. When the underlying resource is passed
|
|
62
|
+
* as a constructor argument to one of these wrappers, closing the wrapper
|
|
63
|
+
* cascades to the inner stream (per JDK javadoc), so no leak exists.
|
|
64
|
+
* Reference: java.io + java.util.zip standard wrappers.
|
|
65
|
+
*/
|
|
66
|
+
const WRAPPER_CTORS = new Set([
|
|
67
|
+
// java.io wrappers
|
|
68
|
+
'BufferedInputStream', 'BufferedOutputStream', 'BufferedReader', 'BufferedWriter',
|
|
69
|
+
'InputStreamReader', 'OutputStreamWriter', 'DataInputStream', 'DataOutputStream',
|
|
70
|
+
'PrintStream', 'PrintWriter', 'LineNumberReader', 'PushbackInputStream',
|
|
71
|
+
'PushbackReader', 'SequenceInputStream',
|
|
72
|
+
// java.util.zip wrappers
|
|
73
|
+
'GZIPInputStream', 'GZIPOutputStream', 'ZipInputStream', 'ZipOutputStream',
|
|
74
|
+
'InflaterInputStream', 'DeflaterOutputStream', 'CheckedInputStream',
|
|
75
|
+
'CheckedOutputStream',
|
|
76
|
+
]);
|
|
77
|
+
/**
|
|
78
|
+
* #227 — Anonymous-class / functional-interface literal method names that
|
|
79
|
+
* commonly hold long-lived resources handed off from an outer scope. When a
|
|
80
|
+
* resource-field is closed inside one of these literals, ownership has been
|
|
81
|
+
* transferred to the worker and the outer method should not be flagged.
|
|
82
|
+
*/
|
|
83
|
+
const WORKER_METHODS = new Set([
|
|
84
|
+
'run', // Runnable, Thread
|
|
85
|
+
'call', // Callable
|
|
86
|
+
'accept', // Consumer
|
|
87
|
+
'get', // Supplier
|
|
88
|
+
'apply', // Function
|
|
89
|
+
]);
|
|
59
90
|
export class ResourceLeakPass {
|
|
60
91
|
name = 'resource-leak';
|
|
61
92
|
category = 'reliability';
|
|
@@ -103,6 +134,24 @@ export class ResourceLeakPass {
|
|
|
103
134
|
if (this.isFactoryMethod(methodInfo.method)) {
|
|
104
135
|
continue;
|
|
105
136
|
}
|
|
137
|
+
// #226 — suppression 4: resource is immediately passed as a
|
|
138
|
+
// constructor argument to a known Closeable wrapper (e.g.
|
|
139
|
+
// `new GZIPInputStream(fis)`, `new BufferedReader(new
|
|
140
|
+
// InputStreamReader(is))`). Per JDK javadoc, closing the
|
|
141
|
+
// wrapper cascades to the underlying stream, so ownership
|
|
142
|
+
// transfers to the wrapper and the inner reference is released
|
|
143
|
+
// through the outer chain.
|
|
144
|
+
if (this.isWrappedByCloseableCtor(graph.ir.calls, resourceVar, openLine, methodEnd)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
// #227 — suppression 5: resource is stored to a class field
|
|
148
|
+
// AND that field is referenced with a close() call inside a
|
|
149
|
+
// worker literal (Runnable#run / Callable#call / etc.) declared
|
|
150
|
+
// in the same method. Idiomatic Java concurrency: outer method
|
|
151
|
+
// opens the resource, executor thread closes it in cleanup.
|
|
152
|
+
if (this.isClosedInNestedWorker(graph, codeLines, resourceVar, methodInfo, openLine, methodEnd)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
106
155
|
// Look for a close() call on this resource within the method
|
|
107
156
|
const closeCall = graph.ir.calls.find(c => CLOSE_METHODS.has(c.method_name) &&
|
|
108
157
|
c.receiver === resourceVar &&
|
|
@@ -240,5 +289,94 @@ export class ResourceLeakPass {
|
|
|
240
289
|
return false;
|
|
241
290
|
return FACTORY_METHOD_NAME_RE.test(method.name);
|
|
242
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* #226 — true if `variable` is passed as a constructor argument to a
|
|
294
|
+
* known Closeable wrapper call within the enclosing method's line
|
|
295
|
+
* range. Ownership of the inner stream transfers to the wrapper.
|
|
296
|
+
*
|
|
297
|
+
* The check is deliberately per-method: cross-method wrapping does
|
|
298
|
+
* not apply because the inner reference has already escaped the
|
|
299
|
+
* scope by then.
|
|
300
|
+
*/
|
|
301
|
+
isWrappedByCloseableCtor(calls, variable, fromLine, toLine) {
|
|
302
|
+
// WRAPPER_CTORS names are PascalCase JDK class names — safe to
|
|
303
|
+
// match regardless of `is_constructor` (Java tree-sitter often
|
|
304
|
+
// omits the flag).
|
|
305
|
+
for (const call of calls) {
|
|
306
|
+
if (!WRAPPER_CTORS.has(call.method_name))
|
|
307
|
+
continue;
|
|
308
|
+
if (call.location.line < fromLine || call.location.line > toLine)
|
|
309
|
+
continue;
|
|
310
|
+
for (const arg of call.arguments) {
|
|
311
|
+
if (arg.variable === variable)
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* #227 — true if `variable` refers to a field of the enclosing class
|
|
319
|
+
* that is closed inside a worker-literal method (Runnable#run,
|
|
320
|
+
* Callable#call, Consumer#accept, ...) declared in the same enclosing
|
|
321
|
+
* method. Ownership transfers to the executor thread.
|
|
322
|
+
*
|
|
323
|
+
* The heuristic is intentionally conservative: it requires both
|
|
324
|
+
* (a) the resource variable to match a declared field name on the
|
|
325
|
+
* enclosing class (so a stray local named `selector` cannot
|
|
326
|
+
* accidentally opt in to the suppression), AND
|
|
327
|
+
* (b) a `<field>.close()` (or CLOSE_METHODS) call to appear on a
|
|
328
|
+
* line inside a method whose name is one of WORKER_METHODS and
|
|
329
|
+
* whose start_line is strictly within the enclosing method's
|
|
330
|
+
* body (nested literal indicator).
|
|
331
|
+
*/
|
|
332
|
+
isClosedInNestedWorker(graph, lines, variable, methodInfo, fromLine, toLine) {
|
|
333
|
+
// (a) resource variable must match a declared field of the class,
|
|
334
|
+
// or be text-assigned to a field within the outer method.
|
|
335
|
+
const fieldNames = new Set(methodInfo.type.fields.map(f => f.name));
|
|
336
|
+
let candidateField = null;
|
|
337
|
+
if (fieldNames.has(variable)) {
|
|
338
|
+
candidateField = variable;
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
// Fallback: `this.<field> = variable` or `<field> = variable`
|
|
342
|
+
// where <field> is a declared field.
|
|
343
|
+
const thisAssignRe = new RegExp(`(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${escapeRegex(variable)}\\b`);
|
|
344
|
+
for (let l = fromLine; l <= toLine && l <= lines.length; l++) {
|
|
345
|
+
const m = thisAssignRe.exec(lines[l - 1] ?? '');
|
|
346
|
+
if (m && fieldNames.has(m[1])) {
|
|
347
|
+
candidateField = m[1];
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (!candidateField)
|
|
353
|
+
return false;
|
|
354
|
+
// (b) find a close-style call on the candidate field whose line
|
|
355
|
+
// falls inside a WORKER_METHODS method nested in the enclosing
|
|
356
|
+
// method's body.
|
|
357
|
+
for (const call of graph.ir.calls) {
|
|
358
|
+
if (call.receiver !== candidateField)
|
|
359
|
+
continue;
|
|
360
|
+
if (!CLOSE_METHODS.has(call.method_name))
|
|
361
|
+
continue;
|
|
362
|
+
const closeLine = call.location.line;
|
|
363
|
+
if (closeLine < fromLine || closeLine > toLine)
|
|
364
|
+
continue;
|
|
365
|
+
const enclosing = graph.methodAtLine(closeLine);
|
|
366
|
+
if (!enclosing)
|
|
367
|
+
continue;
|
|
368
|
+
// Same outer method — not nested.
|
|
369
|
+
if (enclosing.method === methodInfo.method)
|
|
370
|
+
continue;
|
|
371
|
+
if (!WORKER_METHODS.has(enclosing.method.name))
|
|
372
|
+
continue;
|
|
373
|
+
// Nested worker start must lie inside the outer method body.
|
|
374
|
+
if (enclosing.method.start_line > fromLine &&
|
|
375
|
+
enclosing.method.start_line <= toLine) {
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
243
381
|
}
|
|
244
382
|
//# sourceMappingURL=resource-leak-pass.js.map
|