Skip to content

Commit 7c6eb7c

Browse files
authored
wasm-ctor-eval: Do not emit invalid code after hitting a GC cycle (#8188)
We break up cycles during serialization of GC data, but we can't always do so: if the cycle uses immutable and non-nullable fields, we can't write a null first and close the loop later. Before this PR, we would error inside the topological sort utility; instead, catch an error from there, and stop our work. Unfortunately, it is complicated to detect this situation ahead of time: we only hit a problem on the specific type of cycle mentioned before, and we do so while already modifying the module state. Rather than do a large refactoring to handle this, just give up on the state in memory, clearing it, losing any evalling work thus far. If this becomes important enough, we can consider handling it more gracefully later.
1 parent 57d5ae3 commit 7c6eb7c

File tree

3 files changed

+93
-2
lines changed

3 files changed

+93
-2
lines changed

src/support/topological_sort.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ namespace wasm {
3333

3434
namespace TopologicalSort {
3535

36+
// Thrown when no valid topological sort exists due to a cycle.
37+
struct CycleException {};
38+
3639
// An adjacency list containing edges from vertices to their successors. Uses
3740
// `Index` because we are primarily sorting elements of Wasm modules. If we ever
3841
// need to sort signficantly larger objects, we might need to switch to
@@ -220,7 +223,10 @@ template<typename Cmp> struct TopologicalOrdersImpl {
220223
// Select the next available vertex, decrement in-degrees, and update the
221224
// sequence of available vertices. Return the Selector for the next vertex.
222225
Selector select(TopologicalOrdersImpl& ctx) {
223-
assert(count >= 1);
226+
if (count == 0) {
227+
// No choices remain, indicating a cycle.
228+
throw TopologicalSort::CycleException();
229+
}
224230
assert(start + count <= ctx.buf.size());
225231
if constexpr (TopologicalOrdersImpl::useMinHeap) {
226232
ctx.buf[start] = ctx.popChoice();

src/tools/wasm-ctor-eval.cpp

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,12 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance,
13281328
}
13291329
}
13301330

1331+
// This is set when we find a situation we cannot handle in the middle of our
1332+
// work. In that case we give up, throwing away the invalid state in memory.
1333+
// TODO: Handle all those situations more gracefully, if that becomes useful
1334+
// enough at some point.
1335+
static bool invalidState = false;
1336+
13311337
// Eval all ctors in a module.
13321338
void evalCtors(Module& wasm,
13331339
std::vector<std::string>& ctors,
@@ -1432,7 +1438,19 @@ void evalCtors(Module& wasm,
14321438
std::cout << " ...stopping since could not create module instance: "
14331439
<< fail.why << "\n";
14341440
}
1435-
return;
1441+
} catch (TopologicalSort::CycleException e) {
1442+
// We use a topological sort for GC globals. If there is a non-breakable
1443+
// cycle there, we will hit an error (we can break cycles in nullable and
1444+
// mutable fields by setting a null and filling in the value later, but
1445+
// other situations are a problem).
1446+
if (!quiet) {
1447+
std::cout << " ...stopping since global sorting hit a cycle\n";
1448+
}
1449+
1450+
// We found the cycle during the processing of globals, making wasm->globals
1451+
// invalid. Rather than refactor the code heavily to handle this very rare
1452+
// case, just give up entirely, and avoid writing out the current state.
1453+
invalidState = true;
14361454
}
14371455
}
14381456

@@ -1552,6 +1570,17 @@ int main(int argc, const char* argv[]) {
15521570
if (canEval(wasm)) {
15531571
evalCtors(wasm, ctors, keptExports);
15541572

1573+
if (invalidState) {
1574+
// We ended up in a state that cannot be written out. Forget about all of
1575+
// it, by re-reading the input and continuing from there (with nothing at
1576+
// all evalled, effectively).
1577+
auto features = wasm.features;
1578+
ModuleUtils::clearModule(wasm);
1579+
wasm.features = features;
1580+
ModuleReader reader;
1581+
reader.read(options.extra["infile"], wasm);
1582+
}
1583+
15551584
if (!WasmValidator().validate(wasm)) {
15561585
std::cout << wasm << '\n';
15571586
Fatal() << "error in validating output";

test/lit/ctor-eval/gc-cycle.wast

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,3 +1291,59 @@
12911291
;; CHECK-NEXT: (global.get $ctor-eval$global)
12921292
;; CHECK-NEXT: )
12931293
;; CHECK-NEXT: )
1294+
(module
1295+
;; A circular reference using a non-nullable (but mutable) field. Unlike cases
1296+
;; above, we cannot break up such cycles, and must give up. We should at least
1297+
;; not error.
1298+
1299+
;; CHECK: (type $array (array i8))
1300+
(type $array (array i8))
1301+
;; CHECK: (type $struct (struct (field (mut (ref any)))))
1302+
(type $struct (struct (field (mut (ref any)))))
1303+
1304+
;; CHECK: (type $2 (func))
1305+
1306+
;; CHECK: (global $global (mut i32) (i32.const 42))
1307+
(global $global (mut i32) (i32.const 42))
1308+
1309+
;; CHECK: (export "test" (func $test))
1310+
1311+
;; CHECK: (func $test (type $2)
1312+
;; CHECK-NEXT: (local $temp (ref $struct))
1313+
;; CHECK-NEXT: (global.set $global
1314+
;; CHECK-NEXT: (i32.const 1337)
1315+
;; CHECK-NEXT: )
1316+
;; CHECK-NEXT: (local.set $temp
1317+
;; CHECK-NEXT: (struct.new $struct
1318+
;; CHECK-NEXT: (array.new_fixed $array 0)
1319+
;; CHECK-NEXT: )
1320+
;; CHECK-NEXT: )
1321+
;; CHECK-NEXT: (struct.set $struct 0
1322+
;; CHECK-NEXT: (local.get $temp)
1323+
;; CHECK-NEXT: (local.get $temp)
1324+
;; CHECK-NEXT: )
1325+
;; CHECK-NEXT: )
1326+
(func $test (export "test")
1327+
(local $temp (ref $struct))
1328+
1329+
;; Set the global. This will not get written out, as we will cancel all our
1330+
;; work when we hit the cycle below. (TODO: improve that)
1331+
(global.set $global
1332+
(i32.const 1337)
1333+
)
1334+
1335+
;; Start with the struct referring to an array.
1336+
(local.set $temp
1337+
(struct.new $struct
1338+
(array.new_fixed $array 0)
1339+
)
1340+
)
1341+
1342+
;; Make the struct refer to itself, circularly.
1343+
(struct.set $struct 0
1344+
(local.get $temp)
1345+
(local.get $temp)
1346+
)
1347+
)
1348+
)
1349+

0 commit comments

Comments
 (0)