Skip to content

Commit a48ced8

Browse files
authored
Fix a race condition with re-used compilation isolate IDs (#2018)
Closes #2004
1 parent 62f29c8 commit a48ced8

File tree

7 files changed

+50
-19
lines changed

7 files changed

+50
-19
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
* Fix a deadlock when running at high concurrency on 32-bit systems.
1111

12+
* Fix a race condition where the embedded compiler could deadlock or crash if a
13+
compilation ID was reused immediately after the compilation completed.
14+
1215
## 1.63.4
1316

1417
### JavaScript API

lib/src/embedded/dispatcher.dart

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -322,10 +322,17 @@ class Dispatcher {
322322
var protobufWriter = CodedBufferWriter();
323323
message.writeToCodedBufferWriter(protobufWriter);
324324

325-
var packet =
326-
Uint8List(_compilationIdVarint.length + protobufWriter.lengthInBytes);
327-
packet.setAll(0, _compilationIdVarint);
328-
protobufWriter.writeTo(packet, _compilationIdVarint.length);
325+
// Add one additional byte to the beginning to indicate whether or not the
326+
// compilation is finished, so the [IsolateDispatcher] knows whether to
327+
// treat this isolate as inactive.
328+
var packet = Uint8List(
329+
1 + _compilationIdVarint.length + protobufWriter.lengthInBytes);
330+
packet[0] =
331+
message.whichMessage() == OutboundMessage_Message.compileResponse
332+
? 1
333+
: 0;
334+
packet.setAll(1, _compilationIdVarint);
335+
protobufWriter.writeTo(packet, 1 + _compilationIdVarint.length);
329336
_channel.sink.add(packet);
330337
}
331338
}

lib/src/embedded/isolate_dispatcher.dart

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -158,19 +158,27 @@ class IsolateDispatcher {
158158
var receivePort = ReceivePort();
159159
isolate.sink.add((receivePort.sendPort, compilationId));
160160

161-
var channel = IsolateChannel<Uint8List?>.connectReceive(receivePort)
162-
.transform(const ExplicitCloseTransformer());
163-
channel.stream.listen(_channel.sink.add,
161+
var channel = IsolateChannel<Uint8List>.connectReceive(receivePort);
162+
channel.stream.listen(
163+
(message) {
164+
// The first byte of messages from isolates indicates whether the
165+
// entire compilation is finished. Sending this as part of the message
166+
// buffer rather than a separate message avoids a race condition where
167+
// the host might send a new compilation request with the same ID as
168+
// one that just finished before the [IsolateDispatcher] receives word
169+
// that the isolate with that ID is done. See sass/dart-sass#2004.
170+
if (message[0] == 1) {
171+
channel.sink.close();
172+
_activeIsolates.remove(compilationId);
173+
_inactiveIsolates.add(isolate);
174+
resource.release();
175+
}
176+
_channel.sink.add(Uint8List.sublistView(message, 1));
177+
},
164178
onError: (Object error, StackTrace stackTrace) =>
165179
_handleError(error, stackTrace),
166180
onDone: () {
167-
_activeIsolates.remove(compilationId);
168-
if (_closed) {
169-
isolate.sink.close();
170-
} else {
171-
_inactiveIsolates.add(isolate);
172-
}
173-
resource.release();
181+
if (_closed) isolate.sink.close();
174182
});
175183
_activeIsolates[compilationId] = channel.sink;
176184
return channel.sink;
@@ -228,8 +236,7 @@ void _isolateMain(SendPort sendPort) {
228236
channel.stream.listen((initialMessage) async {
229237
var (compilationSendPort, compilationId) = initialMessage;
230238
var compilationChannel =
231-
IsolateChannel<Uint8List?>.connectSend(compilationSendPort)
232-
.transform(const ExplicitCloseTransformer());
239+
IsolateChannel<Uint8List>.connectSend(compilationSendPort);
233240
var success = await Dispatcher(compilationChannel, compilationId).listen();
234241
if (!success) channel.sink.close();
235242
});

pkg/sass_api/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 7.1.5
2+
3+
* No user-visible changes.
4+
15
## 7.1.4
26

37
* No user-visible changes.

pkg/sass_api/pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ name: sass_api
22
# Note: Every time we add a new Sass AST node, we need to bump the *major*
33
# version because it's a breaking change for anyone who's implementing the
44
# visitor interface(s).
5-
version: 7.1.4
5+
version: 7.1.5
66
description: Additional APIs for Dart Sass.
77
homepage: https://github.com/sass/dart-sass
88

99
environment:
1010
sdk: ">=3.0.0 <4.0.0"
1111

1212
dependencies:
13-
sass: 1.63.4
13+
sass: 1.63.5
1414

1515
dev_dependencies:
1616
dartdoc: ^5.0.0

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: sass
2-
version: 1.63.5-dev
2+
version: 1.63.5
33
description: A Sass implementation in Dart.
44
homepage: https://github.com/sass/dart-sass
55

test/embedded/protocol_test.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,16 @@ void main() {
224224
await process.shouldExit(0);
225225
});
226226

227+
// Regression test for sass/dart-sass#2004
228+
test("handles many sequential compilation requests", () async {
229+
var totalRequests = 1000;
230+
for (var i = 1; i <= totalRequests; i++) {
231+
process.send(compileString("a {b: 1px + 2px}"));
232+
await expectSuccess(process, "a { b: 3px; }");
233+
}
234+
await process.close();
235+
});
236+
227237
test("closes gracefully with many in-flight compilations", () async {
228238
// This should always be equal to the size of
229239
// [IsolateDispatcher._isolatePool], since that's as many concurrent

0 commit comments

Comments
 (0)