diff --git a/BUILD.gn b/BUILD.gn index c25c17660a1..6c26e7eacec 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -61,6 +61,10 @@ group("runtime") { "utils/dds:dds", ] } + + if (is_linux || is_android) { + deps += [ "runtime/bin:abstract_socket_test" ] + } } # A separate target and not included in group("runtime"). This way the target\ @@ -82,6 +86,9 @@ group("runtime_precompiled") { "runtime/bin:dart_precompiled_runtime", "runtime/bin:process_test", ] + if (is_linux || is_android) { + deps += [ "runtime/bin:abstract_socket_test" ] + } } group("create_sdk") { diff --git a/runtime/bin/BUILD.gn b/runtime/bin/BUILD.gn index 42d895aedbe..ff820cf4713 100644 --- a/runtime/bin/BUILD.gn +++ b/runtime/bin/BUILD.gn @@ -875,6 +875,11 @@ executable("process_test") { sources = [ "process_test.cc" ] } +executable("abstract_socket_test") { + sources = [ "abstract_socket_test.cc" ] + include_dirs = [ ".." ] +} + executable("run_vm_tests") { if (target_os == "fuchsia") { testonly = true diff --git a/runtime/bin/abstract_socket_test.cc b/runtime/bin/abstract_socket_test.cc new file mode 100644 index 00000000000..7af06b61a20 --- /dev/null +++ b/runtime/bin/abstract_socket_test.cc @@ -0,0 +1,104 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// This is a utility program for testing that a Dart program can connect to an +// abstract UNIX socket created by a non-Dart program. It creates such a socket +// accepts one connection, echoes back the first message it receives, and then +// closes the connection and UNIX socket. + +#include "platform/globals.h" +#if defined(HOST_OS_LINUX) || defined(HOST_OS_ANDROID) + +#include +#include +#include +#include +#include + +const intptr_t kOffsetOfPtr = 32; + +#define OFFSET_OF(type, field) \ + (reinterpret_cast( \ + &(reinterpret_cast(kOffsetOfPtr)->field)) - \ + kOffsetOfPtr) // NOLINT + +int main(int argc, char* argv[]) { + struct sockaddr_un addr; + char* socket_path; + int server_socket; + char buf[1024]; + + if (argc < 2) { + fprintf( + stderr, + "Usage: abstract_socket_test
\n\n" + "
should be an abstract UNIX socket address like @hidden\n"); + exit(-1); + } + + socket_path = argv[1]; + if (socket_path[0] != '@') { + fprintf(stderr, + "The first argument should be an abstract socket " + "address and start with '@'\n"); + exit(-1); + } + + if ((server_socket = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { + perror("socket error"); + exit(-1); + } + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + addr.sun_path[0] = '\0'; + strncpy(addr.sun_path + 1, socket_path + 1, sizeof(addr.sun_path) - 2); + + int address_length = + OFFSET_OF(struct sockaddr_un, sun_path) + strlen(socket_path); + if (bind(server_socket, (struct sockaddr*)&addr, address_length) == -1) { + perror("bind error"); + exit(-1); + } + + if (listen(server_socket, 5) == -1) { + perror("listen error"); + exit(-1); + } + + int client_socket; + if ((client_socket = accept(server_socket, NULL, NULL)) == -1) { + perror("accept error"); + exit(-1); + } + + int read_count; + while ((read_count = read(client_socket, buf, sizeof(buf))) > 0) { + int write_count = 0; + while (write_count < read_count) { + int w; + if ((w = write(client_socket, buf, read_count)) < 0) { + perror("write"); + exit(-1); + } + write_count += w; + } + } + if (read_count == -1) { + perror("read"); + exit(-1); + } + + close(client_socket); + close(server_socket); + return 0; +} + +#else + +int main() { + return -1; +} + +#endif // defined(HOST_OS_LINUX) || defined(HOST_OS_ANDROID) diff --git a/runtime/bin/socket_base.cc b/runtime/bin/socket_base.cc index e90b1381ca5..7a62ab8550f 100644 --- a/runtime/bin/socket_base.cc +++ b/runtime/bin/socket_base.cc @@ -42,8 +42,26 @@ intptr_t SocketAddress::GetAddrLength(const RawAddr& addr) { return sizeof(struct sockaddr_in6); case AF_INET: return sizeof(struct sockaddr_in); - case AF_UNIX: - return sizeof(struct sockaddr_un); + case AF_UNIX: { + // For an abstract UNIX socket, trailing null bytes in the name are + // meaningful. That is, the bytes '\0/tmp/dbus-xxxx' are a different name + // than '\0/tmp/dbus-xxxx\0\0\0...'. The length of the address structure + // passed to connect() etc. tells those calls how many bytes of the name + // to look at. Therefore, when computing the length of the address in + // this case, any trailing null bytes are trimmed. + // TODO(dart:io): Support abstract UNIX socket addresses that have + // trailing null bytes on purpose. + // https://github.com/dart-lang/sdk/issues/46158 + intptr_t nulls = 0; + if (addr.un.sun_path[0] == '\0') { + intptr_t i = sizeof(addr.un.sun_path) - 1; + while (addr.un.sun_path[i] == '\0') { + nulls++; + i--; + } + } + return sizeof(struct sockaddr_un) - nulls; + } default: UNREACHABLE(); return 0; diff --git a/runtime/bin/socket_linux.cc b/runtime/bin/socket_linux.cc index e6b7e5cb1e6..686852f2b85 100644 --- a/runtime/bin/socket_linux.cc +++ b/runtime/bin/socket_linux.cc @@ -223,7 +223,7 @@ intptr_t ServerSocket::CreateUnixDomainBindListen(const RawAddr& addr, intptr_t backlog) { intptr_t fd = Create(addr); if (NO_RETRY_EXPECTED(bind(fd, (struct sockaddr*)&addr.un, - sizeof(struct sockaddr_un))) < 0) { + SocketAddress::GetAddrLength(addr))) < 0) { FDUtils::SaveErrorAndClose(fd); return -1; } diff --git a/tests/standalone/io/unix_socket_test.dart b/tests/standalone/io/unix_socket_test.dart index e3fec83ddaf..d5ee7cb5b41 100644 --- a/tests/standalone/io/unix_socket_test.dart +++ b/tests/standalone/io/unix_socket_test.dart @@ -163,6 +163,47 @@ Future testAbstractAddress() async { await completer.future; } +String getAbstractSocketTestFileName() { + var executable = Platform.executable; + var dirIndex = executable.lastIndexOf('dart'); + var buffer = new StringBuffer(executable.substring(0, dirIndex)); + buffer.write('abstract_socket_test'); + return buffer.toString(); +} + +Future testShortAbstractAddress() async { + if (!Platform.isLinux && !Platform.isAndroid) { + return; + } + Process? process; + try { + var socketAddress = '@hidden'; + var abstractSocketServer = getAbstractSocketTestFileName(); + process = await Process.start(abstractSocketServer, [socketAddress]); + var serverAddress = + InternetAddress(socketAddress, type: InternetAddressType.unix); + Socket client = await Socket.connect(serverAddress, 0); + List sendData = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + List data = []; + var completer = Completer(); + client.listen(data.addAll, onDone: () { + Expect.listEquals(sendData, data); + completer.complete(); + }); + client.add(sendData); + await client.close(); + await completer.future; + client.destroy(); + var exitCode = await process.exitCode; + process = null; + Expect.equals(exitCode, 0); + } catch (e, st) { + Expect.fail('Failed with exception:\n$e\n$st'); + } finally { + process?.kill(ProcessSignal.sigkill); + } +} + Future testExistingFile(String name) async { // Test that a leftover file(In case of previous process being killed and // finalizer doesn't clean up the file) will be cleaned up and bind() should @@ -398,6 +439,7 @@ void main() async { await withTempDir('unix_socket_test', (Directory dir) async { await testHttpServer('${dir.path}'); }); + await testShortAbstractAddress(); } catch (e) { if (Platform.isMacOS || Platform.isLinux || Platform.isAndroid) { Expect.fail("Unexpected exception $e is thrown"); diff --git a/tests/standalone_2/io/unix_socket_test.dart b/tests/standalone_2/io/unix_socket_test.dart index 31acd7bd8af..aa86ae8cf7a 100644 --- a/tests/standalone_2/io/unix_socket_test.dart +++ b/tests/standalone_2/io/unix_socket_test.dart @@ -165,6 +165,47 @@ Future testAbstractAddress() async { await completer.future; } +String getAbstractSocketTestFileName() { + var executable = Platform.executable; + var dirIndex = executable.lastIndexOf('dart'); + var buffer = new StringBuffer(executable.substring(0, dirIndex)); + buffer.write('abstract_socket_test'); + return buffer.toString(); +} + +Future testShortAbstractAddress() async { + if (!Platform.isLinux && !Platform.isAndroid) { + return; + } + Process process; + try { + var socketAddress = '@hidden'; + var abstractSocketServer = getAbstractSocketTestFileName(); + process = await Process.start(abstractSocketServer, [socketAddress]); + var serverAddress = + InternetAddress(socketAddress, type: InternetAddressType.unix); + Socket client = await Socket.connect(serverAddress, 0); + List sendData = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + List data = []; + var completer = Completer(); + client.listen(data.addAll, onDone: () { + Expect.listEquals(sendData, data); + completer.complete(); + }); + client.add(sendData); + await client.close(); + await completer.future; + client.destroy(); + var exitCode = await process.exitCode; + process = null; + Expect.equals(exitCode, 0); + } catch (e, st) { + Expect.fail('Failed with exception:\n$e\n$st'); + } finally { + process?.kill(ProcessSignal.sigkill); + } +} + Future testExistingFile(String name) async { // Test that a leftover file(In case of previous process being killed and // finalizer doesn't clean up the file) will be cleaned up and bind() should @@ -400,6 +441,7 @@ void main() async { await withTempDir('unix_socket_test', (Directory dir) async { await testHttpServer('${dir.path}'); }); + await testShortAbstractAddress(); } catch (e) { if (Platform.isMacOS || Platform.isLinux || Platform.isAndroid) { Expect.fail("Unexpected exception $e is thrown");