Skip to content

Commit e2c8c23

Browse files
mralephCommit Queue
authored andcommitted
[io] Fix _NativeSocket.shutdownRead
When shutting down receive direction we need to ensure that `closedRead` event is dispatched - as the receive direction is only considered closed if the available data is drained and `closedRead` is dispatched. The code did not account for a possibility that socket has no available data and as such is not dispatching read events so closing receive direction and then separately closing send direction would leave the socket in a state where both directions are closed but the socket is not disposed because `closedRead` is not dispatched - such socket objects will simply leak (even if the other side terminates the connection). This CL also updates documentation around `RawSocket.shutdown` and `RawSocket.readEventsEnabled` to make it clear that users are responsible for draining accumulated data if they want to shutdown receive direction. Fixes #27414 TEST=standalone/io/issue_27414 CoreLibraryReviewExempt: Documentation only changes in VM specific library Change-Id: I4b0ffb4cc67836c2849ec6e49b788a4f3b4c07d3 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/396340 Reviewed-by: Brian Quinlan <[email protected]> Commit-Queue: Slava Egorov <[email protected]>
1 parent c872db1 commit e2c8c23

File tree

4 files changed

+111
-6
lines changed

4 files changed

+111
-6
lines changed

sdk/lib/_internal/vm/bin/socket_patch.dart

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1623,21 +1623,29 @@ base class _NativeSocket extends _NativeSocketNativeWrapper
16231623
void issue() {
16241624
readEventIssued = false;
16251625
if (isClosing) return;
1626+
// Note: it is by design that we don't deliver closedRead event
1627+
// unless read events are enabled. This also means we will not
1628+
// fully close (and dispose) of the socket unless it is drained
1629+
// of accumulated incomming data.
16261630
if (!sendReadEvents) return;
16271631
if (stopRead()) {
16281632
if (isClosedRead && !closedReadEventSent) {
16291633
if (isClosedWrite) close();
1634+
16301635
var handler = closedEventHandler;
16311636
if (handler == null) return;
1637+
16321638
closedReadEventSent = true;
16331639
handler();
16341640
}
16351641
return;
16361642
}
1643+
16371644
var handler = readEventHandler;
16381645
if (handler == null) return;
1639-
readEventIssued = true;
16401646
handler();
1647+
1648+
readEventIssued = true;
16411649
scheduleMicrotask(issue);
16421650
}
16431651

@@ -1846,6 +1854,9 @@ base class _NativeSocket extends _NativeSocketNativeWrapper
18461854
sendToEventHandler(1 << shutdownReadCommand);
18471855
}
18481856
isClosedRead = true;
1857+
// Make sure to dispatch a closedRead event. Shutdown is only complete
1858+
// once the socket is drained of data and readClosed is dispatched.
1859+
issueReadEvent();
18491860
}
18501861
}
18511862

sdk/lib/io/socket.dart

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,12 @@ final class ConnectionTask<S> {
619619
/// ([RawSocketEvent.closed]).
620620
abstract interface class RawSocket implements Stream<RawSocketEvent> {
621621
/// Set or get, if the [RawSocket] should listen for [RawSocketEvent.read]
622-
/// events. Default is `true`.
622+
/// and [RawSocketEvent.readClosed] events. Default is `true`.
623+
///
624+
/// Warning: setting [readEventsEnabled] to `false` might prevent socket
625+
/// from fully closing when [SocketDirection.receive] and
626+
/// [SocketDirection.send] directions are shutdown independently. See
627+
/// [shutdown] for more details.
623628
abstract bool readEventsEnabled;
624629

625630
/// Set or get, if the [RawSocket] should listen for [RawSocketEvent.write]
@@ -636,8 +641,8 @@ abstract interface class RawSocket implements Stream<RawSocketEvent> {
636641
/// The [host] can either be a [String] or an [InternetAddress]. If [host] is a
637642
/// [String], [connect] will perform a [InternetAddress.lookup] and try
638643
/// all returned [InternetAddress]es, until connected. If IPv4 and IPv6
639-
/// addresses are both availble then connections over IPv4 are preferred. If
640-
/// no connection can be establed then the error from the first failing
644+
/// addresses are both available then connections over IPv4 are preferred. If
645+
/// no connection can be established then the error from the first failing
641646
/// connection is returned.
642647
///
643648
/// The argument [sourceAddress] can be used to specify the local
@@ -788,6 +793,17 @@ abstract interface class RawSocket implements Stream<RawSocketEvent> {
788793
/// and calling it several times is supported. Calling
789794
/// shutdown with either [SocketDirection.both] or [SocketDirection.receive]
790795
/// can result in a [RawSocketEvent.readClosed] event.
796+
///
797+
/// Warning: [SocketDirection.receive] direction is only considered to be
798+
/// to be fully shutdown once all available data is drained and
799+
/// [RawSocketEvent.readClosed] is dispatched. Shutting down
800+
/// [SocketDirection.receive] and [SocketDirection.send] directions separately
801+
/// without draining the data will lead to socket staying around until the
802+
/// data is drained. This can happen if [readEventsEnabled] is set
803+
/// to `false` or if received data is not [read] in response to these
804+
/// events. This does not apply to shutting down both directions
805+
/// simultaneously using [SocketDirection.both] which will discard all
806+
/// received data instead.
791807
void shutdown(SocketDirection direction);
792808

793809
/// Customize the [RawSocket].

tests/standalone/io/issue_22636_test.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
import "dart:async";
1010
import "dart:io";
11+
1112
import "package:expect/expect.dart";
13+
import "package:expect/async_helper.dart";
1214

1315
final Duration delay = new Duration(milliseconds: 100);
1416
final List<int> data = new List.generate(100, (i) => i % 20 + 65);
@@ -28,10 +30,11 @@ void serverListen(RawSocket serverSide) {
2830
serverSide.writeEventsEnabled = true;
2931
});
3032
} else {
31-
new Future.delayed(delay, () {
33+
new Future.delayed(delay, () async {
3234
Expect.isTrue(serverReadClosedReceived);
3335
serverSide.shutdown(SocketDirection.send);
34-
server.close();
36+
await server.close();
37+
asyncEnd();
3538
});
3639
}
3740
break;
@@ -58,5 +61,6 @@ Future test() async {
5861
}
5962

6063
void main() {
64+
asyncStart();
6165
test();
6266
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
//
5+
// This test verifies that shuting down receive and send directions separately
6+
// on a socket correctly shuts the socket down instead of leaking it.
7+
8+
import 'dart:async';
9+
import 'dart:convert';
10+
import 'dart:io';
11+
12+
import 'package:expect/expect.dart';
13+
import 'package:expect/async_helper.dart';
14+
15+
const messageContent = "hello, from the client!";
16+
late RawServerSocket server;
17+
late StreamSubscription clientSubscription;
18+
19+
void handleConnection(RawSocket serverSide) {
20+
var readClosedReceived = false;
21+
22+
void serveData(RawSocketEvent event) async {
23+
switch (event) {
24+
case RawSocketEvent.read:
25+
final data = serverSide.read();
26+
Expect.equals(messageContent, utf8.decode(data!));
27+
28+
// There might be a read event in flight, wait for microtasks to drain
29+
// and then shutdown read and write directions separately. This
30+
// should cause [readClosed] to be dispatched.
31+
Future.delayed(Duration(milliseconds: 0), () {
32+
serverSide.shutdown(SocketDirection.receive);
33+
serverSide.shutdown(SocketDirection.send);
34+
});
35+
break;
36+
37+
case RawSocketEvent.readClosed:
38+
Expect.isFalse(readClosedReceived);
39+
readClosedReceived = true;
40+
break;
41+
42+
case RawSocketEvent.closed:
43+
Expect.isTrue(readClosedReceived);
44+
await clientSubscription.cancel();
45+
await server.close();
46+
asyncEnd();
47+
break;
48+
}
49+
}
50+
51+
serverSide.listen(serveData);
52+
}
53+
54+
Future test() async {
55+
server = await RawServerSocket.bind(InternetAddress.loopbackIPv4, 0);
56+
server.listen(handleConnection);
57+
58+
final client = await RawSocket.connect(
59+
InternetAddress.loopbackIPv4,
60+
server.port,
61+
);
62+
clientSubscription = client.listen((RawSocketEvent event) {
63+
switch (event) {
64+
case RawSocketEvent.write:
65+
client.write(utf8.encode(messageContent));
66+
break;
67+
}
68+
});
69+
}
70+
71+
void main() {
72+
asyncStart();
73+
test();
74+
}

0 commit comments

Comments
 (0)