Skip to content

Commit

Permalink
Fix a race condition with re-used compilation isolate IDs (#2018)
Browse files Browse the repository at this point in the history
Closes #2004
  • Loading branch information
nex3 committed Jun 21, 2023
1 parent 62f29c8 commit a48ced8
Show file tree
Hide file tree
Showing 7 changed files with 50 additions and 19 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

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

* Fix a race condition where the embedded compiler could deadlock or crash if a
compilation ID was reused immediately after the compilation completed.

## 1.63.4

### JavaScript API
Expand Down
15 changes: 11 additions & 4 deletions lib/src/embedded/dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -322,10 +322,17 @@ class Dispatcher {
var protobufWriter = CodedBufferWriter();
message.writeToCodedBufferWriter(protobufWriter);

var packet =
Uint8List(_compilationIdVarint.length + protobufWriter.lengthInBytes);
packet.setAll(0, _compilationIdVarint);
protobufWriter.writeTo(packet, _compilationIdVarint.length);
// Add one additional byte to the beginning to indicate whether or not the
// compilation is finished, so the [IsolateDispatcher] knows whether to
// treat this isolate as inactive.
var packet = Uint8List(
1 + _compilationIdVarint.length + protobufWriter.lengthInBytes);
packet[0] =
message.whichMessage() == OutboundMessage_Message.compileResponse
? 1
: 0;
packet.setAll(1, _compilationIdVarint);
protobufWriter.writeTo(packet, 1 + _compilationIdVarint.length);
_channel.sink.add(packet);
}
}
31 changes: 19 additions & 12 deletions lib/src/embedded/isolate_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -158,19 +158,27 @@ class IsolateDispatcher {
var receivePort = ReceivePort();
isolate.sink.add((receivePort.sendPort, compilationId));

var channel = IsolateChannel<Uint8List?>.connectReceive(receivePort)
.transform(const ExplicitCloseTransformer());
channel.stream.listen(_channel.sink.add,
var channel = IsolateChannel<Uint8List>.connectReceive(receivePort);
channel.stream.listen(
(message) {
// The first byte of messages from isolates indicates whether the
// entire compilation is finished. Sending this as part of the message
// buffer rather than a separate message avoids a race condition where
// the host might send a new compilation request with the same ID as
// one that just finished before the [IsolateDispatcher] receives word
// that the isolate with that ID is done. See sass/dart-sass#2004.
if (message[0] == 1) {
channel.sink.close();
_activeIsolates.remove(compilationId);
_inactiveIsolates.add(isolate);
resource.release();
}
_channel.sink.add(Uint8List.sublistView(message, 1));
},
onError: (Object error, StackTrace stackTrace) =>
_handleError(error, stackTrace),
onDone: () {
_activeIsolates.remove(compilationId);
if (_closed) {
isolate.sink.close();
} else {
_inactiveIsolates.add(isolate);
}
resource.release();
if (_closed) isolate.sink.close();
});
_activeIsolates[compilationId] = channel.sink;
return channel.sink;
Expand Down Expand Up @@ -228,8 +236,7 @@ void _isolateMain(SendPort sendPort) {
channel.stream.listen((initialMessage) async {
var (compilationSendPort, compilationId) = initialMessage;
var compilationChannel =
IsolateChannel<Uint8List?>.connectSend(compilationSendPort)
.transform(const ExplicitCloseTransformer());
IsolateChannel<Uint8List>.connectSend(compilationSendPort);
var success = await Dispatcher(compilationChannel, compilationId).listen();
if (!success) channel.sink.close();
});
Expand Down
4 changes: 4 additions & 0 deletions pkg/sass_api/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 7.1.5

* No user-visible changes.

## 7.1.4

* No user-visible changes.
Expand Down
4 changes: 2 additions & 2 deletions pkg/sass_api/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ name: sass_api
# Note: Every time we add a new Sass AST node, we need to bump the *major*
# version because it's a breaking change for anyone who's implementing the
# visitor interface(s).
version: 7.1.4
version: 7.1.5
description: Additional APIs for Dart Sass.
homepage: https://github.com/sass/dart-sass

environment:
sdk: ">=3.0.0 <4.0.0"

dependencies:
sass: 1.63.4
sass: 1.63.5

dev_dependencies:
dartdoc: ^5.0.0
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: sass
version: 1.63.5-dev
version: 1.63.5
description: A Sass implementation in Dart.
homepage: https://github.com/sass/dart-sass

Expand Down
10 changes: 10 additions & 0 deletions test/embedded/protocol_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,16 @@ void main() {
await process.shouldExit(0);
});

// Regression test for sass/dart-sass#2004
test("handles many sequential compilation requests", () async {
var totalRequests = 1000;
for (var i = 1; i <= totalRequests; i++) {
process.send(compileString("a {b: 1px + 2px}"));
await expectSuccess(process, "a { b: 3px; }");
}
await process.close();
});

test("closes gracefully with many in-flight compilations", () async {
// This should always be equal to the size of
// [IsolateDispatcher._isolatePool], since that's as many concurrent
Expand Down

0 comments on commit a48ced8

Please sign in to comment.