@agoric/swingset-liveslots 0.10.3-u16.1 → 0.10.3-u17.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.
@@ -0,0 +1,80 @@
1
+ import test from 'ava';
2
+ import { Far } from '@endo/marshal';
3
+ import { kser, kslot } from '@agoric/kmarshal';
4
+ import { makeLiveSlots } from '../src/liveslots.js';
5
+ import { buildSyscall } from './liveslots-helpers.js';
6
+ import { makeStartVat, makeMessage, makeBringOutYourDead } from './util.js';
7
+ import { makeMockGC } from './mock-gc.js';
8
+
9
+ // Test for https://github.com/Agoric/agoric-sdk/issues/9939
10
+
11
+ test('weakset deletion vs retire', async t => {
12
+ const { syscall, log } = buildSyscall();
13
+ const gcTools = makeMockGC();
14
+
15
+ // #9939 was a bug in liveslots that caused a vat to emit
16
+ // syscall.retireImports despite not having done dropImports
17
+ // first. The setup is:
18
+ //
19
+ // * import a Presence (raising the RAM pillar)
20
+ // * store it in a virtual object (raising the vdata pillar)
21
+ // * use it as a key of a voAwareWeakMap or voAwareWeakSet
22
+ // * drop the Presence (dropping the RAM pillar)
23
+ // * do a BOYD
24
+ // * delete the voAwareWeakSet
25
+ // * do a BOYD
26
+ //
27
+ // When the voAwareWeakSet is collected, a finalizer callback named
28
+ // finalizeDroppedCollection is called, which clears the entries,
29
+ // and adds all its vref keys to possiblyRetiredSet. Later, during
30
+ // BOYD, a loop examines possiblyRetiredSet and adds qualified vrefs
31
+ // to importsToRetire, for the syscall.retireImports at the end.
32
+ //
33
+ // That qualification check was sufficient to prevent the retirement
34
+ // of vrefs that still have a RAM pillar, and also vrefs that were
35
+ // being dropped in this particular BOYD, but it was not sufficient
36
+ // to protect vrefs that still have a vdata pillar.
37
+
38
+ let myVOAwareWeakSet;
39
+ let myPresence;
40
+ function buildRootObject(vatPowers, _vatParameters, baggage) {
41
+ const { WeakSet } = vatPowers;
42
+ myVOAwareWeakSet = new WeakSet();
43
+ return Far('root', {
44
+ store: p => {
45
+ myPresence = p;
46
+ myVOAwareWeakSet.add(p);
47
+ baggage.init('presence', p);
48
+ },
49
+ });
50
+ }
51
+
52
+ const makeNS = () => ({ buildRootObject });
53
+ const ls = makeLiveSlots(syscall, 'vatA', {}, {}, gcTools, undefined, makeNS);
54
+ const { dispatch } = ls;
55
+ await dispatch(makeStartVat(kser()));
56
+ t.truthy(myVOAwareWeakSet);
57
+
58
+ await dispatch(makeMessage('o+0', 'store', [kslot('o-1')]));
59
+ t.truthy(myPresence);
60
+
61
+ log.length = 0;
62
+ gcTools.kill(myPresence);
63
+ gcTools.flushAllFRs();
64
+ await dispatch(makeBringOutYourDead());
65
+
66
+ log.length = 0;
67
+ gcTools.kill(myVOAwareWeakSet);
68
+ gcTools.flushAllFRs();
69
+ await dispatch(makeBringOutYourDead());
70
+
71
+ // The imported vref is still reachable by the 'baggage' durable
72
+ // store, so it must not be dropped or retired yet. The bug caused
73
+ // the vref to be retired without first doing a drop, which is a
74
+ // vat-fatal syscall error
75
+ const gcCalls = log.filter(
76
+ l => l.type === 'dropImports' || l.type === 'retireImports',
77
+ );
78
+ t.deepEqual(gcCalls, []);
79
+ log.length = 0;
80
+ });
@@ -1,4 +1,4 @@
1
- import { assert } from '@agoric/assert';
1
+ import { assert } from '@endo/errors';
2
2
 
3
3
  export function makeDummyMeterControl() {
4
4
  let meteringDisabled = 0;
@@ -262,7 +262,7 @@ async function runDurabilityCheckTest(t, relaxDurabilityRules) {
262
262
  passVal(() => virtualMap.init('non-scalar key', aNonScalarKey));
263
263
  passVal(() => virtualMap.init('non-scalar non-key', aNonScalarNonKey));
264
264
 
265
- condVal(() => durableMap.init('promise', aPassablePromise));
265
+ failVal(() => durableMap.init('promise', aPassablePromise));
266
266
  passVal(() => durableMap.init('error', aPassableError));
267
267
  passVal(() => durableMap.init('non-scalar key', aNonScalarKey));
268
268
  passVal(() => durableMap.init('non-scalar non-key', aNonScalarNonKey));
@@ -0,0 +1,230 @@
1
+ import test from 'ava';
2
+ import { Far } from '@endo/marshal';
3
+ import { kser, kslot } from '@agoric/kmarshal';
4
+ import { makeLiveSlots } from '../src/liveslots.js';
5
+ import { buildSyscall } from './liveslots-helpers.js';
6
+ import { makeStartVat, makeMessage, makeBringOutYourDead } from './util.js';
7
+ import { makeMockGC } from './mock-gc.js';
8
+
9
+ const justGC = log =>
10
+ log.filter(
11
+ l =>
12
+ l.type === 'dropImports' ||
13
+ l.type === 'retireImports' ||
14
+ l.type === 'retireExports',
15
+ );
16
+
17
+ test('presence in COLLECTED state is not dropped yet', async t => {
18
+ const { syscall, log } = buildSyscall();
19
+ const gcTools = makeMockGC();
20
+
21
+ // our GC terminology (see boyd-gc.js for notes):
22
+ // * REACHABLE means userspace can reach a Presence
23
+ // * UNREACHABLE means it cannot
24
+ // * COLLECTED means the engine GC has noticed, so the
25
+ // slotToVal.get(vref) WeakRef is empty, even though
26
+ // slotToVal.has(vref) is still true. A finalizer
27
+ // callback has been queued.
28
+ // * FINALIZED means the callback has been run. Our callback does
29
+ // slotToVal.delete(vref) before adding the vref to
30
+ // possiblyDeadSet
31
+
32
+ // slotToVal.has() returns true for REACHABLE / UNREACHABLE /
33
+ // COLLECTED, and false for FINALIZED. getValForSlot() is another
34
+ // way to probe for reachability, but it also looks to see if the
35
+ // WeakRef is full, so it returns false for both COLLECTED and
36
+ // FINALIZED.
37
+
38
+ // We use slotToVal.has(vref) as the "is it still reachable" probe
39
+ // in scanForDeadObjects(), because if we have a Presence in the
40
+ // COLLECTED state, we must not dropImports it during this BOYD. The
41
+ // finalizer is still enqueued, so some future turn will run it, and
42
+ // the vref will be added to possiblyDeadSet again. We want the drop
43
+ // to happen exactly once, and we don't remove the finalizer from
44
+ // the queue, which means the drop must happen in the future BOYD.
45
+
46
+ // We can get into this situation with the following sequence:
47
+ // 1: vref is held by both Presence and vdata
48
+ // 2: Presence is dropped by userspace (->UNREACHABLE)
49
+ // 3: userspace store.delete(), drops vdata refcount, addToPossiblyDeadSet
50
+ // 4: organic GC happens (->COLLECTED, WeakRef is empty)
51
+ // 5: BOYD is called (finalizer has not run yet)
52
+
53
+ // The order of steps 3 and 4 is not important. What matters is that
54
+ // the WeakRef is empty, but the finalizer has not yet run, at the
55
+ // time that BOYD happens. And that the vref is in possiblyDeadSet
56
+ // (because of the vdata drop, not the finalizer), so this BOYD will
57
+ // examine the vref.
58
+
59
+ // This test simulates this case with our mockGC tools. It would
60
+ // fail if boyd-gc.js used getValForSlot instead of slotToVal.has.
61
+
62
+ // The GC code used to call getValForSlot() instead of
63
+ // slotToVal.has(), in the possiblyRetiredSet. The second test
64
+ // exercises this case, and would fail if it still used
65
+ // getValForSlot
66
+
67
+ let myPresence;
68
+ function buildRootObject(vatPowers) {
69
+ const { VatData } = vatPowers;
70
+ const { makeScalarBigMapStore } = VatData;
71
+ const store = makeScalarBigMapStore();
72
+ return Far('root', {
73
+ store: p => {
74
+ myPresence = p;
75
+ store.init('presence', p);
76
+ },
77
+ drop: () => store.delete('presence'),
78
+ });
79
+ }
80
+
81
+ const makeNS = () => ({ buildRootObject });
82
+ const ls = makeLiveSlots(syscall, 'vatA', {}, {}, gcTools, undefined, makeNS);
83
+ const { dispatch } = ls;
84
+ await dispatch(makeStartVat(kser()));
85
+
86
+ await dispatch(makeMessage('o+0', 'store', [kslot('o-1')]));
87
+ t.truthy(myPresence);
88
+
89
+ // clear out everything before our check
90
+ await dispatch(makeBringOutYourDead());
91
+ log.length = 0;
92
+
93
+ // drop the vdata reference
94
+ await dispatch(makeMessage('o+0', 'drop', []));
95
+ log.length = 0;
96
+
97
+ // and, before BOYD can happen, collect (but do not finalize) the Presence
98
+ gcTools.kill(myPresence);
99
+
100
+ // now BOYD
101
+ await dispatch(makeBringOutYourDead());
102
+
103
+ // the BOYD must not have done dropImports or retireImports
104
+ t.deepEqual(log, []);
105
+
106
+ // eventually, the finalizer runs
107
+ gcTools.flushAllFRs();
108
+
109
+ // *now* a BOYD will drop+retire
110
+ await dispatch(makeBringOutYourDead());
111
+ t.deepEqual(justGC(log), [
112
+ { type: 'dropImports', slots: ['o-1'] },
113
+ { type: 'retireImports', slots: ['o-1'] },
114
+ ]);
115
+ });
116
+
117
+ test('presence in COLLECTED state is not retired early', async t => {
118
+ const { syscall, log } = buildSyscall();
119
+ const gcTools = makeMockGC();
120
+
121
+ // The GC code used to call getValForSlot() instead of
122
+ // slotToVal.has(), in the possiblyRetiredSet. This test would fail
123
+ // if it still used getValForSlot.
124
+
125
+ // The setup is a Presence in the COLLECTED state as the only
126
+ // pillar, but not in possiblyDeadSet, whose vref also appears in
127
+ // possiblyRetiredSet (because a recognizer was just deleted). On
128
+ // the BOYD that handles the possiblyRetiredSet, the "reachability
129
+ // inhibits retirement" check should treat the COLLECTED state as
130
+ // reachable, so the retirement is deferred for a later BOYD (which
131
+ // would drop the vref first)
132
+ //
133
+ // To build this, we have an anchored (virtual) MapStore msA holding
134
+ // the only reference (vdata) to a (virtual) WeakSetStore wssB. wssB
135
+ // has one (weak) key, o-1, for which there is a Presence P.
136
+ //
137
+ // 1: Construct everything, kill the wssB Representative, BOYD. That
138
+ // will process wssB, but leave it alive because of the vdata in
139
+ // msA.
140
+
141
+ // 2: Use msA.delete(key) to drop its vdata ref to wssB (adding wssB
142
+ // to possiblyDeadSet), and use gcTools.kill(P) to mock-mark it
143
+ // as COLLECTED (but not finalized)
144
+
145
+ // 3: Do a BOYD. The first pass will see wssB is unreachable, and
146
+ // delete it. The collection deleter will put o-1 in
147
+ // possiblyRetiredSet. There might be a second pass (doMore=1),
148
+ // but it won't have anything in possiblyDeadSet and will do
149
+ // nothing. Then the post-deadSet loop will process
150
+ // possiblyRetiredSet, which will contain our o-1. That
151
+ // processing step contains the valToSlot.has (or the
152
+ // would-be-incorrect getValForSlot) that we want to exercise.
153
+
154
+ let myPresence;
155
+ let myWeakStore;
156
+
157
+ function buildRootObject(vatPowers) {
158
+ const { VatData } = vatPowers;
159
+ const { makeScalarBigMapStore, makeScalarBigWeakSetStore } = VatData;
160
+ const store = makeScalarBigMapStore();
161
+ myWeakStore = makeScalarBigWeakSetStore();
162
+ return Far('root', {
163
+ store: p => {
164
+ myPresence = p;
165
+ myWeakStore.add(p);
166
+ t.truthy(myWeakStore.has(p));
167
+ store.init('weakstore', myWeakStore);
168
+ },
169
+ dropWeakStore: () => store.delete('weakstore'),
170
+ });
171
+ }
172
+
173
+ const makeNS = () => ({ buildRootObject });
174
+ const ls = makeLiveSlots(syscall, 'vatA', {}, {}, gcTools, undefined, makeNS);
175
+ const { dispatch, testHooks } = ls;
176
+ const { possiblyDeadSet, possiblyRetiredSet, slotToVal } = testHooks;
177
+ await dispatch(makeStartVat(kser()));
178
+
179
+ // step 1 (setup): store, kill WeakSetStore representative, BOYD
180
+ await dispatch(makeMessage('o+0', 'store', [kslot('o-1')]));
181
+ t.truthy(myPresence);
182
+ gcTools.kill(myWeakStore);
183
+ gcTools.flushAllFRs();
184
+ await dispatch(makeBringOutYourDead());
185
+ log.length = 0;
186
+
187
+ // myPresence vref is held by the Presence, and recognized by myWeakStore
188
+ t.is(possiblyDeadSet.size, 0);
189
+ t.is(possiblyRetiredSet.size, 0);
190
+
191
+ // step 2: delete vdata ref to weakstore, make myPresence COLLECTED
192
+ await dispatch(makeMessage('o+0', 'dropWeakStore', []));
193
+ gcTools.kill(myPresence);
194
+ log.length = 0;
195
+ // weakstore is possiblyDead (NARRATORS VOICE: it's dead). Presence
196
+ // is not, because the finalizer hasn't run.
197
+ t.is(possiblyDeadSet.size, 1);
198
+ t.is(possiblyRetiredSet.size, 0);
199
+ t.not([...possiblyDeadSet][0], 'o-1');
200
+ // the empty weakref is still there
201
+ t.true(slotToVal.has('o-1'));
202
+
203
+ // step 3: BOYD. It will collect myWeakStore on the first pass,
204
+ // whose deleter should clear all entries, which will add its
205
+ // recognized vrefs to possiblyRetiredSet. The post-pass will check
206
+ // o-1 for reachability with slotToVal.has, and because that says it
207
+ // is reachable, it will not be retired (even though it has no
208
+ // recognizer by now).
209
+ //
210
+ // *If* scanForDeadObjects() were mistakenly using getValForSlot()
211
+ // *instead of slotToVal.has(), we would see a retireImports here,
212
+ // *which would be a vat-fatal error, because we haven't seen a
213
+ // *dropImports yet.
214
+ await dispatch(makeBringOutYourDead());
215
+ // I tested this manually, by modifying boyd-gc.js *to use
216
+ // getValForSlot, and observed that this deepEqual(justGC(log), [])
217
+ // failed: it had an unexpected retireImports
218
+ t.deepEqual(justGC(log), []);
219
+ log.length = 0;
220
+
221
+ // eventually, the finalizer runs
222
+ gcTools.flushAllFRs();
223
+
224
+ // *now* a BOYD will drop+retire
225
+ await dispatch(makeBringOutYourDead());
226
+ t.deepEqual(justGC(log), [
227
+ { type: 'dropImports', slots: ['o-1'] },
228
+ { type: 'retireImports', slots: ['o-1'] },
229
+ ]);
230
+ });