Skip to content

Commit 9458e68

Browse files
authored
fix: detect use-after-destroy on parameters (#1273)
1 parent ebf96e8 commit 9458e68

5 files changed

Lines changed: 108 additions & 18 deletions

File tree

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public static LuaExpr translate(ImFuncRef e, LuaTranslator tr) {
6969
LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(tempRes),
7070
LuaAst.LuaExprFunctionCall(tr.luaFunc.getFor(e.getFunc()), LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(dots.copy())))))
7171
),
72-
LuaAst.LuaLiteral("function(err) if err == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end BJDebugMsg(\"lua callback error: \" .. tostring(err)) xpcall(function() " + callErrorFunc(tr, "tostring(err)") + " end, function(err2) if err2 == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end BJDebugMsg(\"error reporting error: \" .. tostring(err2)) BJDebugMsg(\"while reporting: \" .. tostring(err)) end) end"),
72+
LuaAst.LuaLiteral("function(err) if err == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end BJDebugMsg(\"lua callback error: \" .. tostring(err)) xpcall(function() " + callErrorFunc(tr, "tostring(err)", "in lua callback error handler") + " end, function(err2) if err2 == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end BJDebugMsg(\"error reporting error: \" .. tostring(err2)) BJDebugMsg(\"while reporting: \" .. tostring(err)) end) end"),
7373
LuaAst.LuaExprVarAccess(dots.copy())
7474
)
7575
));
@@ -83,7 +83,7 @@ public static LuaExpr translate(ImFuncRef e, LuaTranslator tr) {
8383
LuaAst.LuaExprFunctionCall(tr.luaFunc.getFor(e.getFunc()), LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(dots.copy())))
8484
)
8585
),
86-
LuaAst.LuaLiteral("function(err) if err == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end BJDebugMsg(\"lua callback error: \" .. tostring(err)) xpcall(function() " + callErrorFunc(tr, "tostring(err)") + " end, function(err2) if err2 == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end BJDebugMsg(\"error reporting error: \" .. tostring(err2)) BJDebugMsg(\"while reporting: \" .. tostring(err)) end) end"),
86+
LuaAst.LuaLiteral("function(err) if err == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end BJDebugMsg(\"lua callback error: \" .. tostring(err)) xpcall(function() " + callErrorFunc(tr, "tostring(err)", "in lua callback error handler") + " end, function(err2) if err2 == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end BJDebugMsg(\"error reporting error: \" .. tostring(err2)) BJDebugMsg(\"while reporting: \" .. tostring(err)) end) end"),
8787
LuaAst.LuaExprVarAccess(dots.copy())
8888
)
8989
));
@@ -92,13 +92,14 @@ public static LuaExpr translate(ImFuncRef e, LuaTranslator tr) {
9292
}
9393

9494
static String callErrorFunc(LuaTranslator tr, String msg) {
95+
return callErrorFunc(tr, msg, "<lua error>");
96+
}
97+
98+
static String callErrorFunc(LuaTranslator tr, String msg, String stackPos) {
9599
LuaFunction ef = tr.getErrorFunc();
96100
if (ef != null) {
97101
if (ef.getParams().size() == 2) {
98-
// StackTraceInjector adds the second parameter to ErrorHandling.error. Lua's
99-
// xpcall handler already has the real failing call stack, so pass it through
100-
// instead of the old placeholder (which made -stacktraces ineffective in Lua).
101-
return ef.getName() + "(" + msg + ", debug.traceback(" + msg + ", 2))";
102+
return ef.getName() + "(" + msg + ", \"" + stackPos + "\")";
102103
}
103104
return ef.getName() + "(" + msg + ")";
104105
}

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/controlflow/DataflowAnomalyAnalysis.java

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,22 @@
1919
//w(11)->{r(17), r(19)} & w(19)->{r(17), r(19)} & w(22)
2020
class VarStates {
2121
final ImmutableMap<LocalVarDef, VState> states;
22+
final ImmutableSet<NameDef> destroyedParameters;
2223
final boolean thisDestroyed;
2324

2425
public VarStates(ImmutableMap<LocalVarDef, VState> states, boolean thisDestroyed) {
26+
this(states, ImmutableSet.of(), thisDestroyed);
27+
}
28+
29+
public VarStates(ImmutableMap<LocalVarDef, VState> states, ImmutableSet<NameDef> destroyedParameters, boolean thisDestroyed) {
2530
this.states = states;
31+
this.destroyedParameters = destroyedParameters;
2632
this.thisDestroyed = thisDestroyed;
2733
}
2834

2935
VarStates merge(VarStates other) {
3036
ImmutableMap<LocalVarDef, VState> merged = Utils.mergeMaps(states, other.states, VState::merge);
31-
return new VarStates(merged, thisDestroyed || other.thisDestroyed);
37+
return new VarStates(merged, ImmutableSet.<NameDef>builder().addAll(destroyedParameters).addAll(other.destroyedParameters).build(), thisDestroyed || other.thisDestroyed);
3238
}
3339

3440
@Override
@@ -37,12 +43,13 @@ public boolean equals(Object o) {
3743
if (o == null || getClass() != o.getClass()) return false;
3844
VarStates varStates = (VarStates) o;
3945
return thisDestroyed == varStates.thisDestroyed &&
40-
Objects.equals(states, varStates.states);
46+
Objects.equals(states, varStates.states) &&
47+
Objects.equals(destroyedParameters, varStates.destroyedParameters);
4148
}
4249

4350
@Override
4451
public int hashCode() {
45-
return Objects.hash(states, thisDestroyed);
52+
return Objects.hash(states, destroyedParameters, thisDestroyed);
4653
}
4754

4855
public static VarStates initial(Set<LocalVarDef> r) {
@@ -55,7 +62,7 @@ public static VarStates initial(Set<LocalVarDef> r) {
5562

5663
public boolean destroyed(NameDef v) {
5764
VState s = states.get(v);
58-
return s != null && s.mightBeDestroyed;
65+
return (s != null && s.mightBeDestroyed) || destroyedParameters.contains(v);
5966
}
6067

6168
public boolean uninitialized(NameDef v) {
@@ -78,7 +85,7 @@ public VarStates addRead(LocalVarDef v, Element r) {
7885
ImmutableMap<LocalVarDef, VState> rs = builder
7986
.put(v, s)
8087
.build();
81-
return new VarStates(rs, thisDestroyed);
88+
return new VarStates(rs, destroyedParameters, thisDestroyed);
8289
}
8390

8491
public ImmutableSet<WStatement> getUnreadWrites(NameDef var) {
@@ -107,7 +114,7 @@ public VarStates addWrite(LocalVarDef var, WStatement s) {
107114
}
108115
vState = vState.addWrite(s);
109116
res.put(var, vState);
110-
return new VarStates(res.build(), thisDestroyed);
117+
return new VarStates(res.build(), destroyedParameters, thisDestroyed);
111118
}
112119

113120
public VarStates addDestroy(LocalVarDef var) {
@@ -118,7 +125,26 @@ public VarStates addDestroy(LocalVarDef var) {
118125
}
119126
}
120127
res.put(var, VState.destroyed);
121-
return new VarStates(res.build(), thisDestroyed);
128+
return new VarStates(res.build(), destroyedParameters, thisDestroyed);
129+
}
130+
131+
public VarStates addDestroyParameter(NameDef var) {
132+
ImmutableSet.Builder<NameDef> destroyed = ImmutableSet.builder();
133+
destroyed.addAll(destroyedParameters).add(var);
134+
return new VarStates(states, destroyed.build(), thisDestroyed);
135+
}
136+
137+
public VarStates clearDestroyParameter(NameDef var) {
138+
if (!destroyedParameters.contains(var)) {
139+
return this;
140+
}
141+
ImmutableSet.Builder<NameDef> remaining = ImmutableSet.builder();
142+
for (NameDef destroyed : destroyedParameters) {
143+
if (destroyed != var) {
144+
remaining.add(destroyed);
145+
}
146+
}
147+
return new VarStates(states, remaining.build(), thisDestroyed);
122148
}
123149

124150

@@ -144,7 +170,7 @@ public boolean isThisDestroyed() {
144170
}
145171

146172
public VarStates withThisDestroyed(boolean thisDestroyed) {
147-
return new VarStates(states, thisDestroyed);
173+
return new VarStates(states, destroyedParameters, thisDestroyed);
148174
}
149175

150176

@@ -300,6 +326,9 @@ VarStates calculate(WStatement s, VarStates incoming) {
300326
if (isLocalVarDef(destroyedVar)) {
301327
return incoming.addDestroy((LocalVarDef) destroyedVar);
302328
}
329+
if (destroyedVar instanceof WParameter || destroyedVar instanceof WShortParameter) {
330+
return incoming.addDestroyParameter(destroyedVar);
331+
}
303332
} else if (destr.getDestroyedObj() instanceof ExprThis) {
304333
return incoming.withThisDestroyed(true);
305334
}
@@ -318,6 +347,9 @@ VarStates calculate(WStatement s, VarStates incoming) {
318347
LocalVarDef lv = (LocalVarDef) n;
319348
return incoming.addWrite(lv, s);
320349
}
350+
if (n instanceof WParameter || n instanceof WShortParameter) {
351+
return incoming.clearDestroyParameter(n);
352+
}
321353
}
322354
return incoming;
323355
}
@@ -509,7 +541,9 @@ void checkFinal(VarStates fin) {
509541
@Nullable ExprClosure exprClosure = errorPos.attrNearestExprClosure();
510542
@Nullable ExprClosure exprClosure1 = var.attrNearestExprClosure();
511543
if (exprClosure != null && exprClosure != exprClosure1) {
512-
errorPos.addWarning("This assignment to the closure-captured variable " + Utils.printElement(var) + " has no effect outside the closure.");
544+
errorPos.addWarning("This assignment to the closure-captured variable " + Utils.printElement(var)
545+
+ " does not propagate outside the closure because closures capture locals by value. "
546+
+ "If you want to update the outer value, use reference(" + var.getName() + ") deliberately.");
513547
} else {
514548
errorPos.addWarning("The assignment to " + Utils.printElement(var) + " is never read.");
515549
}

de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -928,7 +928,7 @@ public void unreadVarWarning2() { // #380
928928

929929
@Test
930930
public void unreadVarWarning3() { // #380
931-
testAssertErrorsLines(true, "closure-captured variable",
931+
testAssertErrorsLines(true, "does not propagate outside the closure because closures capture locals by value",
932932
"package test",
933933
"@annotation public function annotation()",
934934
"@annotation public function extern()",

de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FlowAnalysisTests.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,38 @@ public void destroyDataflowTest() {
120120
);
121121
}
122122

123+
@Test
124+
public void destroyParameterThenUseIsReported() {
125+
testAssertErrorsLines(false, "Variable a may have been destroyed already",
126+
"package test",
127+
"class A",
128+
" function foo()",
129+
"function consume(A a)",
130+
" destroy a",
131+
" a.foo()",
132+
"init",
133+
" consume(new A())"
134+
);
135+
}
136+
137+
@Test
138+
public void destroyShortParameterThenUseIsReported() {
139+
testAssertErrorsLines(false, "Variable a may have been destroyed already",
140+
"package test",
141+
"class A",
142+
" function foo()",
143+
"interface Consumer",
144+
" function accept(A a)",
145+
"function apply(Consumer c)",
146+
" c.accept(new A())",
147+
"init",
148+
" apply((A a) -> begin",
149+
" destroy a",
150+
" a.foo()",
151+
" end)"
152+
);
153+
}
154+
123155
@Test
124156
public void destroyThisDataflowTest() {
125157
testAssertErrorsLines(false, "Cannot access 'this' because it might already have been destroyed.",

de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ public void stacktraceStringsNotInjectedIntoNumericComparisons() {
277277
}
278278

279279
@Test
280-
public void luaErrorWrappersPassNativeTracebackToStacktracedErrorHandler() throws IOException {
280+
public void luaErrorWrappersAvoidUnavailableNativeTraceback() throws IOException {
281281
String compiled = compileLuaWithRunArgs(
282282
"LuaTranslationTests_luaErrorWrappersPassNativeTracebackToStacktracedErrorHandler",
283283
false,
@@ -289,7 +289,7 @@ public void luaErrorWrappersPassNativeTracebackToStacktracedErrorHandler() throw
289289
"init",
290290
" fail()"
291291
);
292-
assertTrue(compiled.contains("debug.traceback"));
292+
assertFalse(compiled.contains("debug.traceback"));
293293
}
294294

295295
@Test
@@ -2423,6 +2423,29 @@ public void luaFunctionRefWrapperForwardsVarargs() throws IOException {
24232423
assertFalse(compiled.contains("ForForce(f, function (...) \n\t\t\tlocal tempRes"));
24242424
}
24252425

2426+
@Test
2427+
public void luaFunctionRefStacktraceHandlerUsesWurstStackPosition() throws IOException {
2428+
CU errorHandling = new CU("ErrorHandling.wurst", String.join("\n",
2429+
"package ErrorHandling",
2430+
"public function error(string msg)",
2431+
" skip"));
2432+
String compiled = compileLuaWithCUs(
2433+
"LuaTranslationTests_luaFunctionRefStacktraceHandlerUsesWurstStackPosition",
2434+
false,
2435+
Collections.singletonList(errorHandling),
2436+
"package Test",
2437+
"import ErrorHandling",
2438+
"native apply(code c)",
2439+
"init",
2440+
" apply(() -> error(\"callback\"))"
2441+
);
2442+
assertFalse(compiled.contains("debug.traceback"));
2443+
assertContainsRegex(compiled,
2444+
"function\\s+error1\\([^\\)]*__wurst_stackPos");
2445+
assertContainsRegex(compiled,
2446+
"error1\\(tostring\\(err\\), \\\"in lua callback error handler\\\"\\)");
2447+
}
2448+
24262449
@Test
24272450
public void forForceIsRemappedToWurstHelperInLua() throws IOException {
24282451
test().testLua(true).withStdLib().lines(

0 commit comments

Comments
 (0)