From 83c52d86f153437953abea4881a36ea238fa79b7 Mon Sep 17 00:00:00 2001 From: Rahul-Sutariya Date: Thu, 13 Aug 2026 17:36:42 +0530 Subject: [PATCH 1/2] feat(gateway/transport_layer): add qemu transport layer --- quality/coverage/coverage_justifications.yaml | 44 + score/mw/com/gateway/README.md | 8 +- .../mw/com/gateway/gateway_application/BUILD | 4 +- .../gateway_application/configuration/BUILD | 2 + score/mw/com/gateway/transport_layer/BUILD | 5 + .../mw/com/gateway/transport_layer/qemu/BUILD | 53 ++ .../gateway/transport_layer/qemu/README.md | 57 ++ .../transport_layer/qemu/configuration/BUILD | 51 ++ .../mw_com_gateway_qemu_transport_config.json | 11 + .../qemu_transport_config_parser.cpp | 84 ++ .../qemu_transport_config_parser.h | 29 + .../qemu_transport_config_parser_test.cpp | 87 ++ .../qemu_transport_configuration.h | 44 + .../transport_layer/qemu/ivshmem/BUILD | 89 ++ .../qemu/ivshmem/ivshmem_bar_discovery.cpp | 145 +++ .../qemu/ivshmem/ivshmem_bar_discovery.h | 61 ++ .../ivshmem/ivshmem_bar_discovery_test.cpp | 92 ++ .../ivshmem/ivshmem_typed_memory_provider.cpp | 489 ++++++++++ .../ivshmem/ivshmem_typed_memory_provider.h | 267 ++++++ .../ivshmem_typed_memory_provider_mock.cpp | 14 + .../ivshmem_typed_memory_provider_mock.h | 62 ++ .../ivshmem_typed_memory_provider_test.cpp | 840 ++++++++++++++++++ .../qemu/qemu_hypervisor_transport.cpp | 415 +++++++++ .../qemu/qemu_hypervisor_transport.h | 107 +++ .../qemu/qemu_hypervisor_transport_test.cpp | 689 ++++++++++++++ .../gateway/transport_layer/qemu/test/BUILD | 57 ++ .../transport_layer/qemu/test/app1_main.cpp | 366 ++++++++ .../transport_layer/qemu/test/app2_main.cpp | 364 ++++++++ .../qemu/test/integration_test/BUILD | 23 + .../dual_qemu_transport_config.json | 28 + .../integration_test/test_qemu_transport.py | 72 ++ .../transport_layer/transport_factory.cpp | 49 +- 32 files changed, 4696 insertions(+), 12 deletions(-) create mode 100644 score/mw/com/gateway/transport_layer/qemu/BUILD create mode 100644 score/mw/com/gateway/transport_layer/qemu/README.md create mode 100644 score/mw/com/gateway/transport_layer/qemu/configuration/BUILD create mode 100644 score/mw/com/gateway/transport_layer/qemu/configuration/example/mw_com_gateway_qemu_transport_config.json create mode 100644 score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_config_parser.cpp create mode 100644 score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_config_parser.h create mode 100644 score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_config_parser_test.cpp create mode 100644 score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_configuration.h create mode 100644 score/mw/com/gateway/transport_layer/qemu/ivshmem/BUILD create mode 100644 score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery.cpp create mode 100644 score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery.h create mode 100644 score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery_test.cpp create mode 100644 score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider.cpp create mode 100644 score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider.h create mode 100644 score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider_mock.cpp create mode 100644 score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider_mock.h create mode 100644 score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider_test.cpp create mode 100644 score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport.cpp create mode 100644 score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport.h create mode 100644 score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport_test.cpp create mode 100644 score/mw/com/gateway/transport_layer/qemu/test/BUILD create mode 100644 score/mw/com/gateway/transport_layer/qemu/test/app1_main.cpp create mode 100644 score/mw/com/gateway/transport_layer/qemu/test/app2_main.cpp create mode 100644 score/mw/com/gateway/transport_layer/qemu/test/integration_test/BUILD create mode 100644 score/mw/com/gateway/transport_layer/qemu/test/integration_test/dual_qemu_transport_config.json create mode 100644 score/mw/com/gateway/transport_layer/qemu/test/integration_test/test_qemu_transport.py diff --git a/quality/coverage/coverage_justifications.yaml b/quality/coverage/coverage_justifications.yaml index 6598ae317..06f1d0903 100644 --- a/quality/coverage/coverage_justifications.yaml +++ b/quality/coverage/coverage_justifications.yaml @@ -93,3 +93,47 @@ justifications: Coverage tool incorrectly marks this line as not covered. Lines immediately before and after are covered. Covered by the test BidirectionalTransportSocketFixture.DispatchThreadDeliversQueuedMessageToHandler inside score/mw/com/gateway/transport_layer/sample/bidirectional_transport_test.cpp + + - id: ivshmem-bind-shm-to-bar-shm-ctl-special-success + category: defensive_programming + platforms: [qnx] + reason: > + QNX x86_64 only: shm_ctl_special() needs a real ivshmem device and WB-backed shared memory, + so the mock-fd test path always hits EBADF. Fallback to standard shm_ctl() on failure. + locations: + - file: score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider.cpp + line_start: 321 + line_end: 329 + + - id: ivshmem-write-directory-entry-idempotent + category: defensive_programming + platforms: [qnx] + reason: > + Idempotent guard in WriteDirectoryEntry() requires multi-VM concurrent write race; + single-process tests cache allocations on first write, preventing re-entry. + locations: + - file: score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider.cpp + line_start: 222 + line_end: 230 + + - id: ivshmem-bar-discovery-hardware-path + category: defensive_programming + platforms: [qnx] + reason: > + QNX PCI API calls require real ivshmem device. Tests with no hardware hit pci_device_find→PCI_BDF_NONE + and cannot mock PCI subsystem APIs. + locations: + - file: score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery.cpp + line_start: 45 + line_end: 133 + + - id: qemu-worldwritable-ctor-gcc-artifact + category: defensive_programming + platforms: [qnx] + reason: > + GCC/GCOV x86_64 QNX emits separate counter for WorldWritable{} on continuation lines; + QCC omits sequence-point counter on trivial temporaries. + locations: + - file: score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport.cpp + line_start: 356 + line_end: 357 diff --git a/score/mw/com/gateway/README.md b/score/mw/com/gateway/README.md index 5f7d43f7b..11f7cf505 100644 --- a/score/mw/com/gateway/README.md +++ b/score/mw/com/gateway/README.md @@ -78,10 +78,12 @@ files in its domain: 2. The transport-layer config file referenced by the `config-path` field of the `transport-layer` section in `mw_com_gateway_config.json`. Its content is transport-specific and is parsed during `GatewayApplication::Setup()` by the selected transport (via `TransportFactory::Create()`). For the bundled - `sample_hypervisor` transport this is the hypervisor-socket config (remote IP, local/remote ports, request - timeout) — see its + `sample_hypervisor` and `qemu_hypervisor` transports this is the hypervisor-socket config (remote IP, + local/remote ports, request timeout) — see its [schema](transport_layer/sample/configuration/mw_com_gateway_sample_transport_config_schema.json) and - [example](transport_layer/sample/configuration/example/mw_com_gateway_sample_transport_config.json). + [example](transport_layer/sample/configuration/example/mw_com_gateway_sample_transport_config.json). For the + QEMU transport, the same file also carries the optional `ivshmem.preferred-bar-num` setting (default BAR2) + used when mapping the shared-memory BAR. 3. `mw_com_config.json` — standard `mw::com` deployment config (provided as for all `mw::com` applications). Required by the `mw::com` runtime to resolve the `InstanceSpecifier`s referenced in `mw_com_gateway_config.json` into concrete deployment info (binding, number of slots, ASIL level, allowed diff --git a/score/mw/com/gateway/gateway_application/BUILD b/score/mw/com/gateway/gateway_application/BUILD index 40340fe93..dce95b933 100644 --- a/score/mw/com/gateway/gateway_application/BUILD +++ b/score/mw/com/gateway/gateway_application/BUILD @@ -30,9 +30,7 @@ cc_library( "@score_baselibs//score/language/futurecpp", "@score_baselibs//score/mw/log", ], - visibility = [ - "//score/mw/com/gateway:__subpackages__", - ], + visibility = ["//score/mw/com/gateway:__subpackages__"], deps = [ ":gateway_core", "//score/mw/com/gateway/gateway_application/configuration:gateway_configuration", diff --git a/score/mw/com/gateway/gateway_application/configuration/BUILD b/score/mw/com/gateway/gateway_application/configuration/BUILD index 6308945e8..70b81e03e 100644 --- a/score/mw/com/gateway/gateway_application/configuration/BUILD +++ b/score/mw/com/gateway/gateway_application/configuration/BUILD @@ -37,6 +37,7 @@ cc_library( srcs = ["gateway_configuration.cpp"], hdrs = ["gateway_configuration.h"], features = COMPILER_WARNING_FEATURES, + visibility = ["//score/mw/com/gateway:__subpackages__"], deps = [ "@score_baselibs//score/language/futurecpp", ], @@ -50,6 +51,7 @@ cc_library( implementation_deps = [ "@score_baselibs//score/mw/log", ], + visibility = ["//score/mw/com/gateway:__subpackages__"], deps = [ ":gateway_configuration", "@score_baselibs//score/json", diff --git a/score/mw/com/gateway/transport_layer/BUILD b/score/mw/com/gateway/transport_layer/BUILD index 726c2e590..983e00006 100644 --- a/score/mw/com/gateway/transport_layer/BUILD +++ b/score/mw/com/gateway/transport_layer/BUILD @@ -56,7 +56,12 @@ cc_library( ], deps = [ ":transport", + "//score/memory/shared", "//score/mw/com/gateway/gateway_application:gateway_core", + "//score/mw/com/gateway/transport_layer/qemu:qemu_hypervisor_transport", + "//score/mw/com/gateway/transport_layer/qemu/configuration:qemu_transport_config_parser", + "//score/mw/com/gateway/transport_layer/qemu/ivshmem:ivshmem_bar_discovery", + "//score/mw/com/gateway/transport_layer/qemu/ivshmem:ivshmem_typed_memory_provider", "//score/mw/com/gateway/transport_layer/sample:bidirectional_transport", "//score/mw/com/gateway/transport_layer/sample:sample_hypervisor_transport", "//score/mw/com/gateway/transport_layer/sample/configuration:hypervisor_socket_configuration", diff --git a/score/mw/com/gateway/transport_layer/qemu/BUILD b/score/mw/com/gateway/transport_layer/qemu/BUILD new file mode 100644 index 000000000..9d320157c --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/BUILD @@ -0,0 +1,53 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@rules_cc//cc:defs.bzl", "cc_library") +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") +load("//quality/unit_testing:unit_testing.bzl", "cc_unit_test") + +cc_library( + name = "qemu_hypervisor_transport", + srcs = ["qemu_hypervisor_transport.cpp"], + hdrs = ["qemu_hypervisor_transport.h"], + features = COMPILER_WARNING_FEATURES + [ + "aborts_upon_exception", + ], + visibility = ["//score/mw/com/gateway:__subpackages__"], + deps = [ + "//score/memory/shared", + "//score/mw/com/gateway/gateway_application:gateway_core", + "//score/mw/com/gateway/transport_layer:transport", + "//score/mw/com/gateway/transport_layer/qemu/ivshmem:ivshmem_typed_memory_provider", + "//score/mw/com/gateway/transport_layer/sample:i_bidirectional_transport", + "//score/mw/com/gateway/transport_layer/sample:sample_hypervisor_transport", + "//score/mw/com/gateway/transport_layer/sample/messages:gateway_messages", + "@score_baselibs//score/mw/log", + ], +) + +cc_unit_test( + name = "qemu_hypervisor_transport_test", + srcs = ["qemu_hypervisor_transport_test.cpp"], + features = COMPILER_WARNING_FEATURES, + deps = [ + ":qemu_hypervisor_transport", + "//score/mw/com/gateway/gateway_application:gateway_core_mock", + "//score/mw/com/gateway/transport_layer:transport_error", + "//score/mw/com/gateway/transport_layer/qemu/ivshmem:ivshmem_typed_memory_provider_mock", + "//score/mw/com/gateway/transport_layer/sample:bidirectional_transport_mock", + "@score_baselibs//score/mw/log", + "@score_baselibs//score/mw/log:recorder_mock", + ] + select({ + "@platforms//os:qnx": ["@score_baselibs//score/os/mocklib/qnx:mman_mock"], + "//conditions:default": [], + }), +) diff --git a/score/mw/com/gateway/transport_layer/qemu/README.md b/score/mw/com/gateway/transport_layer/qemu/README.md new file mode 100644 index 000000000..8be34e233 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/README.md @@ -0,0 +1,57 @@ + + +# QEMU Transport Layer + +Inter-VM communication and shared memory exchange for LoLa gateway using **ivshmem** (Inter-VM Shared Memory BAR) for zero-copy data transfer between QEMU virtual machines. Extends the [sample transport layer](../sample/README.md) with ivshmem-based memory sharing. + +## What It Provides + +- **Message Communication** — Service provisioning and lifecycle events via TCP over QEMU's intervm network (reuses sample transport layer) +- **Shared Memory** — Zero-copy data exchange through ivshmem BAR mapping +- **Cross-VM Coordination** — Shared directory for consistent memory region access + +## Main Components + +- **QemuHypervisorTransport** — Orchestrates message and memory layers +- **IvshmemTypedMemoryProvider** — Manages named shared memory backed by ivshmem BAR + +## Configuration + +See [`configuration/example/mw_com_gateway_qemu_transport_config.json`](./configuration/example/mw_com_gateway_qemu_transport_config.json) + +The config contains the usual `hypervisor-socket` block plus an optional `ivshmem` block. +`ivshmem.preferred-bar-num` selects which PCI BAR to map for the shared-memory region and +defaults to BAR2 for QEMU `ivshmem-plain`. + +## How It Works + +**Offering a service (Source VM):** +- Allocate CTRL and DATA shared memory regions via ivshmem BAR ([`IvshmemTypedMemoryProvider::AllocateNamedTypedMemory`](./ivshmem/ivshmem_typed_memory_provider.h)) +- Register allocation offsets in the shared directory (visible to both VMs) +- Send service availability notification to remote gateway over TCP +- Remote VMs can now discover and map to the same physical memory regions + +**Consuming a service (Destination VM):** +- Receive service notification and look up memory offsets in the shared directory +- Bind local shared memory objects to those discovered offsets in ivshmem BAR ([`IvshmemTypedMemoryProvider::AllocateNamedTypedMemoryAtOffset`](./ivshmem/ivshmem_typed_memory_provider.h)) +- Both VMs now access the exact same physical memory + +See [`QemuHypervisorTransport`](./qemu_hypervisor_transport.h) for the orchestration logic. + +## Supported Platforms + +- **QNX 7.1+** — Full ivshmem support +- **Linux** — Message layer only diff --git a/score/mw/com/gateway/transport_layer/qemu/configuration/BUILD b/score/mw/com/gateway/transport_layer/qemu/configuration/BUILD new file mode 100644 index 000000000..d2c3da2d6 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/configuration/BUILD @@ -0,0 +1,51 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_cc//cc:defs.bzl", "cc_library") +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") +load("//quality/unit_testing:unit_testing.bzl", "cc_unit_test") + +package( + default_visibility = ["//score/mw/com/gateway/transport_layer/qemu:__subpackages__"], +) + +exports_files(["example/mw_com_gateway_qemu_transport_config.json"]) + +cc_library( + name = "qemu_transport_config_parser", + srcs = ["qemu_transport_config_parser.cpp"], + hdrs = [ + "qemu_transport_config_parser.h", + "qemu_transport_configuration.h", + ], + features = COMPILER_WARNING_FEATURES, + implementation_deps = [ + "@score_baselibs//score/mw/log", + ], + visibility = ["//score/mw/com/gateway/transport_layer:__subpackages__"], + deps = [ + "//score/mw/com/gateway/transport_layer/qemu/ivshmem:ivshmem_bar_discovery", + "@score_baselibs//score/json", + "@score_baselibs//score/language/futurecpp", + ], +) + +cc_unit_test( + name = "qemu_transport_config_parser_test", + srcs = ["qemu_transport_config_parser_test.cpp"], + deps = [ + ":qemu_transport_config_parser", + "@googletest//:gtest", + "@score_baselibs//score/json", + ], +) diff --git a/score/mw/com/gateway/transport_layer/qemu/configuration/example/mw_com_gateway_qemu_transport_config.json b/score/mw/com/gateway/transport_layer/qemu/configuration/example/mw_com_gateway_qemu_transport_config.json new file mode 100644 index 000000000..790c34051 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/configuration/example/mw_com_gateway_qemu_transport_config.json @@ -0,0 +1,11 @@ +{ + "hypervisor-socket": { + "remote-ip": "10.0.2.2", + "local-port": 45001, + "remote-port": 45002, + "request-timeout-ms": 5000 + }, + "ivshmem": { + "preferred-bar-num": 2 + } +} diff --git a/score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_config_parser.cpp b/score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_config_parser.cpp new file mode 100644 index 000000000..212a14d45 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_config_parser.cpp @@ -0,0 +1,84 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_config_parser.h" + +#include "score/json/json_parser.h" +#include "score/mw/log/logging.h" + +#include + +#include + +namespace score::mw::com::gateway::qemu +{ +namespace +{ + +using std::string_view_literals::operator""sv; + +constexpr auto kIvshmemConfigurationKey = "ivshmem"sv; +constexpr auto kPreferredBarNumKey = "preferred-bar-num"sv; + +} // namespace + +auto ParseQemuTransportConfig(const std::string_view path) noexcept -> QemuTransportConfiguration +{ + const score::json::JsonParser json_parser_obj; + // NOLINTNEXTLINE(score-banned-function): AoU of score::json::JsonParser — caller must guarantee path integrity. + auto json_result = json_parser_obj.FromFile(path); + if (!json_result.has_value()) + { + ::score::mw::log::LogFatal("lola") + << "Parsing qemu transport config file" << path << "failed with error:" << json_result.error().Message() + << ": " << json_result.error().UserMessage() << " . Terminating."; + std::terminate(); + } + return ParseQemuTransportConfig(std::move(json_result).value()); +} + +auto ParseQemuTransportConfig(score::json::Any json) noexcept -> QemuTransportConfiguration +{ + auto top_level_object = json.As(); + if (!top_level_object.has_value()) + { + ::score::mw::log::LogFatal("lola") + << "Parsing qemu transport configuration failed: Expected top-level JSON object. Terminating."; + std::terminate(); + } + + const auto& obj = top_level_object.value().get(); + + std::uint32_t preferred_bar_num = ivshmem::kDefaultIvshmemBarNum; + const auto ivshmem_entry = obj.find(kIvshmemConfigurationKey); + if (ivshmem_entry != obj.cend()) + { + const auto ivshmem_obj_result = ivshmem_entry->second.As(); + if (ivshmem_obj_result.has_value()) + { + const auto& ivshmem_obj = ivshmem_obj_result.value().get(); + const auto preferred_bar_num_entry = ivshmem_obj.find(kPreferredBarNumKey); + if (preferred_bar_num_entry != ivshmem_obj.cend()) + { + const auto preferred_bar_num_result = preferred_bar_num_entry->second.As(); + if (preferred_bar_num_result.has_value()) + { + preferred_bar_num = preferred_bar_num_result.value(); + } + } + } + } + + return QemuTransportConfiguration{preferred_bar_num}; +} + +} // namespace score::mw::com::gateway::qemu diff --git a/score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_config_parser.h b/score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_config_parser.h new file mode 100644 index 000000000..3317bc337 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_config_parser.h @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_CONFIGURATION_QEMU_TRANSPORT_CONFIG_PARSER_H +#define SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_CONFIGURATION_QEMU_TRANSPORT_CONFIG_PARSER_H + +#include "score/json/json_parser.h" +#include "score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_configuration.h" + +#include + +namespace score::mw::com::gateway::qemu +{ + +QemuTransportConfiguration ParseQemuTransportConfig(const std::string_view path) noexcept; +QemuTransportConfiguration ParseQemuTransportConfig(score::json::Any json) noexcept; + +} // namespace score::mw::com::gateway::qemu + +#endif // SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_CONFIGURATION_QEMU_TRANSPORT_CONFIG_PARSER_H diff --git a/score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_config_parser_test.cpp b/score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_config_parser_test.cpp new file mode 100644 index 000000000..2085cf87a --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_config_parser_test.cpp @@ -0,0 +1,87 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_config_parser.h" + +#include "score/json/json_parser.h" + +#include + +#include + +namespace score::mw::com::gateway::qemu +{ +namespace +{ + +using score::json::operator""_json; + +TEST(QemuTransportConfigParserTest, UsesDefaultIvshmemBarNumWhenMissing) +{ + auto json = R"JSON( + { + "hypervisor-socket": { + "remote-ip": "10.0.2.2", + "local-port": 45001, + "remote-port": 45002 + } + } + )JSON"_json; + + const auto config = ParseQemuTransportConfig(std::move(json)); + EXPECT_EQ(config.GetPreferredIvshmemBarNum(), ivshmem::kDefaultIvshmemBarNum); +} + +TEST(QemuTransportConfigParserTest, ReadsPreferredIvshmemBarNum) +{ + auto json = R"JSON( + { + "hypervisor-socket": { + "remote-ip": "10.0.2.2", + "local-port": 45001, + "remote-port": 45002 + }, + "ivshmem": { + "preferred-bar-num": 3 + } + } + )JSON"_json; + + const auto config = ParseQemuTransportConfig(std::move(json)); + EXPECT_EQ(config.GetPreferredIvshmemBarNum(), 3U); +} + +TEST(QemuTransportConfigParserTest, UsesDefaultWhenPreferredBarNumIsNotInteger) +{ + // Given a config where preferred-bar-num is a non-integer value + auto json = R"JSON( + { + "hypervisor-socket": { + "remote-ip": "10.0.2.2", + "local-port": 45001, + "remote-port": 45002 + }, + "ivshmem": { + "preferred-bar-num": "not-a-number" + } + } + )JSON"_json; + + // When parsing the config + const auto config = ParseQemuTransportConfig(std::move(json)); + + // Then the parser silently falls back to the default bar num instead of crashing or throwing + EXPECT_EQ(config.GetPreferredIvshmemBarNum(), ivshmem::kDefaultIvshmemBarNum); +} + +} // namespace +} // namespace score::mw::com::gateway::qemu diff --git a/score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_configuration.h b/score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_configuration.h new file mode 100644 index 000000000..fba1f5f2a --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_configuration.h @@ -0,0 +1,44 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_CONFIGURATION_QEMU_TRANSPORT_CONFIGURATION_H +#define SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_CONFIGURATION_QEMU_TRANSPORT_CONFIGURATION_H + +#include "score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery.h" + +#include + +namespace score::mw::com::gateway::qemu +{ + +class QemuTransportConfiguration +{ + public: + QemuTransportConfiguration() = default; + + explicit QemuTransportConfiguration(std::uint32_t preferred_ivshmem_bar_num) noexcept + : preferred_ivshmem_bar_num_{preferred_ivshmem_bar_num} + { + } + + std::uint32_t GetPreferredIvshmemBarNum() const noexcept + { + return preferred_ivshmem_bar_num_; + } + + private: + std::uint32_t preferred_ivshmem_bar_num_{ivshmem::kDefaultIvshmemBarNum}; +}; + +} // namespace score::mw::com::gateway::qemu + +#endif // SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_CONFIGURATION_QEMU_TRANSPORT_CONFIGURATION_H diff --git a/score/mw/com/gateway/transport_layer/qemu/ivshmem/BUILD b/score/mw/com/gateway/transport_layer/qemu/ivshmem/BUILD new file mode 100644 index 000000000..2be95f854 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/ivshmem/BUILD @@ -0,0 +1,89 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@rules_cc//cc:defs.bzl", "cc_library") +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") +load("//quality/unit_testing:unit_testing.bzl", "cc_unit_test") + +cc_library( + name = "ivshmem_bar_discovery", + srcs = ["ivshmem_bar_discovery.cpp"], + hdrs = ["ivshmem_bar_discovery.h"], + features = COMPILER_WARNING_FEATURES + [ + "aborts_upon_exception", + ], + linkopts = select({ + "@platforms//os:qnx": ["-lpci"], + "//conditions:default": [], + }), + visibility = ["//score/mw/com/gateway/transport_layer:__subpackages__"], + deps = [ + "@score_baselibs//score/mw/log", + ], +) + +cc_library( + name = "ivshmem_typed_memory_provider", + srcs = ["ivshmem_typed_memory_provider.cpp"], + hdrs = ["ivshmem_typed_memory_provider.h"], + features = COMPILER_WARNING_FEATURES + [ + "aborts_upon_exception", + ], + visibility = ["//score/mw/com/gateway/transport_layer:__subpackages__"], + deps = [ + "//score/memory/shared", + "//score/memory/shared/typedshm/typedshm_wrapper:typedmemory", + "@score_baselibs//score/mw/log", + "@score_baselibs//score/os:errno", + "@score_baselibs//score/os:mman", + ] + select({ + "@platforms//os:qnx": ["@score_baselibs//score/os/qnx:mman"], + "//conditions:default": [], + }), +) + +cc_library( + name = "ivshmem_typed_memory_provider_mock", + srcs = ["ivshmem_typed_memory_provider_mock.cpp"], + hdrs = ["ivshmem_typed_memory_provider_mock.h"], + features = COMPILER_WARNING_FEATURES, + visibility = ["//score/mw/com/gateway/transport_layer:__subpackages__"], + deps = [ + ":ivshmem_typed_memory_provider", + "@googletest//:gtest", + ], +) + +cc_unit_test( + name = "ivshmem_typed_memory_provider_test", + srcs = ["ivshmem_typed_memory_provider_test.cpp"], + features = COMPILER_WARNING_FEATURES, + deps = [ + ":ivshmem_typed_memory_provider", + "@googletest//:gtest", + "@score_baselibs//score/mw/log", + "@score_baselibs//score/os/mocklib:mman_mock", + ] + select({ + "@platforms//os:qnx": ["@score_baselibs//score/os/mocklib/qnx:mman_mock"], + "//conditions:default": [], + }), +) + +cc_unit_test( + name = "ivshmem_bar_discovery_test", + srcs = ["ivshmem_bar_discovery_test.cpp"], + features = COMPILER_WARNING_FEATURES, + deps = [ + ":ivshmem_bar_discovery", + "@score_baselibs//score/mw/log", + ], +) diff --git a/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery.cpp b/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery.cpp new file mode 100644 index 000000000..b99b748fb --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery.cpp @@ -0,0 +1,145 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery.h" + +#include "score/mw/log/logging.h" + +#include +#include +#include + +#if defined(__QNXNTO__) +extern "C" { +#include +} +#include // ThreadCtl, _NTO_TCTL_IO +#endif + +namespace score::mw::com::gateway::qemu::ivshmem +{ + +std::optional SelectIvshmemBar(const std::vector& bars, std::uint32_t required_bar_num) noexcept +{ + for (const auto& bar : bars) + { + if (bar.is_memory && bar.bar_num == required_bar_num) + { + return bar; + } + } + return std::nullopt; +} + +#if defined(__QNXNTO__) + +// COV_JUSTIFIED_START ivshmem-bar-discovery-hardware-path +bool DiscoverIvshmemBar(std::uint64_t& paddr, std::uint64_t& size, std::uint32_t required_bar_num) noexcept +{ + constexpr pci_vid_t kVendor = 0x1af4U; + constexpr pci_did_t kDevice = 0x1110U; + + if (::ThreadCtl(_NTO_TCTL_IO, nullptr) == -1) + { + ::score::mw::log::LogError() << "ThreadCtl(_NTO_TCTL_IO) failed: " << std::strerror(errno); + return false; + } + + const pci_bdf_t bdf = pci_device_find(0U, kVendor, kDevice, PCI_CCODE_ANY); + if (bdf == PCI_BDF_NONE) + { + ::score::mw::log::LogError() << "ivshmem PCI device " << static_cast(kVendor) << ":" + << static_cast(kDevice) << " not found"; + return false; + } + + pci_err_t err = PCI_ERR_OK; + const pci_devhdl_t hdl = pci_device_attach(bdf, pci_attachFlags_OWNER, &err); + if (hdl == nullptr || err != PCI_ERR_OK) + { + ::score::mw::log::LogError() << "pci_device_attach failed (err=" << static_cast(err) << ")"; + return false; + } + + constexpr pci_cmd_t kMemSpaceEnable = static_cast(1U << 1); + constexpr pci_cmd_t kBusMasterEnable = static_cast(1U << 2); + pci_cmd_t cmd = 0U; + err = pci_device_read_cmd(bdf, &cmd); + if (err != PCI_ERR_OK) + { + (void)pci_device_detach(hdl); + return false; + } + pci_cmd_t cmd_set = 0U; + err = pci_device_write_cmd(hdl, static_cast(cmd | kMemSpaceEnable | kBusMasterEnable), &cmd_set); + if (err != PCI_ERR_OK) + { + (void)pci_device_detach(hdl); + return false; + } + + pci_ba_t ba[7]; + int_t nba = static_cast(sizeof(ba) / sizeof(ba[0])); + err = pci_device_read_ba(hdl, &nba, ba, pci_reqType_e_UNSPECIFIED); + if (err != PCI_ERR_OK) + { + (void)pci_device_detach(hdl); + return false; + } + + std::vector bars; + bars.reserve(static_cast(nba)); + for (int_t i = 0; i < nba; ++i) + { + bars.push_back(IvshmemBar{static_cast(ba[i].bar_num), + static_cast(ba[i].addr), + static_cast(ba[i].size), + ba[i].type == pci_asType_e_MEM}); + } + for (const auto& bar : bars) + { + ::score::mw::log::LogInfo() << "ivshmem: BAR" << static_cast(bar.bar_num) + << " type=" << (bar.is_memory ? "MEM" : "IO") << " addr=0x" + << static_cast(bar.addr) << " size=0x" + << static_cast(bar.size); + } + + const auto shared = SelectIvshmemBar(bars, required_bar_num); + if (!shared.has_value()) + { + ::score::mw::log::LogError() << "required ivshmem BAR" << static_cast(required_bar_num) + << " not found on the ivshmem device"; + (void)pci_device_detach(hdl); + return false; + } + + ::score::mw::log::LogInfo() << "ivshmem: BAR" << static_cast(shared->bar_num) << " paddr=0x" + << static_cast(shared->addr) << " size=0x" + << static_cast(shared->size); + + paddr = static_cast(shared->addr); + size = static_cast(shared->size); + return true; +} +// COV_JUSTIFIED_STOP + +#else + +bool DiscoverIvshmemBar(std::uint64_t& /*paddr*/, std::uint64_t& /*size*/, std::uint32_t /*required_bar_num*/) noexcept +{ + ::score::mw::log::LogError() << "DiscoverIvshmemBar is only supported on QNX"; + return false; +} + +#endif + +} // namespace score::mw::com::gateway::qemu::ivshmem diff --git a/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery.h b/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery.h new file mode 100644 index 000000000..1caf837d3 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery.h @@ -0,0 +1,61 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_IVSHMEM_BAR_DISCOVERY_H +#define SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_IVSHMEM_BAR_DISCOVERY_H + +#include +#include +#include + +namespace score::mw::com::gateway::qemu::ivshmem +{ + +/// Descriptor for one PCI BAR returned by the selector helper. +struct IvshmemBar +{ + std::uint32_t bar_num; + std::uint64_t addr; + std::uint64_t size; + bool is_memory; +}; + +/// Default BAR number used by QEMU ivshmem-plain for the shared-memory region. +static constexpr std::uint32_t kDefaultIvshmemBarNum = 2U; + +/// Selects the ivshmem BAR that matches the required BAR number. +/// +/// The selector is intentionally strict: it returns the exact BAR that QEMU exposes for the +/// shared-memory region, rather than guessing by size. +std::optional SelectIvshmemBar(const std::vector& bars, + std::uint32_t required_bar_num = kDefaultIvshmemBarNum) noexcept; + +/// \brief Discovers the ivshmem shared-memory PCI device through pci-server. +/// +/// Looks up the QEMU ivshmem-plain PCI device (vendor 0x1af4 / device 0x1110), enables +/// Memory-Space and Bus-Master decoding, and returns the physical address and size of the +/// configured shared-memory BAR (BAR2 by default). +/// +/// The device remains attached for the lifetime of the process and is released by the OS on +/// exit. This function is supported only on QNX; on other operating systems it returns false. +/// +/// \param paddr Receives the physical base address of the discovered shared-memory BAR. +/// \param size Receives the size in bytes of the discovered shared-memory BAR. +/// \param required_bar_num The BAR number to select. +/// \return true if the ivshmem BAR was discovered successfully; otherwise false. +bool DiscoverIvshmemBar(std::uint64_t& paddr, + std::uint64_t& size, + std::uint32_t required_bar_num = kDefaultIvshmemBarNum) noexcept; + +} // namespace score::mw::com::gateway::qemu::ivshmem + +#endif // SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_IVSHMEM_BAR_DISCOVERY_H diff --git a/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery_test.cpp b/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery_test.cpp new file mode 100644 index 000000000..759b1965d --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery_test.cpp @@ -0,0 +1,92 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery.h" + +#include + +#include +#include + +namespace score::mw::com::gateway::qemu::ivshmem +{ + +namespace +{ + +TEST(IvshmemBarDiscoveryTest, SelectIvshmemBarReturnsRequiredBar) +{ + const std::vector bars{ + IvshmemBar{0U, 0x1000U, 0x2000U, true}, + IvshmemBar{2U, 0x3000U, 0x4000U, true}, + IvshmemBar{3U, 0x5000U, 0x8000U, true}, + }; + + const auto selected = SelectIvshmemBar(bars); + ASSERT_TRUE(selected.has_value()); + EXPECT_EQ(selected->bar_num, kDefaultIvshmemBarNum); + EXPECT_EQ(selected->addr, 0x3000U); + EXPECT_EQ(selected->size, 0x4000U); +} + +TEST(IvshmemBarDiscoveryTest, SelectIvshmemBarReturnsCustomRequiredBar) +{ + const std::vector bars{ + IvshmemBar{0U, 0x1000U, 0x2000U, true}, + IvshmemBar{2U, 0x3000U, 0x4000U, true}, + IvshmemBar{3U, 0x5000U, 0x8000U, true}, + }; + + const auto selected = SelectIvshmemBar(bars, 3U); + ASSERT_TRUE(selected.has_value()); + EXPECT_EQ(selected->bar_num, 3U); + EXPECT_EQ(selected->addr, 0x5000U); + EXPECT_EQ(selected->size, 0x8000U); +} + +TEST(IvshmemBarDiscoveryTest, SelectIvshmemBarReturnsNulloptWhenRequiredBarMissing) +{ + const std::vector bars{ + IvshmemBar{0U, 0x1000U, 0x2000U, true}, + IvshmemBar{3U, 0x5000U, 0x8000U, true}, + }; + + const auto selected = SelectIvshmemBar(bars); + EXPECT_FALSE(selected.has_value()); +} + +TEST(IvshmemBarDiscoveryTest, SelectIvshmemBarIgnoresIoBars) +{ + // Given bars where the BAR at kDefaultIvshmemBarNum exists but is I/O-space, not memory-space. + // An I/O BAR cannot be mmap'd for shared memory, so SelectIvshmemBar must reject it even + // though the bar_num matches. + const std::vector bars{ + IvshmemBar{kDefaultIvshmemBarNum, 0x1000U, 0x2000U, /*is_memory=*/false}, + }; + + const auto selected = SelectIvshmemBar(bars); + EXPECT_FALSE(selected.has_value()); +} + +TEST(IvshmemBarDiscoveryTest, DiscoverIvshmemBarReturnsFalseOnNonQnx) +{ + std::uint64_t paddr = 0U; + std::uint64_t size = 0U; + const bool result = DiscoverIvshmemBar(paddr, size); + EXPECT_FALSE(result); + EXPECT_EQ(paddr, 0U); + EXPECT_EQ(size, 0U); +} + +} // namespace + +} // namespace score::mw::com::gateway::qemu::ivshmem diff --git a/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider.cpp b/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider.cpp new file mode 100644 index 000000000..446b38cc2 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider.cpp @@ -0,0 +1,489 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider.h" +#include "score/mw/log/logging.h" + +#include "score/os/mman.h" + +#include +#include + +#include +#include +#include +#include +#include + +#if defined(__QNXNTO__) +#include +#if defined(__X86_64__) || defined(__x86_64__) +#include +#endif +#endif + +namespace score::mw::com::gateway::qemu::ivshmem +{ + +namespace +{ +#if defined(__QNXNTO__) +constexpr std::uint64_t kPageSize = 4096U; + +struct DirectoryHeader +{ + std::uint32_t lock_word; + std::uint32_t count; +}; + +std::uint64_t AlignUp(std::uint64_t value, std::uint64_t alignment) noexcept +{ + return (value + alignment - 1U) & ~(alignment - 1U); +} + +class DirectoryLockGuard +{ + public: + /// Maximum CAS retries before declaring the peer VM crashed and bailing out. + static constexpr unsigned int kMaxSpinRetries = 100000U; + + explicit DirectoryLockGuard(std::uint32_t* lock_word) noexcept : lock_word_{lock_word} + { + std::uint32_t expected = 0U; + for (unsigned int i = 0U; i < kMaxSpinRetries; ++i) + { + if (__atomic_compare_exchange_n(lock_word_, &expected, 1U, false, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) + { + acquired_ = true; + return; + } + expected = 0U; + std::this_thread::yield(); + } + ::score::mw::log::LogError() << "IvshmemTypedMemoryProvider: BAR directory lock spin-timeout (" + << kMaxSpinRetries + << " retries); peer VM may have crashed while holding the lock. " + "Directory operation will be skipped."; + } + + bool IsAcquired() const noexcept + { + return acquired_; + } + + DirectoryLockGuard(const DirectoryLockGuard&) = delete; + DirectoryLockGuard& operator=(const DirectoryLockGuard&) = delete; + + ~DirectoryLockGuard() noexcept + { + if (acquired_) + { + __atomic_store_n(lock_word_, 0U, __ATOMIC_RELEASE); + } + } + + private: + std::uint32_t* lock_word_; + bool acquired_{false}; +}; + +DirectoryHeader* AsDirectoryHeader(void* dir) noexcept +{ + return static_cast(dir); +} + +const DirectoryHeader* AsDirectoryHeader(const void* dir) noexcept +{ + return static_cast(dir); +} + +IvshmemTypedMemoryProvider::DirectoryEntry* GetDirectoryEntries(DirectoryHeader* header) noexcept +{ + return reinterpret_cast( + static_cast(static_cast(header)) + sizeof(DirectoryHeader)); +} + +const IvshmemTypedMemoryProvider::DirectoryEntry* GetDirectoryEntries(const DirectoryHeader* header) noexcept +{ + return reinterpret_cast( + static_cast(static_cast(header)) + sizeof(DirectoryHeader)); +} +#endif +} // namespace + +#if defined(__QNXNTO__) +static_assert(sizeof(DirectoryHeader) == IvshmemTypedMemoryProvider::kDirectoryHeaderSize, + "DirectoryHeader size mismatch with kDirectoryHeaderSize constant"); +#endif + +#if defined(__QNXNTO__) +IvshmemTypedMemoryProvider::IvshmemTypedMemoryProvider(std::uint64_t paddr, + std::uint64_t size, + std::unique_ptr mman_qnx) noexcept + : paddr_{paddr}, mman_qnx_{std::move(mman_qnx)}, usable_size_{size > kDirectorySize ? size - kDirectorySize : 0U} +{ +} +#else +IvshmemTypedMemoryProvider::IvshmemTypedMemoryProvider(std::uint64_t /*paddr*/, std::uint64_t /*size*/) noexcept {} +#endif + +std::uint32_t IvshmemTypedMemoryProvider::HashName(const std::string& name) noexcept +{ + // FNV-1a 32-bit + std::uint32_t hash = kFnvOffsetBasis; + for (const char c : name) + { + hash ^= static_cast(static_cast(c)); + hash *= kFnvPrime; + } + return hash; +} + +#if defined(__QNXNTO__) +void* IvshmemTypedMemoryProvider::MapDirectory() const noexcept +{ + std::lock_guard lock{directory_map_mutex_}; + if (directory_map_ != nullptr) + { + return directory_map_; + } + const std::uint64_t dir_paddr = paddr_ + usable_size_; + // MAP_PHYS|MAP_SHARED with fd=NOFD maps physical memory — equivalent to mmap_device_memory(). + const auto mmap_result = mman_qnx_->mmap(nullptr, + kDirectorySize, + static_cast(PROT_READ | PROT_WRITE), + static_cast(MAP_PHYS | MAP_SHARED), + static_cast(NOFD), + static_cast(dir_paddr)); + if (!mmap_result.has_value()) + { + return nullptr; + } + directory_map_ = mmap_result.value(); + return mmap_result.value(); +} + +std::optional LookupOffsetInDirectoryLocked(const std::string& shm_name, + const DirectoryHeader* header) noexcept +{ + const auto* entries = GetDirectoryEntries(header); + const std::uint32_t count = header->count; + const std::uint32_t hash = IvshmemTypedMemoryProvider::HashName(shm_name); + + for (std::uint32_t i = 0U; i < count && i < IvshmemTypedMemoryProvider::kMaxDirectoryEntries; ++i) + { + // Check hash first (fast path), then verify the stored name to avoid false positives + // from 32-bit FNV-1a hash collisions between different shm names. + if (entries[i].name_hash == hash && + std::strncmp(entries[i].name, shm_name.c_str(), IvshmemTypedMemoryProvider::kMaxNameLength) == 0) + { + return static_cast(entries[i].bar_offset); + } + } + return std::nullopt; +} +#endif + +#if defined(__QNXNTO__) +void IvshmemTypedMemoryProvider::WriteDirectoryEntry(const std::string& shm_name, + std::uint64_t offset, + std::uint32_t size) const noexcept +{ + // Caller must hold the shared directory lock. + void* dir = MapDirectory(); + if (dir == nullptr) + { + return; + } + + auto* header = AsDirectoryHeader(dir); + auto* entries = GetDirectoryEntries(header); + + const std::uint32_t count = header->count; + if (count >= kMaxDirectoryEntries) + { + ::score::mw::log::LogError() << "IvshmemTypedMemoryProvider: directory full (" << count << " entries)"; + return; + } + + const std::uint32_t hash = HashName(shm_name); + + // Idempotent: check if already recorded. Only reachable in a multi-VM concurrent + // write race; the allocations_ cache prevents re-entry within a single VM. + // COV_JUSTIFIED_START ivshmem-write-directory-entry-idempotent + for (std::uint32_t i = 0U; i < count; ++i) + { + if (entries[i].name_hash == hash && entries[i].bar_offset == offset) + { + return; + } + } + // COV_JUSTIFIED_STOP + + // Append new entry, then increment count with release semantics. + DirectoryEntry entry{}; + entry.name_hash = hash; + entry.alloc_size = size; + entry.bar_offset = offset; + if (shm_name.size() >= kMaxNameLength) + { + ::score::mw::log::LogWarn() << "IvshmemTypedMemoryProvider: shm name '" << shm_name + << "' exceeds kMaxNameLength=" << kMaxNameLength << ", storing truncated name"; + } + std::strncpy(entry.name, shm_name.c_str(), kMaxNameLength - 1U); + entry.name[kMaxNameLength - 1U] = '\0'; + + std::memcpy(&entries[count], &entry, sizeof(entry)); + std::atomic_thread_fence(std::memory_order_release); + header->count = count + 1U; +} + +std::uint64_t IvshmemTypedMemoryProvider::FindNextFreeOffsetInDirectory() const noexcept +{ + // Caller must hold the shared directory lock. + void* dir = MapDirectory(); + if (dir == nullptr) + { + return 0U; + } + + auto* header = AsDirectoryHeader(dir); + auto* entries = GetDirectoryEntries(header); + + // The DirectoryLockGuard acquire already provides acquire semantics; no additional fence needed. + const std::uint32_t count = header->count; + + // Find the end of the highest existing allocation (from either VM). + std::uint64_t highest_end = 0U; + for (std::uint32_t i = 0U; i < count && i < kMaxDirectoryEntries; ++i) + { + const std::uint64_t entry_end = entries[i].bar_offset + entries[i].alloc_size; + highest_end = std::max(highest_end, entry_end); + } + + return AlignUp(highest_end, kPageSize); +} +#endif + +std::optional IvshmemTypedMemoryProvider::LookupOffsetInDirectory( + const std::string& shm_name) const noexcept +{ +#if defined(__QNXNTO__) + void* dir = MapDirectory(); + if (dir == nullptr) + { + return std::nullopt; + } + + auto* header = AsDirectoryHeader(dir); + DirectoryLockGuard lock{&header->lock_word}; + if (!lock.IsAcquired()) + { + return std::nullopt; + } + // The acquire semantics of DirectoryLockGuard make a separate fence redundant. + return LookupOffsetInDirectoryLocked(shm_name, header); +#else + (void)shm_name; + return std::nullopt; +#endif +} + +score::cpp::expected_blank IvshmemTypedMemoryProvider::BindShmToBar( + [[maybe_unused]] std::size_t shm_size, + [[maybe_unused]] const std::string& shm_name, + [[maybe_unused]] std::uint64_t offset) const noexcept +{ +#if defined(__QNXNTO__) + const std::uint64_t alloc_size = AlignUp(static_cast(shm_size), kPageSize); + const std::uint64_t sub_paddr = paddr_ + offset; + + const auto shm_open_result = mman_qnx_->shm_open(shm_name.c_str(), O_RDWR | O_CREAT, mode_t{0666}); + if (!shm_open_result.has_value()) + { + return score::cpp::make_unexpected(shm_open_result.error()); + } + const std::int32_t fd = shm_open_result.value(); + + // Request Write-Back caching (ivshmem-plain is host-RAM backed, WB is correct). + // shm_ctl_special succeeds only on real ivshmem hardware with a WB-capable host; + // on x86_64 test VMs it always returns -1 (EBADF with a negative test fd). +#if defined(__X86_64__) || defined(__x86_64__) + // COV_JUSTIFIED_START ivshmem-bind-shm-to-bar-shm-ctl-special-success + const int rc_special = ::shm_ctl_special( + fd, SHMCTL_PHYS | SHMCTL_HAS_SPECIAL, sub_paddr, alloc_size, static_cast(X86_64_PTE_MTYPE_WB)); + if (rc_special != -1) + { + std::ignore = ::close(static_cast(fd)); + return {}; + } + // COV_JUSTIFIED_STOP +#endif + const auto shm_ctl_result = mman_qnx_->shm_ctl(fd, SHMCTL_PHYS, sub_paddr, alloc_size); + if (!shm_ctl_result.has_value()) + { + const score::os::Error saved_error = shm_ctl_result.error(); + std::ignore = ::close(static_cast(fd)); + score::cpp::ignore = this->Unlink(shm_name); + return score::cpp::make_unexpected(saved_error); + } + + std::ignore = ::close(static_cast(fd)); + return {}; +#else + return score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOSYS)); +#endif +} + +score::cpp::expected_blank IvshmemTypedMemoryProvider::AllocateNamedTypedMemory( + [[maybe_unused]] std::size_t shm_size, + [[maybe_unused]] std::string shm_name, + const score::memory::shared::permission::UserPermissions& /*permissions*/) const noexcept +{ +#if defined(__QNXNTO__) + std::lock_guard lock{mutex_}; + + // Check local cache first — if already allocated by this process, reuse + // without touching the shared BAR directory (avoids unnecessary mmap). + auto it = allocations_.find(shm_name); + if (it != allocations_.end()) + { + return BindShmToBar(shm_size, shm_name, it->second); + } + + const std::uint64_t alloc_size = AlignUp(static_cast(shm_size), kPageSize); + + void* dir = MapDirectory(); + if (dir == nullptr) + { + ::score::mw::log::LogError() << "IvshmemTypedMemoryProvider: failed to map BAR directory; " + "cannot allocate named typed memory without the shared directory"; + return score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOMEM)); + } + + auto* header = AsDirectoryHeader(dir); + DirectoryLockGuard directory_lock{&header->lock_word}; + if (!directory_lock.IsAcquired()) + { + return score::cpp::make_unexpected(score::os::Error::createFromErrno(EBUSY)); + } + + // Check if already in the BAR directory (e.g. allocated by the other VM, or by a + // previous run of this process). Reuse the same offset. + auto dir_offset = LookupOffsetInDirectoryLocked(shm_name, header); + if (dir_offset.has_value()) + { + allocations_[shm_name] = dir_offset.value(); + return BindShmToBar(shm_size, shm_name, dir_offset.value()); + } + + // New allocation: scan ALL directory entries (from both VMs) to find the next free + // offset after the highest existing allocation. + const std::uint64_t offset = FindNextFreeOffsetInDirectory(); + + if (offset + alloc_size > usable_size_) + { + ::score::mw::log::LogError() << "IvshmemTypedMemoryProvider: BAR exhausted (need " + << static_cast(alloc_size) << " at offset " + << static_cast(offset) << ", usable " + << static_cast(usable_size_) << ")"; + return score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOMEM)); + } + + // Write to the BAR-resident directory so the other VM can discover this allocation. + WriteDirectoryEntry(shm_name, offset, static_cast(alloc_size)); + allocations_[shm_name] = offset; + + return BindShmToBar(shm_size, shm_name, offset); +#else + return score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOSYS)); +#endif +} + +score::cpp::expected_blank IvshmemTypedMemoryProvider::AllocateNamedTypedMemoryAtOffset( + [[maybe_unused]] std::size_t shm_size, + [[maybe_unused]] const std::string& shm_name, + [[maybe_unused]] std::uint64_t bar_offset, + const score::memory::shared::permission::UserPermissions& /*permissions*/) const noexcept +{ +#if defined(__QNXNTO__) + std::lock_guard lock{mutex_}; + + const std::uint64_t alloc_size = AlignUp(static_cast(shm_size), kPageSize); + if (bar_offset + alloc_size > usable_size_) + { + ::score::mw::log::LogError() << "IvshmemTypedMemoryProvider::AtOffset: offset " + << static_cast(bar_offset) << " + size " + << static_cast(alloc_size) << " exceeds usable BAR (" + << static_cast(usable_size_) << ")"; + return score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOMEM)); + } + + // Record locally (or verify consistency if already known). + auto it = allocations_.find(shm_name); + if (it != allocations_.end()) + { + if (it->second != bar_offset) + { + ::score::mw::log::LogError() << "IvshmemTypedMemoryProvider::AtOffset: name '" << shm_name + << "' already at offset " << static_cast(it->second) + << ", requested " << static_cast(bar_offset); + return score::cpp::make_unexpected(score::os::Error::createFromErrno(EEXIST)); + } + } + else + { + allocations_[shm_name] = bar_offset; + } + + return BindShmToBar(shm_size, shm_name, bar_offset); +#else + return score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOSYS)); +#endif +} + +std::optional IvshmemTypedMemoryProvider::GetAllocationOffset(const std::string& shm_name) const noexcept +{ + std::lock_guard lock{mutex_}; + auto it = allocations_.find(shm_name); + if (it != allocations_.end()) + { + return it->second; + } + return std::nullopt; +} + +score::cpp::expected IvshmemTypedMemoryProvider::AllocateAndOpenAnonymousTypedMemory( + std::uint64_t /*shm_size*/) const noexcept +{ + return score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOSYS)); +} + +score::cpp::expected_blank IvshmemTypedMemoryProvider::Unlink( + std::string_view shm_name) const noexcept +{ + const std::string name{shm_name}; + const auto result = score::os::Mman::instance().shm_unlink(name.c_str()); + if (!result.has_value()) + { + return score::cpp::make_unexpected(result.error()); + } + return {}; +} + +score::cpp::expected IvshmemTypedMemoryProvider::GetCreatorUid( + std::string_view /*shm_name*/) const noexcept +{ + return ::getuid(); +} + +} // namespace score::mw::com::gateway::qemu::ivshmem diff --git a/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider.h b/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider.h new file mode 100644 index 000000000..5dbd59350 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider.h @@ -0,0 +1,267 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_IVSHMEM_TYPED_MEMORY_PROVIDER_H +#define SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_IVSHMEM_TYPED_MEMORY_PROVIDER_H + +#include "score/memory/shared/typedshm/typedshm_wrapper/typed_memory.h" + +#include + +#if defined(__QNXNTO__) +#include "score/os/qnx/mman.h" +#include "score/os/qnx/mman_impl.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace score::mw::com::gateway::qemu::ivshmem +{ + +/// @brief A TypedMemory provider that backs named shm objects with sub-ranges of the ivshmem BAR. +/// +/// This provider supports multiple shm objects (DATA + CTRL for multiple services) by using a +/// BAR-resident allocation directory that is visible to both VMs via the shared physical BAR memory. +/// +/// @details +/// **BAR Layout** (managed by the provider): +/// - [0 .. usable_end): shm data regions (allocated by either VM) +/// - [usable_end .. usable_end + kDirectorySize): allocation directory (shared, both VMs) +/// +/// Both VMs share a single allocation space with no partitioning. A shared lock in the +/// directory serializes cross-VM access so the directory acts as a shared allocator: +/// - **Source side** (AllocateNamedTypedMemory): scans all directory entries to find the +/// next free offset (after the highest existing allocation), writes a new entry, and +/// binds the QNX shm object to that BAR sub-range. +/// - **Destination side** (AllocateNamedTypedMemoryAtOffset): binds a QNX shm object at a +/// specific offset (looked up from the directory by the transport layer). +/// +/// This design supports any number of providers and consumers on both VMs simultaneously, +/// with services being created and consumed in any order. +class IvshmemTypedMemoryProvider : public score::memory::shared::TypedMemory +{ + public: + /// @brief Constructs an IvshmemTypedMemoryProvider. + /// + /// @param paddr Physical base address of the ivshmem BAR (from DiscoverIvshmemBar). + /// @param size Total usable BAR size in bytes (excluding any handshake region managed outside). + /// @param mman_qnx QNX mman abstraction (only on QNX platforms). +#if defined(__QNXNTO__) + IvshmemTypedMemoryProvider( + std::uint64_t paddr, + std::uint64_t size, + std::unique_ptr mman_qnx = std::make_unique()) noexcept; +#else + IvshmemTypedMemoryProvider(std::uint64_t paddr, std::uint64_t size) noexcept; +#endif + ~IvshmemTypedMemoryProvider() override = default; + + /// @brief Allocates a page-aligned sub-region from the BAR. + /// + /// Scans the BAR-resident directory (visible to both VMs) to find the next free offset, + /// writes a new directory entry, and binds the QNX shm to that sub-range. + /// + /// @param shm_size Size of the shared memory region to allocate. + /// @param shm_name Name of the shared memory object. + /// @param permissions User permissions for the shared memory. + /// + /// @return Expected value containing no error on success, or a score::os::Error on failure. + /// + score::cpp::expected_blank AllocateNamedTypedMemory( + std::size_t shm_size, + std::string shm_name, + const score::memory::shared::permission::UserPermissions& permissions) const noexcept override; + + /// @brief Binds a named shm at a specific BAR offset. + /// + /// The offset is looked up from the directory by the transport layer (LookupOffsetInDirectory). + /// + /// @param shm_size Size of the shared memory region to allocate. + /// @param shm_name Name of the shared memory object. + /// @param bar_offset Offset within the BAR where the shm should be bound. + /// @param permissions User permissions for the shared memory. + /// + /// @return Expected value containing no error on success, or a score::os::Error on failure. + /// + virtual score::cpp::expected_blank AllocateNamedTypedMemoryAtOffset( + std::size_t shm_size, + const std::string& shm_name, + std::uint64_t bar_offset, + const score::memory::shared::permission::UserPermissions& permissions) const noexcept; + + /// @brief Returns the BAR offset for a previously allocated name (local process lookup). + /// + /// @param shm_name Name of the shared memory object. + /// + /// @return The BAR offset if found, or std::nullopt if not found. + /// + std::optional GetAllocationOffset(const std::string& shm_name) const noexcept; + + /// @brief Looks up a shm name in the BAR-resident directory (cross-VM lookup). + /// + /// Reads from shared physical memory so it works even if the name was allocated by the + /// other VM. Returns the offset if found. + /// + /// @param shm_name Name of the shared memory object to look up. + /// + /// @return The BAR offset if found in the directory, or std::nullopt if not found. + /// + virtual std::optional LookupOffsetInDirectory(const std::string& shm_name) const noexcept; + + /// @brief Allocates and opens an anonymous typed shared memory region. + /// + /// @param shm_size Size of the shared memory region in bytes. + /// + /// @return Expected value containing the file descriptor on success, or a score::os::Error on failure. + score::cpp::expected AllocateAndOpenAnonymousTypedMemory( + std::uint64_t shm_size) const noexcept override; + + /// @brief Unlinks a shared memory object. + /// + /// @param shm_name Name of the shared memory object to unlink. + /// + /// @return Expected value containing no error on success, or a score::os::Error on failure. + score::cpp::expected_blank Unlink(std::string_view shm_name) const noexcept override; + + /// @brief Gets the user ID (UID) of the creator of a shared memory object. + /// + /// @param shm_name Name of the shared memory object. + /// + /// @return Expected value containing the UID on success, or a score::os::Error on failure. + score::cpp::expected GetCreatorUid(std::string_view shm_name) const noexcept override; + +#if defined(__QNXNTO__) + /// @brief Returns the MmanQnx abstraction used by this provider (QNX only). + /// + /// Allows callers (e.g. the transport layer) to reuse the same abstraction + /// instance for shm_open operations without creating a separate instance. + /// + /// @return Raw pointer to the MmanQnx instance. Lifetime is tied to this provider. + score::os::qnx::MmanQnx* GetMmanQnx() const noexcept + { + return mman_qnx_.get(); + } +#endif + + /// @brief Maximum length (including null terminator) of a shm name stored in a directory entry. + /// + /// Covers paths like /intervm-shared-shmem///ctrl (typically ≤ 55 chars). + /// Names exceeding this limit are truncated and a warning is logged; the 32-bit FNV-1a + /// hash is then the sole disambiguator for those entries. + static constexpr std::uint32_t kMaxNameLength = 56U; + + /// @brief Directory entry stored in the BAR. + /// + /// Visible to both VMs via the shared physical memory. Used by the allocation directory + /// to track allocated regions. The @c name field provides a secondary collision check + /// on top of the 32-bit FNV-1a @c name_hash so that two different shm names that happen + /// to produce the same hash value are not confused. + struct DirectoryEntry + { + std::uint32_t name_hash; ///< FNV-1a hash of the shm name (fast first filter) + std::uint32_t alloc_size; ///< page-aligned allocation size in bytes + std::uint64_t bar_offset; ///< offset within the usable BAR region + char name[kMaxNameLength]; ///< null-terminated shm name (truncated if > kMaxNameLength-1) + }; + static_assert(sizeof(DirectoryEntry) == 72U, "DirectoryEntry layout must be 72 bytes for BAR wire format"); + + /// @brief Size of the directory region reserved at the end of the usable BAR. + static constexpr std::uint64_t kDirectorySize = 4096U; + + /// @brief Size of the directory header (cross-VM lock word + entry count, two uint32_t fields). + static constexpr std::uint64_t kDirectoryHeaderSize = sizeof(std::uint32_t) * 2U; + + /// @brief Maximum directory entries (header + entries fit in one page). + static constexpr std::uint32_t kMaxDirectoryEntries = + (kDirectorySize - kDirectoryHeaderSize) / sizeof(DirectoryEntry); + + /// FNV-1a 32-bit hash algorithm offset basis + static constexpr std::uint32_t kFnvOffsetBasis = 2166136261U; + + /// FNV-1a 32-bit hash algorithm prime + static constexpr std::uint32_t kFnvPrime = 16777619U; + + /// @brief Computes FNV-1a 32-bit hash for a string. + /// + /// @param name String to hash. + /// + /// @return The 32-bit FNV-1a hash value. + static std::uint32_t HashName(const std::string& name) noexcept; + + protected: + /// @brief Creates the QNX shm object bound to BAR at (paddr_ + offset). + /// + /// @param shm_size Size of the shared memory region in bytes. + /// @param shm_name Name of the shared memory object. + /// @param offset Offset within the BAR where the shm should be bound. + /// + /// @return Expected value containing no error on success, or a score::os::Error on failure. + /// + score::cpp::expected_blank BindShmToBar(std::size_t shm_size, + const std::string& shm_name, + std::uint64_t offset) const noexcept; + + /// @brief Inserts or overwrites an entry in the local per-process allocation cache. + /// + /// Provided for white-box test subclasses that need to pre-populate the cache without + /// going through the full allocation path. Must not be called from production code. + void SetAllocationInCache(const std::string& name, std::uint64_t offset) const noexcept + { + std::lock_guard lock{mutex_}; + allocations_[name] = offset; + } + + private: + /// Local cache: shm name → BAR offset. Protected by mutex_. + mutable std::unordered_map allocations_; +#if defined(__QNXNTO__) + /// @brief Writes an entry to the BAR-resident directory. + /// + /// @param shm_name Name of the shared memory object. + /// @param offset Offset within the BAR where the shm is bound. + /// @param size Size of the shared memory allocation. + void WriteDirectoryEntry(const std::string& shm_name, std::uint64_t offset, std::uint32_t size) const noexcept; + + /// @brief Scans all directory entries (from both VMs) and returns the offset just past the highest existing + /// allocation. + /// + /// This is the next free offset for a new allocation. + /// + /// @return The next available offset for a new allocation. + /// + std::uint64_t FindNextFreeOffsetInDirectory() const noexcept; + + /// @brief Maps the directory region of the BAR into this process's address space (cached). + /// + /// @return Pointer to the mapped directory region, or nullptr on failure. + void* MapDirectory() const noexcept; + + mutable void* directory_map_{nullptr}; ///< cached mmap of the directory region + mutable std::mutex directory_map_mutex_; ///< guards the lazy mmap initialisation of directory_map_ + std::unique_ptr mman_qnx_; + std::uint64_t paddr_; ///< Physical base address of the ivshmem BAR + std::uint64_t usable_size_; ///< size - kDirectorySize (space for shm allocations) +#endif + + mutable std::mutex mutex_; ///< protects the local per-process allocation cache +}; + +} // namespace score::mw::com::gateway::qemu::ivshmem + +#endif // SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_IVSHMEM_TYPED_MEMORY_PROVIDER_H diff --git a/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider_mock.cpp b/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider_mock.cpp new file mode 100644 index 000000000..0f639ceae --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider_mock.cpp @@ -0,0 +1,14 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider_mock.h" diff --git a/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider_mock.h b/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider_mock.h new file mode 100644 index 000000000..7956e0151 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider_mock.h @@ -0,0 +1,62 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_IVSHMEM_TYPED_MEMORY_PROVIDER_MOCK_H +#define SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_IVSHMEM_TYPED_MEMORY_PROVIDER_MOCK_H + +#include "score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider.h" + +#include + +namespace score::mw::com::gateway::qemu::ivshmem +{ + +class IvshmemTypedMemoryProviderMock : public IvshmemTypedMemoryProvider +{ + public: + IvshmemTypedMemoryProviderMock() noexcept : IvshmemTypedMemoryProvider{0U, 0U} {} + + MOCK_METHOD((score::cpp::expected_blank), + AllocateNamedTypedMemory, + (std::size_t, std::string, const score::memory::shared::permission::UserPermissions&), + (const, noexcept, override)); + + MOCK_METHOD( + (score::cpp::expected_blank), + AllocateNamedTypedMemoryAtOffset, + (std::size_t, const std::string&, std::uint64_t, const score::memory::shared::permission::UserPermissions&), + (const, noexcept, override)); + + MOCK_METHOD((std::optional), + LookupOffsetInDirectory, + (const std::string&), + (const, noexcept, override)); + + MOCK_METHOD((score::cpp::expected), + AllocateAndOpenAnonymousTypedMemory, + (std::uint64_t), + (const, noexcept, override)); + + MOCK_METHOD((score::cpp::expected_blank), + Unlink, + (std::string_view), + (const, noexcept, override)); + + MOCK_METHOD((score::cpp::expected), + GetCreatorUid, + (std::string_view), + (const, noexcept, override)); +}; + +} // namespace score::mw::com::gateway::qemu::ivshmem + +#endif // SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_IVSHMEM_TYPED_MEMORY_PROVIDER_MOCK_H diff --git a/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider_test.cpp b/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider_test.cpp new file mode 100644 index 000000000..a917dd0ba --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider_test.cpp @@ -0,0 +1,840 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider.h" + +#include "score/memory/shared/user_permission.h" +#include "score/os/mocklib/mman_mock.h" + +#if defined(__QNXNTO__) +#include "score/os/mocklib/qnx/mock_mman.h" +#endif + +#include +#include + +#include +#include + +namespace score::mw::com::gateway::qemu::ivshmem +{ + +namespace +{ + +/// Test subclass that exposes protected members for white-box testing on non-QNX. +class TestableIvshmemTypedMemoryProvider : public IvshmemTypedMemoryProvider +{ + public: + TestableIvshmemTypedMemoryProvider(std::uint64_t paddr, std::uint64_t size) noexcept + : IvshmemTypedMemoryProvider{paddr, size} + { + } + +#if defined(__QNXNTO__) + /// Injection constructor — allows passing a MmanQnx mock for QNX tests. + TestableIvshmemTypedMemoryProvider(std::uint64_t paddr, + std::uint64_t size, + std::unique_ptr mman_qnx) noexcept + : IvshmemTypedMemoryProvider{paddr, size, std::move(mman_qnx)} + { + } +#endif + + /// Bring BindShmToBar into public scope so tests can invoke it directly. + using IvshmemTypedMemoryProvider::BindShmToBar; + + /// Populate the local allocation cache, enabling tests for GetAllocationOffset's found-branch. + void InsertAllocation(const std::string& name, std::uint64_t offset) noexcept + { + SetAllocationInCache(name, offset); + } +}; + +class IvshmemTypedMemoryProviderTest : public ::testing::Test +{ + protected: + static constexpr std::uint64_t kBarPaddr = 0x100000U; + static constexpr std::uint64_t kBarSize = 1024U * 1024U; // 1 MiB + + IvshmemTypedMemoryProvider provider_{kBarPaddr, kBarSize}; +}; + +TEST_F(IvshmemTypedMemoryProviderTest, ConstructorWithNormalSizeCalculatesUsableSize) +{ + // When constructing with normal size + IvshmemTypedMemoryProvider p{kBarPaddr, kBarSize}; + + // Then construction succeeds without issues + (void)p; +} + +TEST_F(IvshmemTypedMemoryProviderTest, ConstructorWithSizeSmallerThanDirectorySetsUsableSizeToZero) +{ + // When BAR size is smaller than directory size + IvshmemTypedMemoryProvider p{kBarPaddr, 100U}; + + // Then construction succeeds with usable_size_ = 0 + (void)p; +} + +TEST_F(IvshmemTypedMemoryProviderTest, ConstructorWithSizeEqualToDirectorySetsUsableSizeToZero) +{ + // When constructing with size equal to directory size + IvshmemTypedMemoryProvider p{kBarPaddr, IvshmemTypedMemoryProvider::kDirectorySize}; + + // Then construction succeeds with usable_size_ = 0 + (void)p; +} + +TEST_F(IvshmemTypedMemoryProviderTest, ConstructorWithZeroSizeSetsUsableSizeToZero) +{ + // When constructing with zero size + IvshmemTypedMemoryProvider p{kBarPaddr, 0U}; + + // Then construction succeeds with usable_size_ = 0 + (void)p; +} + +TEST(IvshmemTypedMemoryProviderHashNameTest, EmptyStringReturnsInitialSeed) +{ + // When hashing an empty string + // Then FNV-1a returns the initial seed value + EXPECT_EQ(IvshmemTypedMemoryProvider::HashName(""), 2166136261U); +} + +TEST(IvshmemTypedMemoryProviderHashNameTest, SingleCharacterProducesExpectedHash) +{ + // When hashing a single character + const std::uint32_t expected = (2166136261U ^ 65U) * 16777619U; + + // Then FNV-1a produces expected hash + EXPECT_EQ(IvshmemTypedMemoryProvider::HashName("A"), expected); +} + +TEST(IvshmemTypedMemoryProviderHashNameTest, SameInputAlwaysProducesSameHash) +{ + // When hashing the same input twice + // Then both calls produce identical hashes + EXPECT_EQ(IvshmemTypedMemoryProvider::HashName("/my_shm"), IvshmemTypedMemoryProvider::HashName("/my_shm")); +} + +TEST(IvshmemTypedMemoryProviderHashNameTest, DifferentInputsProduceDifferentHashes) +{ + // When hashing different inputs + // Then hashes are different + EXPECT_NE(IvshmemTypedMemoryProvider::HashName("/shm_a"), IvshmemTypedMemoryProvider::HashName("/shm_b")); +} + +// On QNX the function actually executes and may succeed; this test only checks the Linux #else stub. +#if !defined(__QNXNTO__) + +TEST_F(IvshmemTypedMemoryProviderTest, AllocateNamedTypedMemoryReturnsEnosysOnNonQnx) +{ + // When calling AllocateNamedTypedMemory on non-QNX + const auto result = + provider_.AllocateNamedTypedMemory(4096U, "/test_shm", score::memory::shared::permission::WorldWritable{}); + + // Then the stub returns error + EXPECT_FALSE(result.has_value()); +} + +TEST_F(IvshmemTypedMemoryProviderTest, AllocateNamedTypedMemoryAtOffsetReturnsEnosysOnNonQnx) +{ + // When calling AllocateNamedTypedMemoryAtOffset on non-QNX + const auto result = provider_.AllocateNamedTypedMemoryAtOffset( + 4096U, "/test_shm", 0U, score::memory::shared::permission::WorldWritable{}); + + // Then the stub returns error + EXPECT_FALSE(result.has_value()); +} + +TEST(IvshmemTypedMemoryProviderBindShmToBarTest, ReturnsEnosysOnNonQnx) +{ + // Given a testable provider + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U}; + + // When calling BindShmToBar on non-QNX + const auto result = p.BindShmToBar(4096U, "/test_shm", 0U); + + // Then the stub returns error + ASSERT_FALSE(result.has_value()); +} + +#endif // !defined(__QNXNTO__) + +// Compiled only on QNX where mman_qnx_ is active and MmanQnxMock is available. +#if defined(__QNXNTO__) + +TEST(IvshmemTypedMemoryProviderBindShmToBarTest, ReturnsErrorWhenShmOpenFails) +{ + // Given mock that fails on shm_open + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U, std::move(mman_mock)}; + + // Expecting shm_open to be called and return error + EXPECT_CALL(*raw, shm_open(::testing::_, ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOMEM)))); + + // When calling BindShmToBar + const auto result = p.BindShmToBar(4096U, "/test_shm", 0U); + + // Then the operation fails + EXPECT_FALSE(result.has_value()); +} + +TEST(IvshmemTypedMemoryProviderBindShmToBarTest, ReturnsErrorWhenShmCtlFails) +{ + // Given mock where shm_open succeeds but shm_ctl fails + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U, std::move(mman_mock)}; + + // Expecting shm_open to succeed and shm_ctl to fail + EXPECT_CALL(*raw, shm_open(::testing::_, ::testing::_, ::testing::_)).WillOnce(::testing::Return(std::int32_t{-2})); + EXPECT_CALL(*raw, shm_ctl(std::int32_t{-2}, ::testing::_, ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(EINVAL)))); + + // When calling BindShmToBar + const auto result = p.BindShmToBar(4096U, "/test_shm", 0U); + + // Then the operation fails + EXPECT_FALSE(result.has_value()); +} + +#endif // defined(__QNXNTO__) + +TEST_F(IvshmemTypedMemoryProviderTest, AllocateAndOpenAnonymousTypedMemoryReturnsEnosys) +{ + // When calling AllocateAndOpenAnonymousTypedMemory + const auto result = provider_.AllocateAndOpenAnonymousTypedMemory(4096U); + + // Then the stub returns error + EXPECT_FALSE(result.has_value()); +} + +TEST_F(IvshmemTypedMemoryProviderTest, UnlinkSucceedsWhenShmUnlinkSucceeds) +{ + // Given mock where shm_unlink succeeds + score::os::MockGuard mman_mock{}; + EXPECT_CALL(*mman_mock, shm_unlink(::testing::StrEq("/test_shm"))).WillOnce(::testing::Return(score::cpp::blank{})); + + // When calling Unlink + const auto result = provider_.Unlink("/test_shm"); + + // Then the operation succeeds + EXPECT_TRUE(result.has_value()); +} + +TEST_F(IvshmemTypedMemoryProviderTest, UnlinkReturnsErrorWhenShmUnlinkFails) +{ + // Given mock where shm_unlink fails + score::os::MockGuard mman_mock{}; + EXPECT_CALL(*mman_mock, shm_unlink(::testing::StrEq("/test_shm"))) + .WillOnce(::testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOENT)))); + + // When calling Unlink + const auto result = provider_.Unlink("/test_shm"); + + // Then the operation fails + EXPECT_FALSE(result.has_value()); +} + +TEST_F(IvshmemTypedMemoryProviderTest, GetCreatorUidReturnsCurrentUid) +{ + // When calling GetCreatorUid + const auto result = provider_.GetCreatorUid("/test_shm"); + + // Then the current UID is returned + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), ::getuid()); +} + +TEST_F(IvshmemTypedMemoryProviderTest, GetAllocationOffsetReturnsNulloptForUnknownName) +{ + // When calling GetAllocationOffset with unknown name + const auto result = provider_.GetAllocationOffset("/unknown"); + + // Then nullopt is returned + EXPECT_FALSE(result.has_value()); +} + +TEST(IvshmemTypedMemoryProviderGetAllocationOffsetTest, ReturnsOffsetWhenAllocationExists) +{ + // Given a provider with an allocation in the cache + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U}; + p.InsertAllocation("/my_shm", 8192U); + + // When calling GetAllocationOffset + const auto result = p.GetAllocationOffset("/my_shm"); + + // Then the cached offset is returned + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), 8192U); +} + +TEST(IvshmemTypedMemoryProviderGetAllocationOffsetTest, ReturnsNulloptForUninsertedName) +{ + // Given a provider with a different allocation cached + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U}; + p.InsertAllocation("/other_shm", 4096U); + + // When calling GetAllocationOffset with a name not in cache + // Then nullopt is returned + EXPECT_FALSE(p.GetAllocationOffset("/my_shm").has_value()); +} + +// On QNX the function actually executes MapDirectory() against physical memory; only test on Linux. +#if !defined(__QNXNTO__) + +TEST_F(IvshmemTypedMemoryProviderTest, LookupOffsetInDirectoryReturnsNulloptOnNonQnx) +{ + // When calling LookupOffsetInDirectory on non-QNX + const auto result = provider_.LookupOffsetInDirectory("/test_shm"); + + // Then the stub returns nullopt + EXPECT_FALSE(result.has_value()); +} + +#endif // !defined(__QNXNTO__) + +#if defined(__QNXNTO__) + +// Helper: build a zero-initialised directory buffer and optionally fill N entries. +// Each synthetic entry uses a unique non-colliding hash and sequential 4 KiB offsets. +static void FillDirectoryEntries(std::uint8_t* buf, std::uint32_t count) noexcept +{ + *reinterpret_cast(buf) = 0U; + *reinterpret_cast(buf + sizeof(std::uint32_t)) = count; + auto* entries = reinterpret_cast( + buf + IvshmemTypedMemoryProvider::kDirectoryHeaderSize); + for (std::uint32_t i = 0U; i < count; ++i) + { + entries[i].name_hash = 0xDEAD0000U + i; // chosen to not collide with any test name + entries[i].bar_offset = static_cast(i) * 4096U; + entries[i].alloc_size = 4096U; + } +} + +TEST(IvshmemQnxMapDirectoryTest, MmapFailureReturnsNulloptFromLookup) +{ + // Given mock where mmap fails + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U, std::move(mman_mock)}; + + // Expecting mmap to fail + EXPECT_CALL(*raw, + mmap(nullptr, + IvshmemTypedMemoryProvider::kDirectorySize, + ::testing::_, + ::testing::_, + ::testing::_, + ::testing::_)) + .WillOnce(::testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOMEM)))); + + // When calling LookupOffsetInDirectory + // Then nullopt is returned + EXPECT_FALSE(p.LookupOffsetInDirectory("/test_shm").has_value()); +} + +TEST(IvshmemQnxMapDirectoryTest, DirectoryMapIsCachedAfterFirstCall) +{ + // Given a provider with a mock that maps directory to buffer + std::array buf{}; + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U, std::move(mman_mock)}; + + // Expecting mmap to be called exactly once + EXPECT_CALL(*raw, + mmap(nullptr, + IvshmemTypedMemoryProvider::kDirectorySize, + ::testing::_, + ::testing::_, + ::testing::_, + ::testing::_)) + .WillOnce(::testing::Return(static_cast(buf.data()))); + + // When calling LookupOffsetInDirectory twice + EXPECT_FALSE(p.LookupOffsetInDirectory("/first").has_value()); + EXPECT_FALSE(p.LookupOffsetInDirectory("/second").has_value()); + + // Then mmap is called only once due to caching +} + +TEST(IvshmemQnxLookupOffsetTest, ReturnsOffsetWhenEntryFoundInDirectory) +{ + // Given a directory buffer with matching entry + std::array buf{}; + const std::uint32_t target_hash = IvshmemTypedMemoryProvider::HashName("/shm_target"); + *reinterpret_cast(buf.data() + sizeof(std::uint32_t)) = 1U; + auto* entry = reinterpret_cast( + buf.data() + IvshmemTypedMemoryProvider::kDirectoryHeaderSize); + entry->name_hash = target_hash; + entry->bar_offset = 8192U; + entry->alloc_size = 4096U; + std::strncpy(entry->name, "/shm_target", IvshmemTypedMemoryProvider::kMaxNameLength - 1U); + + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U, std::move(mman_mock)}; + + EXPECT_CALL(*raw, + mmap(nullptr, + IvshmemTypedMemoryProvider::kDirectorySize, + ::testing::_, + ::testing::_, + ::testing::_, + ::testing::_)) + .WillOnce(::testing::Return(static_cast(buf.data()))); + + // When looking up an entry that exists + const auto result = p.LookupOffsetInDirectory("/shm_target"); + + // Then the offset is returned + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), 8192U); +} + +TEST(IvshmemQnxLookupOffsetTest, ReturnsNulloptWhenEntryNotFoundInDirectory) +{ + // Given a directory buffer with a different entry + std::array buf{}; + *reinterpret_cast(buf.data() + sizeof(std::uint32_t)) = 1U; + auto* entry = reinterpret_cast( + buf.data() + IvshmemTypedMemoryProvider::kDirectoryHeaderSize); + entry->name_hash = IvshmemTypedMemoryProvider::HashName("/other_shm"); + entry->bar_offset = 4096U; + entry->alloc_size = 4096U; + + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U, std::move(mman_mock)}; + + EXPECT_CALL(*raw, + mmap(nullptr, + IvshmemTypedMemoryProvider::kDirectorySize, + ::testing::_, + ::testing::_, + ::testing::_, + ::testing::_)) + .WillOnce(::testing::Return(static_cast(buf.data()))); + + // When looking up an entry that doesn't exist + // Then nullopt is returned + EXPECT_FALSE(p.LookupOffsetInDirectory("/test_shm").has_value()); +} + +TEST(IvshmemTypedMemoryProviderBindShmToBarTest, SucceedsWhenShmOpenAndShmCtlSucceed) +{ + // Given mock where shm_open and shm_ctl succeed + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U, std::move(mman_mock)}; + + // Expecting shm_open and shm_ctl calls to succeed + EXPECT_CALL(*raw, shm_open(::testing::_, ::testing::_, ::testing::_)).WillOnce(::testing::Return(std::int32_t{-2})); + EXPECT_CALL(*raw, shm_ctl(std::int32_t{-2}, ::testing::_, ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(std::int32_t{0})); + + // When calling BindShmToBar + const auto result = p.BindShmToBar(4096U, "/test_shm", 0U); + + // Then the operation succeeds + EXPECT_TRUE(result.has_value()); +} + +TEST(IvshmemQnxAllocateNamedTest, LocalCacheHitBindsShm) +{ + // Given a provider with allocation already in local cache + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U, std::move(mman_mock)}; + p.InsertAllocation("/test_shm", 4096U); + + // Expecting shm_open and shm_ctl to be called + EXPECT_CALL(*raw, shm_open(::testing::_, ::testing::_, ::testing::_)).WillOnce(::testing::Return(std::int32_t{-2})); + EXPECT_CALL(*raw, shm_ctl(std::int32_t{-2}, ::testing::_, ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(std::int32_t{0})); + + // When allocating the same name + const auto result = + p.AllocateNamedTypedMemory(4096U, "/test_shm", score::memory::shared::permission::WorldWritable{}); + + // Then the allocation succeeds using cached offset + EXPECT_TRUE(result.has_value()); +} + +TEST(IvshmemQnxAllocateNamedTest, DirectoryHitBindsShm) +{ + // Given a directory with matching entry + std::array buf{}; + const std::uint32_t target_hash = IvshmemTypedMemoryProvider::HashName("/test_shm"); + // Write count=1 at the correct header offset (lock_word is at offset 0, count at offset 4) + *reinterpret_cast(buf.data() + sizeof(std::uint32_t)) = 1U; + auto* entry = reinterpret_cast( + buf.data() + IvshmemTypedMemoryProvider::kDirectoryHeaderSize); + entry->name_hash = target_hash; + entry->bar_offset = 4096U; + entry->alloc_size = 4096U; + std::strncpy(entry->name, "/test_shm", IvshmemTypedMemoryProvider::kMaxNameLength - 1U); + + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U, std::move(mman_mock)}; + + // Expecting mmap, shm_open, and shm_ctl to be called + EXPECT_CALL(*raw, + mmap(nullptr, + IvshmemTypedMemoryProvider::kDirectorySize, + ::testing::_, + ::testing::_, + ::testing::_, + ::testing::_)) + .WillOnce(::testing::Return(static_cast(buf.data()))); + EXPECT_CALL(*raw, shm_open(::testing::_, ::testing::_, ::testing::_)).WillOnce(::testing::Return(std::int32_t{-2})); + EXPECT_CALL(*raw, shm_ctl(std::int32_t{-2}, ::testing::_, ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(std::int32_t{0})); + + // When allocating a name found in directory + const auto result = + p.AllocateNamedTypedMemory(4096U, "/test_shm", score::memory::shared::permission::WorldWritable{}); + + // Then allocation succeeds and entry is cached + EXPECT_TRUE(result.has_value()); + EXPECT_TRUE(p.GetAllocationOffset("/test_shm").has_value()); +} + +TEST(IvshmemQnxAllocateNamedTest, ReturnsEnomemWhenBarExhausted) +{ + // Given provider with exhausted usable space (100 bytes < 4 KiB request) + std::array buf{}; + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{ + 0x100000U, IvshmemTypedMemoryProvider::kDirectorySize + 100U, std::move(mman_mock)}; + + EXPECT_CALL(*raw, + mmap(nullptr, + IvshmemTypedMemoryProvider::kDirectorySize, + ::testing::_, + ::testing::_, + ::testing::_, + ::testing::_)) + .WillOnce(::testing::Return(static_cast(buf.data()))); + + // When attempting to allocate + const auto result = + p.AllocateNamedTypedMemory(4096U, "/test_shm", score::memory::shared::permission::WorldWritable{}); + + // Then allocation fails due to insufficient space + EXPECT_FALSE(result.has_value()); +} + +TEST(IvshmemQnxAllocateNamedTest, NewAllocationSucceedsAndCaches) +{ + // Given provider with empty directory + std::array buf{}; + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U, std::move(mman_mock)}; + + // Expecting mmap once and both shm_open and shm_ctl calls + EXPECT_CALL(*raw, + mmap(nullptr, + IvshmemTypedMemoryProvider::kDirectorySize, + ::testing::_, + ::testing::_, + ::testing::_, + ::testing::_)) + .WillOnce(::testing::Return(static_cast(buf.data()))); + EXPECT_CALL(*raw, shm_open(::testing::_, ::testing::_, ::testing::_)).WillOnce(::testing::Return(std::int32_t{-2})); + EXPECT_CALL(*raw, shm_ctl(std::int32_t{-2}, ::testing::_, ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(std::int32_t{0})); + + // When allocating a new named typed memory + const auto result = + p.AllocateNamedTypedMemory(4096U, "/test_shm", score::memory::shared::permission::WorldWritable{}); + + // Then allocation succeeds and is cached for future lookups + EXPECT_TRUE(result.has_value()); + EXPECT_TRUE(p.GetAllocationOffset("/test_shm").has_value()); +} + +// This covers WriteDirectoryEntry's count-limit guard; the allocation still succeeds via BindShmToBar. + +TEST(IvshmemQnxAllocateNamedTest, FullDirectoryStillSucceedsViaBindShmToBar) +{ + // Given a directory with maximum entries filled + constexpr std::uint64_t kLargeBarSize = 2U * 1024U * 1024U; + std::array buf{}; + FillDirectoryEntries(buf.data(), IvshmemTypedMemoryProvider::kMaxDirectoryEntries); + + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{0x100000U, kLargeBarSize, std::move(mman_mock)}; + + // Expecting mmap, shm_open, and shm_ctl calls + EXPECT_CALL(*raw, + mmap(nullptr, + IvshmemTypedMemoryProvider::kDirectorySize, + ::testing::_, + ::testing::_, + ::testing::_, + ::testing::_)) + .WillOnce(::testing::Return(static_cast(buf.data()))); + EXPECT_CALL(*raw, shm_open(::testing::_, ::testing::_, ::testing::_)).WillOnce(::testing::Return(std::int32_t{-2})); + EXPECT_CALL(*raw, shm_ctl(std::int32_t{-2}, ::testing::_, ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(std::int32_t{0})); + + // When allocating a new name when directory is full + const auto result = + p.AllocateNamedTypedMemory(4096U, "/new_shm", score::memory::shared::permission::WorldWritable{}); + + // Then allocation still succeeds via BindShmToBar + EXPECT_TRUE(result.has_value()); +} + +TEST(IvshmemQnxAllocateAtOffsetTest, ReturnsEnomemWhenBarExhausted) +{ + // Given provider with exhausted usable space + auto mman_mock = std::make_unique<::testing::StrictMock>(); + TestableIvshmemTypedMemoryProvider p{ + 0x100000U, IvshmemTypedMemoryProvider::kDirectorySize + 100U, std::move(mman_mock)}; + + // When attempting to allocate at specific offset + const auto result = + p.AllocateNamedTypedMemoryAtOffset(4096U, "/test_shm", 0U, score::memory::shared::permission::WorldWritable{}); + + // Then allocation fails + EXPECT_FALSE(result.has_value()); +} + +TEST(IvshmemQnxAllocateAtOffsetTest, ReturnsEexistWhenOffsetMismatch) +{ + // Given a provider with a cached allocation at different offset + auto mman_mock = std::make_unique<::testing::StrictMock>(); + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U, std::move(mman_mock)}; + p.InsertAllocation("/test_shm", 4096U); + + // When attempting to allocate the same name at different offset + const auto result = p.AllocateNamedTypedMemoryAtOffset( + 4096U, "/test_shm", 8192U, score::memory::shared::permission::WorldWritable{}); + + // Then allocation fails due to offset mismatch + EXPECT_FALSE(result.has_value()); +} + +TEST(IvshmemQnxAllocateAtOffsetTest, MatchingOffsetInCacheBindsShm) +{ + // Given a provider with cached allocation at specified offset + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U, std::move(mman_mock)}; + p.InsertAllocation("/test_shm", 4096U); + + // Expecting shm_open and shm_ctl to be called + EXPECT_CALL(*raw, shm_open(::testing::_, ::testing::_, ::testing::_)).WillOnce(::testing::Return(std::int32_t{-2})); + EXPECT_CALL(*raw, shm_ctl(std::int32_t{-2}, ::testing::_, ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(std::int32_t{0})); + + // When allocating at the matching cached offset + const auto result = p.AllocateNamedTypedMemoryAtOffset( + 4096U, "/test_shm", 4096U, score::memory::shared::permission::WorldWritable{}); + + // Then allocation succeeds + EXPECT_TRUE(result.has_value()); +} + +TEST(IvshmemQnxAllocateAtOffsetTest, NewEntrySucceedsAndCaches) +{ + // Given a provider with no prior allocation + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U, std::move(mman_mock)}; + + // Expecting shm_open and shm_ctl to be called + EXPECT_CALL(*raw, shm_open(::testing::_, ::testing::_, ::testing::_)).WillOnce(::testing::Return(std::int32_t{-2})); + EXPECT_CALL(*raw, shm_ctl(std::int32_t{-2}, ::testing::_, ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(std::int32_t{0})); + + // When allocating new entry at specific offset + const auto result = p.AllocateNamedTypedMemoryAtOffset( + 4096U, "/test_shm", 4096U, score::memory::shared::permission::WorldWritable{}); + + // Then allocation succeeds and offset is cached + EXPECT_TRUE(result.has_value()); + ASSERT_TRUE(p.GetAllocationOffset("/test_shm").has_value()); + EXPECT_EQ(p.GetAllocationOffset("/test_shm").value(), 4096U); +} + +#endif // defined(__QNXNTO__) + +#if defined(__QNXNTO__) + +// Flow: LookupOffset→mmap fails→nullopt; FindNextFreeOffset→mmap fails→return 0U; +// WriteDirectoryEntry→mmap fails→early return; BindShmToBar succeeds normally. + +TEST(IvshmemQnxNullDirPathsTest, MmapAlwaysFailsCoversNullDirBranches) +{ + // Given provider with mmap that always fails + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U, std::move(mman_mock)}; + + // Expecting mmap to fail; shm_open/shm_ctl must NOT be called since we return ENOMEM early. + EXPECT_CALL(*raw, + mmap(nullptr, + IvshmemTypedMemoryProvider::kDirectorySize, + ::testing::_, + ::testing::_, + ::testing::_, + ::testing::_)) + .WillOnce(::testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOMEM)))); + + // When allocating when mmap fails + const auto result = + p.AllocateNamedTypedMemory(4096U, "/test_shm", score::memory::shared::permission::WorldWritable{}); + + // Then allocation fails: without a mapped directory there is no safe place to record the + // allocation, so the provider returns ENOMEM rather than silently placing everything at offset 0. + EXPECT_FALSE(result.has_value()); +} + +TEST(IvshmemQnxWriteDirEntryTest, PrefilledNonMatchingEntriesLoopIteratesBeforeAppend) +{ + // Given directory buffer with non-matching entries + std::array buf{}; + FillDirectoryEntries(buf.data(), 2U); + + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U, std::move(mman_mock)}; + + // Expecting mmap, shm_open, and shm_ctl calls + EXPECT_CALL(*raw, + mmap(nullptr, + IvshmemTypedMemoryProvider::kDirectorySize, + ::testing::_, + ::testing::_, + ::testing::_, + ::testing::_)) + .WillOnce(::testing::Return(static_cast(buf.data()))); + EXPECT_CALL(*raw, shm_open(::testing::_, ::testing::_, ::testing::_)).WillOnce(::testing::Return(std::int32_t{-2})); + EXPECT_CALL(*raw, shm_ctl(std::int32_t{-2}, ::testing::_, ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(std::int32_t{0})); + + // When allocating new entry with existing entries in directory + const auto result = + p.AllocateNamedTypedMemory(4096U, "/new_shm", score::memory::shared::permission::WorldWritable{}); + + // Then allocation succeeds and new entry is appended and cached + EXPECT_TRUE(result.has_value()); + EXPECT_TRUE(p.GetAllocationOffset("/new_shm").has_value()); +} + +TEST(IvshmemQnxLookupOffsetTest, OversizedCountStopsAtMaxEntriesGuard) +{ + // Given directory buffer with oversized count exceeding limit + std::array buf{}; + FillDirectoryEntries(buf.data(), IvshmemTypedMemoryProvider::kMaxDirectoryEntries); + *reinterpret_cast(buf.data() + sizeof(std::uint32_t)) = + IvshmemTypedMemoryProvider::kMaxDirectoryEntries + 5U; + + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{0x100000U, 1024U * 1024U, std::move(mman_mock)}; + + EXPECT_CALL(*raw, + mmap(nullptr, + IvshmemTypedMemoryProvider::kDirectorySize, + ::testing::_, + ::testing::_, + ::testing::_, + ::testing::_)) + .WillOnce(::testing::Return(static_cast(buf.data()))); + + // When looking up entry with oversized count + const auto result = p.LookupOffsetInDirectory("/safe_shm"); + + // Then scan stops at max entries guard instead of count field + EXPECT_FALSE(result.has_value()); +} + +TEST(IvshmemQnxLookupOffsetTest, OversizedCountAllocateCoversAllocateFindNextFreeOffset) +{ + // Given a tight BAR and a directory already filled to max entries. + constexpr std::uint64_t kEntrySize = 4096U; + constexpr std::uint64_t kTightBarSize = + static_cast(IvshmemTypedMemoryProvider::kMaxDirectoryEntries) * kEntrySize + + IvshmemTypedMemoryProvider::kDirectorySize; + + std::array buf{}; + FillDirectoryEntries(buf.data(), IvshmemTypedMemoryProvider::kMaxDirectoryEntries); + // Write count at the correct header offset (count is at offset 4, not offset 0). + // offset 0 = lock_word, must stay 0 so the spinlock can be acquired. + *reinterpret_cast(buf.data() + sizeof(std::uint32_t)) = + IvshmemTypedMemoryProvider::kMaxDirectoryEntries + 5U; + + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + TestableIvshmemTypedMemoryProvider p{0x100000U, kTightBarSize, std::move(mman_mock)}; + + EXPECT_CALL(*raw, + mmap(nullptr, + IvshmemTypedMemoryProvider::kDirectorySize, + ::testing::_, + ::testing::_, + ::testing::_, + ::testing::_)) + .WillOnce(::testing::Return(static_cast(buf.data()))); + + // When allocating with oversized count exhausts BAR + const auto result = + p.AllocateNamedTypedMemory(4096U, "/safe_shm", score::memory::shared::permission::WorldWritable{}); + + // Then allocation fails because FindNextFreeOffset exhausts available space + EXPECT_FALSE(result.has_value()); +} + +#endif // defined(__QNXNTO__) + +TEST(IvshmemTypedMemoryProviderStaticTest, DirectorySizeIs4096) +{ + // When checking directory size constant + // Then it equals 4096 bytes + EXPECT_EQ(IvshmemTypedMemoryProvider::kDirectorySize, 4096U); +} + +TEST(IvshmemTypedMemoryProviderStaticTest, MaxDirectoryEntriesFitsInDirectoryPage) +{ + // When calculating max directory entries + const std::uint32_t expected = + (4096U - IvshmemTypedMemoryProvider::kDirectoryHeaderSize) / sizeof(IvshmemTypedMemoryProvider::DirectoryEntry); + + // Then count fits within directory page and is positive + EXPECT_EQ(IvshmemTypedMemoryProvider::kMaxDirectoryEntries, expected); + EXPECT_GT(IvshmemTypedMemoryProvider::kMaxDirectoryEntries, 0U); +} + +TEST(IvshmemTypedMemoryProviderStaticTest, DirectoryEntryHasExpectedSize) +{ + // When checking DirectoryEntry struct size + // Then size equals 72 bytes: 4 (name_hash) + 4 (alloc_size) + 8 (bar_offset) + 56 (name) + EXPECT_EQ(sizeof(IvshmemTypedMemoryProvider::DirectoryEntry), 72U); +} + +} // namespace + +} // namespace score::mw::com::gateway::qemu::ivshmem diff --git a/score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport.cpp b/score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport.cpp new file mode 100644 index 000000000..1342b0f5b --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport.cpp @@ -0,0 +1,415 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport.h" + +#include "score/mw/com/gateway/transport_layer/sample/messages/gateway_messages.h" +#include "score/mw/log/logging.h" + +#include "score/memory/shared/shared_memory_factory.h" + +#include +#include + +#if defined(__QNXNTO__) +#include +#include +#include +#include +#endif + +namespace score::mw::com::gateway +{ + +ShmPaths ResolveInterVmShmPaths(const impl::InstanceSpecifier& specifier) +{ + // Constructs the inter-VM shm paths using the well-known prefix that + // SharedMemoryFactory routes through the InterVM memory provider. + // + // In the full LoLa production stack, the skeleton uses ShmPathBuilder(service_id, inter_vm=true) + // to create paths like: /intervm-shared-shmem/lola-data-XXXX-YYYY + // The ShmPathBuilder is an internal LoLa binding detail (visibility restricted to + // impl/plumbing). The transport layer cannot access it directly. + // + // In production integration, the GatewayCore would resolve the specifier via Runtime, + // extract the LoLa deployment info (service_id + instance_id), and pass the resolved + // paths to the transport — either via an extended ProvideService API or by storing them + // in a transport-accessible registry. For now, we construct paths from the specifier + // string, which works for the integration test where paths are self-consistent. + // TODO: The path construction below is a temporary placeholder for integration testing. + // In production, GatewayCore must resolve the specifier via Runtime, extract the LoLa + // deployment info (service_id + instance_id), and supply the canonical ShmPathBuilder + // paths to the transport. Track in issue #TBD. + const std::string base = std::string{"/intervm-shared-shmem/"} + std::string{specifier.ToString()}; + return ShmPaths{base + "/ctrl", base + "/data"}; +} + +#if defined(__QNXNTO__) +ShmSizes GetInterVmShmSizes(const impl::InstanceSpecifier& specifier, const score::os::qnx::MmanQnx* mman_qnx) +{ + // Query the actual sizes of the existing shm objects that the local skeleton created. + // The source-side skeleton has already called SharedMemoryFactory::Create() for both + // CTRL and DATA shm. We open them briefly to read their size via fstat(). + const auto paths = ResolveInterVmShmPaths(specifier); + ShmSizes sizes{0U, 0U}; + + // Query CTRL shm size + { + std::int32_t fd = -1; + const auto fd_result = mman_qnx->shm_open(paths.control.c_str(), O_RDONLY, 0); + if (fd_result.has_value()) + { + fd = fd_result.value(); + } + if (fd != -1) + { + struct stat st{}; + if (::fstat(fd, &st) == 0) + { + sizes.control = static_cast(st.st_size); + } + (void)::close(fd); + } + } + // Query DATA shm size + { + std::int32_t fd = -1; + const auto fd_result = mman_qnx->shm_open(paths.data.c_str(), O_RDONLY, 0); + if (fd_result.has_value()) + { + fd = fd_result.value(); + } + if (fd != -1) + { + struct stat st{}; + if (::fstat(fd, &st) == 0) + { + sizes.data = static_cast(st.st_size); + } + (void)::close(fd); + } + } + return sizes; +} +#else +ShmSizes GetInterVmShmSizes([[maybe_unused]] const impl::InstanceSpecifier& specifier) +{ + return ShmSizes{0U, 0U}; +} +#endif + +QemuHypervisorTransport::QemuHypervisorTransport( + GatewayCore& gateway_app, + std::unique_ptr transport, + std::shared_ptr ivshmem_provider, + ShmPathResolver path_resolver) noexcept + : gateway_app_{gateway_app}, + message_transport_{std::move(transport)}, + ivshmem_provider_{std::move(ivshmem_provider)}, + path_resolver_{std::move(path_resolver)} +{ +} + +QemuHypervisorTransport::~QemuHypervisorTransport() +{ + Shutdown(); +} + +bool QemuHypervisorTransport::IsMemorySharingSupported() const +{ + return true; +} + +score::ResultBlank QemuHypervisorTransport::Setup() +{ + message_transport_->SetMessageHandler([this](std::unique_ptr message) { + OnMessageReceived(std::move(message)); + }); + return message_transport_->Setup(); +} + +void QemuHypervisorTransport::OnMessageReceived(std::unique_ptr message) +{ + if (!message) + { + log::LogError("LoLa") << "QemuTransport: Invalid message received: nullptr"; + return; + } + + if (message->GetType() == MessageType::kProvideServiceRequest) + { + HandleProvideServiceRequest(std::move(message)); + } + else if (message->GetType() == MessageType::kStopOfferServiceRequest) + { + HandleStopOfferServiceRequest(std::move(message)); + } + else if (message->GetType() == MessageType::kOfferServiceRequest) + { + HandleOfferServiceRequest(std::move(message)); + } + else if (message->GetType() == MessageType::kRegisterNotificationRequest) + { + HandleRegisterNotificationRequest(std::move(message)); + } + else if (message->GetType() == MessageType::kUnregisterNotificationRequest) + { + HandleUnregisterNotificationRequest(std::move(message)); + } + else if (message->GetType() == MessageType::kUpdateNotification) + { + HandleUpdateNotification(std::move(message)); + } + else + { + log::LogError("LoLa") << "QemuTransport: Unexpected TransportMessage received: " + << static_cast(message->GetType()); + } +} + +void QemuHypervisorTransport::HandleProvideServiceRequest(std::unique_ptr message) +{ + auto* const request_ptr = dynamic_cast(message.get()); + if (request_ptr == nullptr) + { + log::LogError("LoLa") << "QemuTransport: message type mismatch in HandleProvideServiceRequest"; + return; + } + auto& request = *request_ptr; + auto specifier_result = impl::InstanceSpecifier::Create(std::string{request.GetInstanceSpecifier()}); + if (!specifier_result.has_value()) + { + log::LogError("LoLa") << "QemuTransport: Invalid instance specifier in ProvideServiceRequest!"; + return; + } + PreCreateInterVmSharedMemory(specifier_result.value(), request.GetShmControlSize(), request.GetShmDataSize()); + gateway_app_.ProvideService(specifier_result.value(), request.GetServiceElements()); +} + +void QemuHypervisorTransport::HandleStopOfferServiceRequest(std::unique_ptr message) +{ + auto* const request_ptr = dynamic_cast(message.get()); + if (request_ptr == nullptr) + { + log::LogError("LoLa") << "QemuTransport: message type mismatch in HandleStopOfferServiceRequest"; + return; + } + auto& request = *request_ptr; + auto specifier_result = impl::InstanceSpecifier::Create(std::string{request.GetInstanceSpecifier()}); + if (!specifier_result.has_value()) + { + log::LogError("LoLa") << "QemuTransport: Invalid instance specifier in StopOfferServiceRequest!"; + return; + } + gateway_app_.StopOfferService(specifier_result.value()); +} + +void QemuHypervisorTransport::HandleOfferServiceRequest(std::unique_ptr message) +{ + auto* const request_ptr = dynamic_cast(message.get()); + if (request_ptr == nullptr) + { + log::LogError("LoLa") << "QemuTransport: message type mismatch in HandleOfferServiceRequest"; + return; + } + auto& request = *request_ptr; + auto specifier_result = impl::InstanceSpecifier::Create(std::string{request.GetInstanceSpecifier()}); + if (!specifier_result.has_value()) + { + log::LogError("LoLa") << "QemuTransport: Invalid instance specifier in OfferServiceRequest!"; + return; + } + gateway_app_.OfferService(specifier_result.value()); +} + +void QemuHypervisorTransport::HandleUpdateNotification(std::unique_ptr message) +{ + auto* const notification_ptr = dynamic_cast(message.get()); + if (notification_ptr == nullptr) + { + log::LogError("LoLa") << "QemuTransport: message type mismatch in HandleUpdateNotification"; + return; + } + auto& notification = *notification_ptr; + auto specifier_result = impl::InstanceSpecifier::Create(std::string{notification.GetInstanceSpecifier()}); + if (!specifier_result.has_value()) + { + log::LogError("LoLa") << "QemuTransport: Invalid instance specifier in UpdateNotification!"; + return; + } + gateway_app_.NotifyUpdate(specifier_result.value(), notification.GetElementType(), notification.GetElementName()); +} + +void QemuHypervisorTransport::HandleRegisterNotificationRequest(std::unique_ptr message) +{ + auto* const request_ptr = dynamic_cast(message.get()); + if (request_ptr == nullptr) + { + log::LogError("LoLa") << "QemuTransport: message type mismatch in HandleRegisterNotificationRequest"; + return; + } + auto& request = *request_ptr; + auto specifier_result = impl::InstanceSpecifier::Create(std::string{request.GetInstanceSpecifier()}); + if (!specifier_result.has_value()) + { + log::LogError("LoLa") << "QemuTransport: Invalid instance specifier in RegisterNotificationRequest!"; + return; + } + gateway_app_.RegisterUpdateNotification( + specifier_result.value(), request.GetElementType(), request.GetElementName()); +} + +void QemuHypervisorTransport::HandleUnregisterNotificationRequest(std::unique_ptr message) +{ + auto* const request_ptr = dynamic_cast(message.get()); + if (request_ptr == nullptr) + { + log::LogError("LoLa") << "QemuTransport: message type mismatch in HandleUnregisterNotificationRequest"; + return; + } + auto& request = *request_ptr; + auto specifier_result = impl::InstanceSpecifier::Create(std::string{request.GetInstanceSpecifier()}); + if (!specifier_result.has_value()) + { + log::LogError("LoLa") << "QemuTransport: Invalid instance specifier in UnregisterNotificationRequest!"; + return; + } + gateway_app_.UnregisterUpdateNotification( + specifier_result.value(), request.GetElementType(), request.GetElementName()); +} + +void QemuHypervisorTransport::Shutdown() +{ + message_transport_->Shutdown(); +} + +score::ResultBlank QemuHypervisorTransport::ProvideService(impl::InstanceSpecifier service_instance_specifier, + std::vector service_elements) +{ +#if defined(__QNXNTO__) + const auto shm_sizes = GetInterVmShmSizes(service_instance_specifier, ivshmem_provider_->GetMmanQnx()); +#else + const auto shm_sizes = GetInterVmShmSizes(service_instance_specifier); +#endif + + ProvideServiceRequest request{ + std::move(service_instance_specifier), std::move(service_elements), shm_sizes.control, shm_sizes.data}; + return message_transport_->SendRequest(request); +} + +score::ResultBlank QemuHypervisorTransport::OfferService(impl::InstanceSpecifier service_instance_specifier) +{ + OfferServiceRequest request{std::move(service_instance_specifier)}; + return message_transport_->SendRequest(request); +} + +score::ResultBlank QemuHypervisorTransport::StopOfferService(impl::InstanceSpecifier service_instance_specifier) +{ + StopOfferServiceRequest request{std::move(service_instance_specifier)}; + return message_transport_->SendRequest(request); +} + +score::ResultBlank QemuHypervisorTransport::NotifyUpdate(impl::InstanceSpecifier service_instance_specifier, + impl::ServiceElementType updated_element_type, + std::string updated_element_name) +{ + UpdateNotification notification{ + std::move(service_instance_specifier), updated_element_type, std::move(updated_element_name)}; + return message_transport_->SendNotification(notification); +} + +score::ResultBlank QemuHypervisorTransport::RegisterUpdateNotification( + impl::InstanceSpecifier service_instance_specifier, + impl::ServiceElementType element_type, + std::string element_name) +{ + RegisterNotificationRequest request{std::move(service_instance_specifier), element_type, std::move(element_name)}; + return message_transport_->SendRequest(request); +} + +score::ResultBlank QemuHypervisorTransport::UnregisterUpdateNotification( + impl::InstanceSpecifier service_instance_specifier, + impl::ServiceElementType element_type, + std::string element_name) +{ + UnregisterNotificationRequest request{std::move(service_instance_specifier), element_type, std::move(element_name)}; + return message_transport_->SendRequest(request); +} + +void QemuHypervisorTransport::PreCreateInterVmSharedMemory(const impl::InstanceSpecifier& specifier, + std::uint32_t shm_control_size, + std::uint32_t shm_data_size) +{ + auto paths = path_resolver_(specifier); + if (paths.control.empty()) + { + ::score::mw::log::LogError() << "PreCreateInterVmSharedMemory: failed to resolve SHM paths for " + << specifier.ToString(); + return; + } + + // Look up the BAR offsets from the BAR-resident directory written by the source VM. + // This ensures both VMs bind the same name to the same physical BAR sub-range. + const auto ctrl_offset = ivshmem_provider_->LookupOffsetInDirectory(paths.control); + const auto data_offset = ivshmem_provider_->LookupOffsetInDirectory(paths.data); + + if (ctrl_offset.has_value()) + { + const auto ctrl_result = ivshmem_provider_->AllocateNamedTypedMemoryAtOffset( + shm_control_size, + paths.control, + ctrl_offset.value(), + score::memory::shared::SharedMemoryFactory::WorldWritable{}); // COV_JUSTIFIED + // qemu-worldwritable-ctor-gcc-artifact + if (!ctrl_result.has_value()) + { + ::score::mw::log::LogError() << "PreCreateInterVmSharedMemory: failed to bind CTRL shm to BAR for " + << specifier.ToString() << " at offset " << ctrl_offset.value(); + return; + } + } + else if (shm_control_size > 0U) + { + ::score::mw::log::LogWarn() << "PreCreateInterVmSharedMemory: CTRL offset not found in directory for " + << specifier.ToString() << ", skipping CTRL binding"; + } + + if (data_offset.has_value()) + { + const auto data_result = ivshmem_provider_->AllocateNamedTypedMemoryAtOffset( + shm_data_size, + paths.data, + data_offset.value(), + score::memory::shared::SharedMemoryFactory::WorldWritable{}); // COV_JUSTIFIED + // qemu-worldwritable-ctor-gcc-artifact + if (!data_result.has_value()) + { + ::score::mw::log::LogError() << "PreCreateInterVmSharedMemory: failed to bind DATA shm to BAR for " + << specifier.ToString() << " at offset " << data_offset.value(); + return; + } + } + else if (shm_data_size > 0U) + { + ::score::mw::log::LogWarn() << "PreCreateInterVmSharedMemory: DATA offset not found in directory for " + << specifier.ToString() << ", skipping DATA binding"; + } + + ::score::mw::log::LogInfo() << "PreCreateInterVmSharedMemory: bound CTRL(" << paths.control << ", " + << shm_control_size + << "B, offset=" << (ctrl_offset.has_value() ? ctrl_offset.value() : 0U) << ") + DATA(" + << paths.data << ", " << shm_data_size + << "B, offset=" << (data_offset.has_value() ? data_offset.value() : 0U) + << ") to ivshmem BAR for " << specifier.ToString(); +} + +} // namespace score::mw::com::gateway diff --git a/score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport.h b/score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport.h new file mode 100644 index 000000000..81897f370 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport.h @@ -0,0 +1,107 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_HYPERVISOR_TRANSPORT_H_ +#define SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_HYPERVISOR_TRANSPORT_H_ + +#include "score/mw/com/gateway/gateway_application/gateway_core.h" +#include "score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider.h" +#include "score/mw/com/gateway/transport_layer/sample/i_bidirectional_transport.h" +#include "score/mw/com/gateway/transport_layer/sample/sample_hypervisor_transport.h" +#include "score/mw/com/gateway/transport_layer/transport.h" + +#include +#include + +namespace score::mw::com::gateway +{ + +/// Resolves the inter-VM shm paths (with /intervm-shared-shmem/ prefix) for the given +/// service instance specifier so that lib-memory routes them through the InterVM provider. +ShmPaths ResolveInterVmShmPaths(const impl::InstanceSpecifier& specifier); + +/// Opens the existing shm objects and returns their sizes. +#if defined(__QNXNTO__) +ShmSizes GetInterVmShmSizes(const impl::InstanceSpecifier& specifier, const score::os::qnx::MmanQnx* mman_qnx); +#else +ShmSizes GetInterVmShmSizes(const impl::InstanceSpecifier& specifier); +#endif + +/// QEMU/ivshmem-based transport layer for the LoLa gateway. +/// +/// Reuses the message-communication layer from ``sample`` (BidirectionalTransport over TCP +/// sockets on the QEMU intervm NIC) and implements the memory-sharing part via ivshmem: +/// both VMs map the same ivshmem BAR through the IvshmemTypedMemoryProvider, so shm objects +/// with the inter-VM prefix land on the shared physical BAR. +class QemuHypervisorTransport : public Transport +{ + public: + /// Callable type used to resolve SHM paths for a given instance specifier. + /// Defaults to ResolveInterVmShmPaths; injectable for testing. + using ShmPathResolver = std::function; + + QemuHypervisorTransport(GatewayCore& gateway_app, + std::unique_ptr transport, + std::shared_ptr ivshmem_provider, + ShmPathResolver path_resolver = ResolveInterVmShmPaths) noexcept; + ~QemuHypervisorTransport() override; + + QemuHypervisorTransport(const QemuHypervisorTransport&) = delete; + QemuHypervisorTransport& operator=(const QemuHypervisorTransport&) = delete; + QemuHypervisorTransport(QemuHypervisorTransport&&) = delete; + QemuHypervisorTransport& operator=(QemuHypervisorTransport&&) = delete; + + bool IsMemorySharingSupported() const override; + + Result Setup() override; + void Shutdown() override; + + Result ProvideService(impl::InstanceSpecifier service_instance_specifier, + std::vector service_elements) override; + Result OfferService(impl::InstanceSpecifier service_instance_specifier) override; + Result StopOfferService(impl::InstanceSpecifier service_instance_specifier) override; + + Result NotifyUpdate(impl::InstanceSpecifier service_instance_specifier, + impl::ServiceElementType updated_element_type, + std::string updated_element_name) override; + Result RegisterUpdateNotification(impl::InstanceSpecifier service_instance_specifier, + impl::ServiceElementType element_type, + std::string element_name) override; + Result UnregisterUpdateNotification(impl::InstanceSpecifier service_instance_specifier, + impl::ServiceElementType element_type, + std::string element_name) override; + + private: + void HandleProvideServiceRequest(std::unique_ptr message); + void HandleStopOfferServiceRequest(std::unique_ptr message); + void HandleOfferServiceRequest(std::unique_ptr message); + void HandleUpdateNotification(std::unique_ptr message); + void HandleRegisterNotificationRequest(std::unique_ptr message); + void HandleUnregisterNotificationRequest(std::unique_ptr message); + void OnMessageReceived(std::unique_ptr message); + + /// Binds the inter-VM shm paths to the ivshmem BAR on the destination side. + /// Looks up the source-side BAR offsets from the BAR-resident directory so that + /// both VMs map the same physical sub-range regardless of local allocation order. + void PreCreateInterVmSharedMemory(const impl::InstanceSpecifier& specifier, + std::uint32_t shm_control_size, + std::uint32_t shm_data_size); + + GatewayCore& gateway_app_; + std::unique_ptr message_transport_; + std::shared_ptr ivshmem_provider_; + ShmPathResolver path_resolver_; +}; + +} // namespace score::mw::com::gateway + +#endif // SCORE_MW_COM_GATEWAY_TRANSPORT_LAYER_QEMU_HYPERVISOR_TRANSPORT_H_ diff --git a/score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport_test.cpp b/score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport_test.cpp new file mode 100644 index 000000000..5d5ba3a88 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport_test.cpp @@ -0,0 +1,689 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport.h" + +#include "score/mw/com/gateway/gateway_application/gateway_core_mock.h" +#include "score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider_mock.h" +#include "score/mw/com/gateway/transport_layer/sample/bidirectional_transport_mock.h" +#include "score/mw/com/gateway/transport_layer/transport_error.h" + +#include "score/mw/log/recorder_mock.h" + +#if defined(__QNXNTO__) +#include "score/os/mocklib/qnx/mock_mman.h" +#endif + +#include +#include + +namespace score::mw::com::gateway +{ + +namespace +{ + +class QemuHypervisorTransportTest : public ::testing::Test +{ + public: + QemuHypervisorTransportTest& WithAQemuHypervisorTransport() + { + transport_ = std::make_unique( + gateway_core_mock_, std::move(mock_transport_owner_), ivshmem_provider_mock_); + return *this; + } + + QemuHypervisorTransportTest& WithAQemuHypervisorTransportWithEmptyPathResolver() + { + transport_ = std::make_unique(gateway_core_mock_, + std::move(mock_transport_owner_), + ivshmem_provider_mock_, + [](const impl::InstanceSpecifier&) { + return ShmPaths{"", ""}; + }); + return *this; + } + + QemuHypervisorTransportTest& WithARegisteredOnSetupCallback() + { + EXPECT_CALL(*bi_directional_transport_mock_, SetMessageHandler(::testing::_)) + .WillOnce([this](IBidirectionalTransport::MessageHandler handler) { + captured_handler_ = std::move(handler); + }); + EXPECT_CALL(*bi_directional_transport_mock_, Setup()).WillOnce(::testing::Return(score::ResultBlank{})); + const auto setup_result = transport_->Setup(); + EXPECT_TRUE(setup_result.has_value()); + return *this; + } + + impl::InstanceSpecifier CreateValidInstanceSpecifier() + { + const auto specifier_result = impl::InstanceSpecifier::Create(std::string{"SpeedService/Instance42"}); + EXPECT_TRUE(specifier_result.has_value()); + return specifier_result.value(); + } + + std::unique_ptr CreateMessageOfType(MessageType type, bool valid_instance_specifier = true) + { + if (!valid_instance_specifier) + { + switch (type) + { + case MessageType::kProvideServiceRequest: + return std::make_unique(); + case MessageType::kStopOfferServiceRequest: + return std::make_unique(); + case MessageType::kOfferServiceRequest: + return std::make_unique(); + case MessageType::kRegisterNotificationRequest: + return std::make_unique(); + case MessageType::kUnregisterNotificationRequest: + return std::make_unique(); + case MessageType::kUpdateNotification: + return std::make_unique(); + case MessageType::kAckResponse: + return std::make_unique(); + case MessageType::kInvalid: + default: + return nullptr; + } + } + impl::InstanceSpecifier specifier = CreateValidInstanceSpecifier(); + switch (type) + { + case MessageType::kProvideServiceRequest: + { + std::vector elements{}; + constexpr std::uint32_t kShmControlSize = 1024U; + constexpr std::uint32_t kShmDataSize = 4096U; + return std::make_unique(specifier, elements, kShmControlSize, kShmDataSize); + } + case MessageType::kStopOfferServiceRequest: + return std::make_unique(specifier); + case MessageType::kOfferServiceRequest: + return std::make_unique(specifier); + case MessageType::kRegisterNotificationRequest: + return std::make_unique( + specifier, impl::ServiceElementType::EVENT, "SpeedEvent"); + case MessageType::kUnregisterNotificationRequest: + return std::make_unique( + specifier, impl::ServiceElementType::EVENT, "SpeedEvent"); + case MessageType::kUpdateNotification: + return std::make_unique(specifier, impl::ServiceElementType::EVENT, "SpeedEvent"); + case MessageType::kAckResponse: + return std::make_unique(); + case MessageType::kInvalid: + default: + return nullptr; + } + } + + protected: + void SetUp() override + { + mock_transport_owner_ = std::make_unique(); + bi_directional_transport_mock_ = mock_transport_owner_.get(); + ivshmem_provider_mock_ = std::make_shared(); + ivshmem_provider_raw_ = + std::static_pointer_cast(ivshmem_provider_mock_).get(); + } + + void TearDown() override {} + + std::unique_ptr transport_; + BidirectionalTransportMock* bi_directional_transport_mock_{nullptr}; + std::unique_ptr mock_transport_owner_; + std::shared_ptr ivshmem_provider_mock_; + qemu::ivshmem::IvshmemTypedMemoryProviderMock* ivshmem_provider_raw_{nullptr}; + GatewayCoreMock gateway_core_mock_; + IBidirectionalTransport::MessageHandler captured_handler_; +}; + +TEST_F(QemuHypervisorTransportTest, CanBeConstructedWithValidParameters) +{ + // Expecting Shutdown to be called + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + // When constructing QemuHypervisorTransport with valid parameters + // Then no exception is thrown + EXPECT_NO_THROW(QemuHypervisorTransport transport( + gateway_core_mock_, std::move(mock_transport_owner_), ivshmem_provider_mock_)); +} + +TEST_F(QemuHypervisorTransportTest, DestructorCallsShutdown) +{ + // Expecting Shutdown to be called once via destructor + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + // When transport goes out of scope + { + QemuHypervisorTransport transport(gateway_core_mock_, std::move(mock_transport_owner_), ivshmem_provider_mock_); + } + // Then destructor calls Shutdown +} + +TEST_F(QemuHypervisorTransportTest, IsMemorySharingSupportedReturnsTrue) +{ + // Given a constructed transport + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + QemuHypervisorTransport transport(gateway_core_mock_, std::move(mock_transport_owner_), ivshmem_provider_mock_); + + // When calling IsMemorySharingSupported + // Then it returns true + EXPECT_TRUE(transport.IsMemorySharingSupported()); +} + +TEST_F(QemuHypervisorTransportTest, SetupCallsSetMessageHandlerAndSetupOnTransport) +{ + EXPECT_CALL(*bi_directional_transport_mock_, SetMessageHandler(::testing::_)).Times(1); + EXPECT_CALL(*bi_directional_transport_mock_, Setup()).WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + QemuHypervisorTransport transport(gateway_core_mock_, std::move(mock_transport_owner_), ivshmem_provider_mock_); + const auto result = transport.Setup(); + EXPECT_TRUE(result.has_value()); +} + +TEST_F(QemuHypervisorTransportTest, SetupReturnsErrorWhenTransportSetupFails) +{ + this->WithAQemuHypervisorTransport(); + + EXPECT_CALL(*bi_directional_transport_mock_, SetMessageHandler(::testing::_)).Times(1); + EXPECT_CALL(*bi_directional_transport_mock_, Setup()) + .WillOnce(::testing::Return(score::MakeUnexpected(TransportErrorc::kConnectionFailure))); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + const auto result = transport_->Setup(); + EXPECT_FALSE(result.has_value()); +} + +TEST_F(QemuHypervisorTransportTest, ShutdownCallsShutdownOnTransport) +{ + // Shutdown called explicitly + in destructor = 2 times + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(2); + + QemuHypervisorTransport transport(gateway_core_mock_, std::move(mock_transport_owner_), ivshmem_provider_mock_); + transport.Shutdown(); +} + +TEST_F(QemuHypervisorTransportTest, OfferServiceSendsOfferServiceRequest) +{ + this->WithAQemuHypervisorTransport(); + const auto specifier = CreateValidInstanceSpecifier(); + + EXPECT_CALL(*bi_directional_transport_mock_, + SendRequest(::testing::Property(&TransportMessage::GetType, MessageType::kOfferServiceRequest))) + .WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + transport_->OfferService(specifier); +} + +TEST_F(QemuHypervisorTransportTest, StopOfferServiceSendsStopOfferServiceRequest) +{ + this->WithAQemuHypervisorTransport(); + const auto specifier = CreateValidInstanceSpecifier(); + + EXPECT_CALL(*bi_directional_transport_mock_, + SendRequest(::testing::Property(&TransportMessage::GetType, MessageType::kStopOfferServiceRequest))) + .WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + transport_->StopOfferService(specifier); +} + +TEST_F(QemuHypervisorTransportTest, NotifyUpdateSendsUpdateNotification) +{ + this->WithAQemuHypervisorTransport(); + const auto specifier = CreateValidInstanceSpecifier(); + + EXPECT_CALL(*bi_directional_transport_mock_, + SendNotification(::testing::Property(&TransportMessage::GetType, MessageType::kUpdateNotification))) + .WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + transport_->NotifyUpdate(specifier, impl::ServiceElementType::EVENT, "SpeedEvent"); +} + +TEST_F(QemuHypervisorTransportTest, RegisterUpdateNotificationSendsRegisterRequest) +{ + this->WithAQemuHypervisorTransport(); + const auto specifier = CreateValidInstanceSpecifier(); + + EXPECT_CALL(*bi_directional_transport_mock_, + SendRequest(::testing::Property(&TransportMessage::GetType, MessageType::kRegisterNotificationRequest))) + .WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + transport_->RegisterUpdateNotification(specifier, impl::ServiceElementType::EVENT, "SpeedEvent"); +} + +TEST_F(QemuHypervisorTransportTest, UnregisterUpdateNotificationSendsUnregisterRequest) +{ + this->WithAQemuHypervisorTransport(); + const auto specifier = CreateValidInstanceSpecifier(); + + EXPECT_CALL( + *bi_directional_transport_mock_, + SendRequest(::testing::Property(&TransportMessage::GetType, MessageType::kUnregisterNotificationRequest))) + .WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + transport_->UnregisterUpdateNotification(specifier, impl::ServiceElementType::EVENT, "SpeedEvent"); +} + +TEST_F(QemuHypervisorTransportTest, ProvideServiceSendsProvideServiceRequestWithShmSizes) +{ + this->WithAQemuHypervisorTransport(); + const auto specifier = CreateValidInstanceSpecifier(); + + EXPECT_CALL(*bi_directional_transport_mock_, + SendRequest(::testing::Property(&TransportMessage::GetType, MessageType::kProvideServiceRequest))) + .WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + transport_->ProvideService(specifier, std::vector{}); +} + +TEST_F(QemuHypervisorTransportTest, + OnMessageReceivedProvideServiceRequestWithValidSpecifierCallsProvideServiceOnGatewayCore) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + auto request = CreateMessageOfType(MessageType::kProvideServiceRequest); + + // PreCreateInterVmSharedMemory will call LookupOffsetInDirectory which returns nullopt on non-QNX + EXPECT_CALL(*ivshmem_provider_raw_, LookupOffsetInDirectory(::testing::_)) + .WillRepeatedly(::testing::Return(std::nullopt)); + + EXPECT_CALL(gateway_core_mock_, ProvideService(::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, OnMessageReceivedProvideServiceRequestWithInvalidSpecifierReturnsWithoutCoreCall) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + auto request = CreateMessageOfType(MessageType::kProvideServiceRequest, false); + + EXPECT_CALL(gateway_core_mock_, ProvideService(::testing::_, ::testing::_)).Times(0); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, + OnMessageReceivedStopOfferServiceRequestWithValidSpecifierCallsStopOfferServiceOnGatewayCore) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + auto request = CreateMessageOfType(MessageType::kStopOfferServiceRequest); + + EXPECT_CALL(gateway_core_mock_, StopOfferService(::testing::_)).WillOnce(::testing::Return()); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, OnMessageReceivedStopOfferServiceRequestWithInvalidSpecifierReturnsWithoutCoreCall) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + auto request = CreateMessageOfType(MessageType::kStopOfferServiceRequest, false); + + EXPECT_CALL(gateway_core_mock_, StopOfferService(::testing::_)).Times(0); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, + OnMessageReceivedOfferServiceRequestWithValidSpecifierCallsOfferServiceOnGatewayCore) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + auto request = CreateMessageOfType(MessageType::kOfferServiceRequest); + + EXPECT_CALL(gateway_core_mock_, OfferService(::testing::_)).WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, OnMessageReceivedOfferServiceRequestWithInvalidSpecifierReturnsWithoutCoreCall) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + auto request = CreateMessageOfType(MessageType::kOfferServiceRequest, false); + + EXPECT_CALL(gateway_core_mock_, OfferService(::testing::_)).Times(0); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, + OnMessageReceivedRegisterNotificationRequestWithValidSpecifierCallsRegisterOnGatewayCore) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + auto request = CreateMessageOfType(MessageType::kRegisterNotificationRequest); + + EXPECT_CALL(gateway_core_mock_, RegisterUpdateNotification(::testing::_, ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, + OnMessageReceivedRegisterNotificationRequestWithInvalidSpecifierReturnsWithoutCoreCall) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + auto request = CreateMessageOfType(MessageType::kRegisterNotificationRequest, false); + + EXPECT_CALL(gateway_core_mock_, RegisterUpdateNotification(::testing::_, ::testing::_, ::testing::_)).Times(0); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, + OnMessageReceivedUnregisterNotificationRequestWithValidSpecifierCallsUnregisterOnGatewayCore) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + auto request = CreateMessageOfType(MessageType::kUnregisterNotificationRequest); + + EXPECT_CALL(gateway_core_mock_, UnregisterUpdateNotification(::testing::_, ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, + OnMessageReceivedUnregisterNotificationRequestWithInvalidSpecifierReturnsWithoutCoreCall) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + auto request = CreateMessageOfType(MessageType::kUnregisterNotificationRequest, false); + + EXPECT_CALL(gateway_core_mock_, UnregisterUpdateNotification(::testing::_, ::testing::_, ::testing::_)).Times(0); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, OnMessageReceivedUpdateNotificationWithValidSpecifierCallsNotifyUpdateOnGatewayCore) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + auto request = CreateMessageOfType(MessageType::kUpdateNotification); + + EXPECT_CALL(gateway_core_mock_, NotifyUpdate(::testing::_, ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, OnMessageReceivedUpdateNotificationWithInvalidSpecifierReturnsWithoutCoreCall) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + auto request = CreateMessageOfType(MessageType::kUpdateNotification, false); + + EXPECT_CALL(gateway_core_mock_, NotifyUpdate(::testing::_, ::testing::_, ::testing::_)).Times(0); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, UnsupportedMessageWillBeLogged) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + score::mw::log::RecorderMock recorder_mock{}; + score::mw::log::SetLogRecorder(&recorder_mock); + + auto request = CreateMessageOfType(MessageType::kAckResponse); + + EXPECT_CALL(recorder_mock, StartRecord(::testing::_, mw::log::LogLevel::kError)) + .WillOnce(::testing::Return(mw::log::SlotHandle{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + captured_handler_(std::move(request)); + + score::mw::log::SetLogRecorder(nullptr); +} + +TEST_F(QemuHypervisorTransportTest, NullptrMessageWillBeLogged) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + score::mw::log::RecorderMock recorder_mock{}; + score::mw::log::SetLogRecorder(&recorder_mock); + + EXPECT_CALL(recorder_mock, StartRecord(::testing::_, mw::log::LogLevel::kError)) + .WillOnce(::testing::Return(mw::log::SlotHandle{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + captured_handler_(nullptr); + + score::mw::log::SetLogRecorder(nullptr); +} + +TEST_F(QemuHypervisorTransportTest, PreCreateInterVmSharedMemoryBindsCtrlAndDataWhenOffsetsFound) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + // LookupOffsetInDirectory returns valid offsets for both CTRL and DATA + EXPECT_CALL(*ivshmem_provider_raw_, LookupOffsetInDirectory(::testing::HasSubstr("/ctrl"))) + .WillOnce(::testing::Return(std::optional{0U})); + EXPECT_CALL(*ivshmem_provider_raw_, LookupOffsetInDirectory(::testing::HasSubstr("/data"))) + .WillOnce(::testing::Return(std::optional{4096U})); + + // AllocateNamedTypedMemoryAtOffset succeeds for both + EXPECT_CALL(*ivshmem_provider_raw_, AllocateNamedTypedMemoryAtOffset(::testing::_, ::testing::_, 0U, ::testing::_)) + .WillOnce(::testing::Return(score::cpp::expected_blank{})); + EXPECT_CALL(*ivshmem_provider_raw_, + AllocateNamedTypedMemoryAtOffset(::testing::_, ::testing::_, 4096U, ::testing::_)) + .WillOnce(::testing::Return(score::cpp::expected_blank{})); + + EXPECT_CALL(gateway_core_mock_, ProvideService(::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + auto request = CreateMessageOfType(MessageType::kProvideServiceRequest); + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, PreCreateInterVmSharedMemoryReturnsEarlyWhenCtrlBindFails) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + // Both lookups happen before any allocation + EXPECT_CALL(*ivshmem_provider_raw_, LookupOffsetInDirectory(::testing::HasSubstr("/ctrl"))) + .WillOnce(::testing::Return(std::optional{0U})); + EXPECT_CALL(*ivshmem_provider_raw_, LookupOffsetInDirectory(::testing::HasSubstr("/data"))) + .WillOnce(::testing::Return(std::optional{4096U})); + + // CTRL allocation fails — PreCreateInterVmSharedMemory returns early, DATA allocation not attempted + EXPECT_CALL(*ivshmem_provider_raw_, AllocateNamedTypedMemoryAtOffset(::testing::_, ::testing::_, 0U, ::testing::_)) + .WillOnce(::testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOMEM)))); + + // ProvideService is still called after PreCreateInterVmSharedMemory returns + EXPECT_CALL(gateway_core_mock_, ProvideService(::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + auto request = CreateMessageOfType(MessageType::kProvideServiceRequest); + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, PreCreateInterVmSharedMemoryReturnsEarlyWhenDataBindFails) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + EXPECT_CALL(*ivshmem_provider_raw_, LookupOffsetInDirectory(::testing::HasSubstr("/ctrl"))) + .WillOnce(::testing::Return(std::optional{0U})); + EXPECT_CALL(*ivshmem_provider_raw_, LookupOffsetInDirectory(::testing::HasSubstr("/data"))) + .WillOnce(::testing::Return(std::optional{4096U})); + + // CTRL succeeds + EXPECT_CALL(*ivshmem_provider_raw_, AllocateNamedTypedMemoryAtOffset(::testing::_, ::testing::_, 0U, ::testing::_)) + .WillOnce(::testing::Return(score::cpp::expected_blank{})); + + // DATA allocation fails + EXPECT_CALL(*ivshmem_provider_raw_, + AllocateNamedTypedMemoryAtOffset(::testing::_, ::testing::_, 4096U, ::testing::_)) + .WillOnce(::testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOMEM)))); + + // ProvideService is still called after PreCreateInterVmSharedMemory returns + EXPECT_CALL(gateway_core_mock_, ProvideService(::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + auto request = CreateMessageOfType(MessageType::kProvideServiceRequest); + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, PreCreateInterVmSharedMemoryLogsWarningWhenCtrlNotFoundButSizeNonZero) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + // Both offsets not found in directory + EXPECT_CALL(*ivshmem_provider_raw_, LookupOffsetInDirectory(::testing::_)) + .WillRepeatedly(::testing::Return(std::nullopt)); + + // ProvideService should still be called (PreCreate logs warning but doesn't fail) + EXPECT_CALL(gateway_core_mock_, ProvideService(::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + // ProvideServiceRequest has non-zero sizes (1024 + 4096), so the "offset not found" warning path is taken + auto request = CreateMessageOfType(MessageType::kProvideServiceRequest); + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, PreCreateInterVmSharedMemorySkipsCtrlBindWhenSizeIsZero) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + // Create a ProvideServiceRequest with zero CTRL size + auto specifier = CreateValidInstanceSpecifier(); + auto request = std::make_unique(specifier, std::vector{}, 0U, 4096U); + + // CTRL offset not found, but size is 0 — no warning logged + EXPECT_CALL(*ivshmem_provider_raw_, LookupOffsetInDirectory(::testing::HasSubstr("/ctrl"))) + .WillOnce(::testing::Return(std::nullopt)); + // DATA offset not found, size is non-zero — warning logged + EXPECT_CALL(*ivshmem_provider_raw_, LookupOffsetInDirectory(::testing::HasSubstr("/data"))) + .WillOnce(::testing::Return(std::nullopt)); + + EXPECT_CALL(gateway_core_mock_, ProvideService(::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, PreCreateInterVmSharedMemorySkipsDataBindWhenSizeIsZero) +{ + this->WithAQemuHypervisorTransport().WithARegisteredOnSetupCallback(); + + // Create a ProvideServiceRequest with zero DATA size + auto specifier = CreateValidInstanceSpecifier(); + auto request = std::make_unique(specifier, std::vector{}, 1024U, 0U); + + // CTRL offset not found, size non-zero — warning + EXPECT_CALL(*ivshmem_provider_raw_, LookupOffsetInDirectory(::testing::HasSubstr("/ctrl"))) + .WillOnce(::testing::Return(std::nullopt)); + // DATA offset not found, but size is 0 — no warning + EXPECT_CALL(*ivshmem_provider_raw_, LookupOffsetInDirectory(::testing::HasSubstr("/data"))) + .WillOnce(::testing::Return(std::nullopt)); + + EXPECT_CALL(gateway_core_mock_, ProvideService(::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, PreCreateInterVmSharedMemoryLogsErrorAndReturnsEarlyWhenPathsEmpty) +{ + this->WithAQemuHypervisorTransportWithEmptyPathResolver().WithARegisteredOnSetupCallback(); + + // ivshmem provider must not be touched — we return early before reaching LookupOffsetInDirectory + EXPECT_CALL(*ivshmem_provider_raw_, LookupOffsetInDirectory(::testing::_)).Times(0); + + // HandleProvideServiceRequest calls ProvideService after PreCreateInterVmSharedMemory returns + EXPECT_CALL(gateway_core_mock_, ProvideService(::testing::_, ::testing::_)) + .WillOnce(::testing::Return(score::ResultBlank{})); + EXPECT_CALL(*bi_directional_transport_mock_, Shutdown()).Times(1); + + auto request = CreateMessageOfType(MessageType::kProvideServiceRequest); + captured_handler_(std::move(request)); +} + +TEST_F(QemuHypervisorTransportTest, ResolveInterVmShmPathsReturnsPathsWithInterVmPrefix) +{ + const auto specifier = CreateValidInstanceSpecifier(); + const auto paths = ResolveInterVmShmPaths(specifier); + + EXPECT_EQ(paths.control, "/intervm-shared-shmem/SpeedService/Instance42/ctrl"); + EXPECT_EQ(paths.data, "/intervm-shared-shmem/SpeedService/Instance42/data"); +} + +#if !defined(__QNXNTO__) +TEST_F(QemuHypervisorTransportTest, GetInterVmShmSizesReturnsZeroOnNonQnx) +{ + // On Linux host, GetInterVmShmSizes returns zero sizes (no shm_open support for intervm paths) + const auto specifier = CreateValidInstanceSpecifier(); + const auto sizes = GetInterVmShmSizes(specifier); + EXPECT_EQ(sizes.control, 0U); + EXPECT_EQ(sizes.data, 0U); +} +#endif // !defined(__QNXNTO__) + +#if defined(__QNXNTO__) +TEST_F(QemuHypervisorTransportTest, GetInterVmShmSizesWithMmanQnxHandlesSuccess) +{ + // Given a mock that returns valid fds for shm_open. + auto mman_mock = std::make_unique<::testing::StrictMock>(); + auto* const raw = mman_mock.get(); + + // When GetInterVmShmSizes opens the CTRL and DATA shm objects. + EXPECT_CALL( + *raw, + shm_open(::testing::StrEq("/intervm-shared-shmem/SpeedService/Instance42/ctrl"), ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(std::int32_t{10})); // Return a valid fd + EXPECT_CALL( + *raw, + shm_open(::testing::StrEq("/intervm-shared-shmem/SpeedService/Instance42/data"), ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(std::int32_t{11})); // Return a valid fd + + const auto specifier = CreateValidInstanceSpecifier(); + const auto sizes = GetInterVmShmSizes(specifier, raw); + + // Then the fstat path reports zero sizes for the fake fds. + EXPECT_EQ(sizes.control, 0U); + EXPECT_EQ(sizes.data, 0U); +} +#endif // defined(__QNXNTO__) + +} // namespace + +} // namespace score::mw::com::gateway diff --git a/score/mw/com/gateway/transport_layer/qemu/test/BUILD b/score/mw/com/gateway/transport_layer/qemu/test/BUILD new file mode 100644 index 000000000..265cdf28d --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/test/BUILD @@ -0,0 +1,57 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@rules_cc//cc:defs.bzl", "cc_binary") +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") +load("//score/mw/com/test:pkg_application.bzl", "pkg_application") + +cc_binary( + name = "app1", + srcs = ["app1_main.cpp"], + features = COMPILER_WARNING_FEATURES + ["aborts_upon_exception"], + target_compatible_with = ["@platforms//os:qnx"], + deps = [ + "//score/memory/shared", + "//score/mw/com/gateway/transport_layer/qemu:qemu_hypervisor_transport", + "//score/mw/com/gateway/transport_layer/qemu/ivshmem:ivshmem_bar_discovery", + "//score/mw/com/gateway/transport_layer/qemu/ivshmem:ivshmem_typed_memory_provider", + "//score/mw/com/gateway/transport_layer/sample/messages:gateway_messages", + "@score_baselibs//score/mw/log:console", + "@score_baselibs//score/result", + ], +) + +cc_binary( + name = "app2", + srcs = ["app2_main.cpp"], + features = COMPILER_WARNING_FEATURES + ["aborts_upon_exception"], + target_compatible_with = ["@platforms//os:qnx"], + deps = [ + "//score/memory/shared", + "//score/mw/com/gateway/transport_layer/qemu:qemu_hypervisor_transport", + "//score/mw/com/gateway/transport_layer/qemu/ivshmem:ivshmem_bar_discovery", + "//score/mw/com/gateway/transport_layer/qemu/ivshmem:ivshmem_typed_memory_provider", + "//score/mw/com/gateway/transport_layer/sample/messages:gateway_messages", + "@score_baselibs//score/mw/log:console", + "@score_baselibs//score/result", + ], +) + +pkg_application( + name = "qemu_transport_test_apps", + app_name = "qemu_transport_test", + bin = [ + ":app1", + ":app2", + ], + visibility = ["//score/mw/com/gateway/transport_layer/qemu/test:__subpackages__"], +) diff --git a/score/mw/com/gateway/transport_layer/qemu/test/app1_main.cpp b/score/mw/com/gateway/transport_layer/qemu/test/app1_main.cpp new file mode 100644 index 000000000..14afac141 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/test/app1_main.cpp @@ -0,0 +1,366 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +/// Gateway transport layer integration test — VM-A (bidirectional). +/// +/// Tests the full bidirectional gateway use case over ivshmem, using per-service +/// CTRL + DATA shared memory — matching the production LoLa pattern. +/// +/// - VM-A creates CTRL+DATA shm for service "service_a", writes payload, signals via CTRL +/// - VM-B creates CTRL+DATA shm for service "service_b", writes payload, signals via CTRL +/// - Each VM uses the transport to make peer's shm visible, then reads and verifies +/// +/// Synchronization uses each service's CTRL shm (like production LoLa's ServiceDataControl) +/// instead of a separate raw handshake page: +/// - Provider writes DATA, then sets ctrl->event_count to signal readiness +/// - Consumer polls peer's CTRL shm event_count until it becomes non-zero +/// +/// Both VMs run QemuHypervisorTransport. Each VM is simultaneously a source gateway +/// (for its own service) and a destination gateway (for the peer's service). + +#include "score/mw/com/gateway/gateway_application/gateway_core.h" +#include "score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery.h" +#include "score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider.h" +#include "score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport.h" +#include "score/mw/com/gateway/transport_layer/sample/i_bidirectional_transport.h" +#include "score/mw/com/gateway/transport_layer/sample/messages/gateway_messages.h" + +#include "score/memory/shared/i_shared_memory_resource.h" +#include "score/memory/shared/shared_memory_factory.h" +#include "score/result/result.h" + +#include +#include +#include +#include +#include +#include + +using score::mw::com::gateway::GatewayCore; +using score::mw::com::gateway::IBidirectionalTransport; +using score::mw::com::gateway::QemuHypervisorTransport; +using score::mw::com::gateway::ResolveInterVmShmPaths; +using score::mw::com::gateway::qemu::ivshmem::DiscoverIvshmemBar; +using score::mw::com::gateway::qemu::ivshmem::IvshmemTypedMemoryProvider; +using score::mw::com::impl::InstanceSpecifier; + +namespace +{ +constexpr std::uint32_t kShmSize = 4096U; + +// Payload magic values — different per direction to prove correct routing. +constexpr std::uint32_t kMagicA = 0xCAFEBABEU; // VM-A → VM-B +constexpr std::uint32_t kMagicB = 0xDEADBEEFU; // VM-B → VM-A + +// Service specifiers for each direction. +constexpr char kServiceA[] = "service_a"; // produced by VM-A, consumed on VM-B +constexpr char kServiceB[] = "service_b"; // produced by VM-B, consumed on VM-A + +/// Simplified control structure placed in each service's CTRL shm. +/// In production, this would be ServiceDataControl with EventControl/TransactionLogSet. +/// Here we use a minimal version: the skeleton writes data, then increments event_count. +/// The proxy polls event_count to know when data is ready. +struct ServiceControl +{ + volatile std::uint32_t event_count; // incremented by provider after writing DATA + volatile std::uint32_t verified; // set by consumer after successful read +}; + +/// Polls a volatile uint32 until it reaches the expected value or times out. +bool WaitForCtrl(const volatile std::uint32_t& field, std::uint32_t expected, int timeout_ms = 60000) +{ + constexpr int kSleepMs = 50; + int elapsed = 0; + while (field < expected && elapsed < timeout_ms) + { + std::this_thread::sleep_for(std::chrono::milliseconds(kSleepMs)); + elapsed += kSleepMs; + if (elapsed % 5000 == 0) + { + std::fprintf(stderr, + " [polling] elapsed %d/%d ms, current value=%u, expected=%u\n", + elapsed, + timeout_ms, + field, + expected); + std::fflush(stderr); + } + } + return field >= expected; +} + +/// Test IBidirectionalTransport stub — captures the message handler for injection. +class TestBidirectionalTransport final : public IBidirectionalTransport +{ + public: + score::Result Setup() override + { + return {}; + } + void Shutdown() override {} + bool IsConnected() const override + { + return true; + } + score::Result SendRequest(score::mw::com::gateway::TransportMessage& /*msg*/) override + { + return {}; + } + score::Result SendNotification(score::mw::com::gateway::TransportMessage& /*msg*/) override + { + return {}; + } + void SetMessageHandler(MessageHandler handler) override + { + handler_ = std::move(handler); + } + + void DeliverMessage(std::unique_ptr msg) + { + handler_(std::move(msg)); + } + + private: + MessageHandler handler_; +}; + +/// Test GatewayCore stub — records ProvideService calls. +class TestGatewayCore final : public GatewayCore +{ + public: + bool provide_service_called{false}; + + score::Result ProvideService(score::mw::com::impl::InstanceSpecifier /*s*/, + std::vector /*e*/) override + { + provide_service_called = true; + return {}; + } + score::Result OfferService(score::mw::com::impl::InstanceSpecifier /*s*/) override + { + return {}; + } + void StopOfferService(score::mw::com::impl::InstanceSpecifier /*s*/) override {} + score::Result NotifyUpdate(score::mw::com::impl::InstanceSpecifier /*s*/, + score::mw::com::impl::ServiceElementType /*t*/, + std::string /*n*/) override + { + return {}; + } + score::Result RegisterUpdateNotification(score::mw::com::impl::InstanceSpecifier /*s*/, + score::mw::com::impl::ServiceElementType /*t*/, + std::string /*n*/) override + { + return {}; + } + score::Result UnregisterUpdateNotification(score::mw::com::impl::InstanceSpecifier /*s*/, + score::mw::com::impl::ServiceElementType /*t*/, + std::string /*n*/) override + { + return {}; + } +}; + +} // namespace + +int main() +{ + std::fprintf(stderr, "=== app1 (VM-A): bidirectional gateway transport test ===\n"); + + // --- Setup: discover BAR, create provider, create transport --- + std::uint64_t paddr = 0U; + std::uint64_t size = 0U; + if (!DiscoverIvshmemBar(paddr, size)) + { + std::fprintf(stderr, "app1: failed to discover ivshmem BAR\n"); + return 1; + } + std::fprintf(stderr, + "app1: BAR paddr=0x%llx size=0x%llx\n", + static_cast(paddr), + static_cast(size)); + + // The entire BAR is usable for shm allocations (no reserved handshake page needed). + auto provider = std::make_shared(paddr, size); + score::memory::shared::SharedMemoryFactory::SetInterVMMemoryProvider(provider); + + TestGatewayCore gateway_core; + auto test_transport = std::make_unique(); + auto* test_transport_ptr = test_transport.get(); + QemuHypervisorTransport qemu_transport{gateway_core, std::move(test_transport), provider}; + if (!qemu_transport.Setup().has_value()) + { + std::fprintf(stderr, "app1: QemuHypervisorTransport::Setup failed\n"); + return 1; + } + + // ======================================================================== + // SOURCE SIDE: Create CTRL + DATA shm for service_a via SharedMemoryFactory::Create(). + // This is exactly what the LoLa skeleton does in production — the factory routes + // through our InterVM provider which allocates in the BAR directory. + // ======================================================================== + auto spec_a = InstanceSpecifier::Create(std::string{kServiceA}); + if (!spec_a.has_value()) + { + std::fprintf(stderr, "app1: failed to create InstanceSpecifier for service_a\n"); + return 1; + } + const auto paths_a = ResolveInterVmShmPaths(spec_a.value()); + + // Create CTRL shm for service_a — holds the ServiceControl signaling structure. + auto ctrl_a = score::memory::shared::SharedMemoryFactory::Create( + paths_a.control, + [](std::shared_ptr /*res*/) {}, + sizeof(ServiceControl), + score::memory::shared::SharedMemoryFactory::WorldWritable{}); + if (ctrl_a == nullptr) + { + std::fprintf(stderr, "app1: SharedMemoryFactory::Create for service_a CTRL failed\n"); + return 1; + } + + // Create DATA shm for service_a — holds the actual payload. + auto data_a = score::memory::shared::SharedMemoryFactory::Create( + paths_a.data, + [](std::shared_ptr /*res*/) {}, + kShmSize, + score::memory::shared::SharedMemoryFactory::WorldWritable{}); + if (data_a == nullptr) + { + std::fprintf(stderr, "app1: SharedMemoryFactory::Create for service_a DATA failed\n"); + return 1; + } + + // Initialize CTRL to zero (no events yet). + auto* ctrl_a_ptr = static_cast(ctrl_a->getUsableBaseAddress()); + ctrl_a_ptr->event_count = 0U; + ctrl_a_ptr->verified = 0U; + + // Write payload into DATA shm. + auto* write_data = static_cast(data_a->getUsableBaseAddress()); + write_data[0] = kMagicA; + write_data[1] = 100U; + write_data[2] = 200U; + + // Signal readiness via CTRL shm — like the skeleton updating EventDataControl. + std::atomic_thread_fence(std::memory_order_release); + ctrl_a_ptr->event_count = 1U; + std::fprintf(stderr, "app1: wrote service_a [magic=0x%08x, 100, 200] and signalled via CTRL\n", kMagicA); + + // ======================================================================== + // DESTINATION SIDE: Wait for service_b's CTRL to signal data is ready, then read. + // The transport must first make service_b's CTRL+DATA visible on this VM. + // ======================================================================== + auto spec_b = InstanceSpecifier::Create(std::string{kServiceB}); + if (!spec_b.has_value()) + { + std::fprintf(stderr, "app1: failed to create InstanceSpecifier for service_b\n"); + return 1; + } + const auto paths_b = ResolveInterVmShmPaths(spec_b.value()); + + // Poll: wait until service_b's CTRL shm appears in the BAR directory. + // This means VM-B has created it. In production, the gateway message (ProvideServiceRequest) + // arrives to trigger this, but here we simulate waiting for VM-B's allocation to appear. + std::fprintf(stderr, "app1: waiting for service_b CTRL to appear in BAR directory...\n"); + { + constexpr int kSleepMs = 50; + constexpr int kTimeoutMs = 60000; + int elapsed = 0; + while (!provider->LookupOffsetInDirectory(paths_b.control).has_value() && elapsed < kTimeoutMs) + { + std::this_thread::sleep_for(std::chrono::milliseconds(kSleepMs)); + elapsed += kSleepMs; + if (elapsed % 5000 == 0) + { + std::fprintf(stderr, "app1: still waiting for service_b CTRL... (%d/%d ms)\n", elapsed, kTimeoutMs); + std::fflush(stderr); + } + } + if (!provider->LookupOffsetInDirectory(paths_b.control).has_value()) + { + std::fprintf(stderr, "app1: timed out waiting for service_b CTRL in directory after %d ms\n", elapsed); + return 1; + } + } + std::fprintf(stderr, "app1: service_b CTRL found in BAR directory\n"); + + // Simulate receiving ProvideServiceRequest from VM-B's gateway. + // The transport binds service_b's CTRL+DATA to local shm names at the correct BAR offsets. + auto request_b = std::make_unique( + spec_b.value(), std::vector{}, sizeof(ServiceControl), kShmSize); + test_transport_ptr->DeliverMessage(std::move(request_b)); + + if (!gateway_core.provide_service_called) + { + std::fprintf(stderr, "app1: transport did not call GatewayCore::ProvideService for service_b\n"); + return 1; + } + std::fprintf(stderr, "app1: transport made service_b CTRL+DATA visible on this VM\n"); + + // Open service_b's CTRL shm and poll for readiness — like the proxy reading EventDataControl. + auto ctrl_b = score::memory::shared::SharedMemoryFactory::Open(paths_b.control, /*is_read_write=*/true); + if (ctrl_b == nullptr) + { + std::fprintf(stderr, "app1: Open for service_b CTRL failed\n"); + return 1; + } + auto* ctrl_b_ptr = static_cast(ctrl_b->getUsableBaseAddress()); + + std::fprintf(stderr, "app1: polling service_b CTRL for event_count >= 1...\n"); + if (!WaitForCtrl(ctrl_b_ptr->event_count, 1U)) + { + std::fprintf(stderr, + "app1: timed out waiting for service_b CTRL event_count (final value=%u)\n", + ctrl_b_ptr->event_count); + return 1; + } + std::atomic_thread_fence(std::memory_order_acquire); + std::fprintf(stderr, "app1: service_b event_count signal received\n"); + + // Open service_b's DATA shm and verify the payload. + auto data_b = score::memory::shared::SharedMemoryFactory::Open(paths_b.data, /*is_read_write=*/false); + if (data_b == nullptr) + { + std::fprintf(stderr, "app1: Open for service_b DATA failed\n"); + return 1; + } + + const auto* read_data = static_cast(data_b->getUsableBaseAddress()); + if (read_data[0] != kMagicB || read_data[1] != 300U || read_data[2] != 400U) + { + std::fprintf(stderr, + "app1: service_b verification FAILED (magic=0x%08x d[1]=%u d[2]=%u)\n", + read_data[0], + read_data[1], + read_data[2]); + return 1; + } + std::fprintf(stderr, "app1: service_b verified [magic=0x%08x, 300, 400] — read from VM-B OK\n", kMagicB); + + // Signal back to VM-B that we verified its data (write to service_b's CTRL verified field). + ctrl_b_ptr->verified = 1U; + + // Wait for VM-B to verify our data (polls our CTRL verified field). + std::fprintf(stderr, "app1: waiting for VM-B to verify service_a...\n"); + if (!WaitForCtrl(ctrl_a_ptr->verified, 1U)) + { + std::fprintf( + stderr, "app1: timed out waiting for VM-B to verify service_a (final verified=%u)\n", ctrl_a_ptr->verified); + return 1; + } + std::fprintf(stderr, "app1: VM-B verified service_a successfully!\n"); + + std::fprintf(stderr, "app1: both directions verified successfully!\n"); + std::printf("verified\n"); + return 0; +} diff --git a/score/mw/com/gateway/transport_layer/qemu/test/app2_main.cpp b/score/mw/com/gateway/transport_layer/qemu/test/app2_main.cpp new file mode 100644 index 000000000..45e94189c --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/test/app2_main.cpp @@ -0,0 +1,364 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +/// Gateway transport layer integration test — VM-B (bidirectional). +/// +/// Tests the full bidirectional gateway use case over ivshmem, using per-service +/// CTRL + DATA shared memory — matching the production LoLa pattern. +/// +/// - VM-A creates CTRL+DATA shm for service "service_a", writes payload, signals via CTRL +/// - VM-B creates CTRL+DATA shm for service "service_b", writes payload, signals via CTRL +/// - Each VM uses the transport to make peer's shm visible, then reads and verifies +/// +/// Synchronization uses each service's CTRL shm (like production LoLa's ServiceDataControl) +/// instead of a separate raw handshake page: +/// - Provider writes DATA, then sets ctrl->event_count to signal readiness +/// - Consumer polls peer's CTRL shm event_count until it becomes non-zero +/// +/// Both VMs run QemuHypervisorTransport. Each VM is simultaneously a source gateway +/// (for its own service) and a destination gateway (for the peer's service). + +#include "score/mw/com/gateway/gateway_application/gateway_core.h" +#include "score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery.h" +#include "score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider.h" +#include "score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport.h" +#include "score/mw/com/gateway/transport_layer/sample/i_bidirectional_transport.h" +#include "score/mw/com/gateway/transport_layer/sample/messages/gateway_messages.h" + +#include "score/memory/shared/i_shared_memory_resource.h" +#include "score/memory/shared/shared_memory_factory.h" +#include "score/result/result.h" + +#include +#include +#include +#include +#include +#include + +using score::mw::com::gateway::GatewayCore; +using score::mw::com::gateway::IBidirectionalTransport; +using score::mw::com::gateway::QemuHypervisorTransport; +using score::mw::com::gateway::ResolveInterVmShmPaths; +using score::mw::com::gateway::qemu::ivshmem::DiscoverIvshmemBar; +using score::mw::com::gateway::qemu::ivshmem::IvshmemTypedMemoryProvider; +using score::mw::com::impl::InstanceSpecifier; + +namespace +{ +constexpr std::uint32_t kShmSize = 4096U; + +// Payload magic values — different per direction to prove correct routing. +constexpr std::uint32_t kMagicA = 0xCAFEBABEU; // VM-A → VM-B +constexpr std::uint32_t kMagicB = 0xDEADBEEFU; // VM-B → VM-A + +// Service specifiers for each direction. +constexpr char kServiceA[] = "service_a"; // produced by VM-A, consumed on VM-B +constexpr char kServiceB[] = "service_b"; // produced by VM-B, consumed on VM-A + +/// Simplified control structure placed in each service's CTRL shm. +/// In production, this would be ServiceDataControl with EventControl/TransactionLogSet. +/// Here we use a minimal version: the skeleton writes data, then increments event_count. +/// The proxy polls event_count to know when data is ready. +struct ServiceControl +{ + volatile std::uint32_t event_count; // incremented by provider after writing DATA + volatile std::uint32_t verified; // set by consumer after successful read +}; + +/// Polls a volatile uint32 until it reaches the expected value or times out. +bool WaitForCtrl(const volatile std::uint32_t& field, std::uint32_t expected, int timeout_ms = 60000) +{ + constexpr int kSleepMs = 50; + int elapsed = 0; + while (field < expected && elapsed < timeout_ms) + { + std::this_thread::sleep_for(std::chrono::milliseconds(kSleepMs)); + elapsed += kSleepMs; + if (elapsed % 5000 == 0) + { + std::fprintf(stderr, + " [polling] elapsed %d/%d ms, current value=%u, expected=%u\n", + elapsed, + timeout_ms, + field, + expected); + std::fflush(stderr); + } + } + return field >= expected; +} + +/// Test IBidirectionalTransport stub — captures the message handler for injection. +class TestBidirectionalTransport final : public IBidirectionalTransport +{ + public: + score::Result Setup() override + { + return {}; + } + void Shutdown() override {} + bool IsConnected() const override + { + return true; + } + score::Result SendRequest(score::mw::com::gateway::TransportMessage& /*msg*/) override + { + return {}; + } + score::Result SendNotification(score::mw::com::gateway::TransportMessage& /*msg*/) override + { + return {}; + } + void SetMessageHandler(MessageHandler handler) override + { + handler_ = std::move(handler); + } + + void DeliverMessage(std::unique_ptr msg) + { + handler_(std::move(msg)); + } + + private: + MessageHandler handler_; +}; + +/// Test GatewayCore stub — records ProvideService calls. +class TestGatewayCore final : public GatewayCore +{ + public: + bool provide_service_called{false}; + + score::Result ProvideService(score::mw::com::impl::InstanceSpecifier /*s*/, + std::vector /*e*/) override + { + provide_service_called = true; + return {}; + } + score::Result OfferService(score::mw::com::impl::InstanceSpecifier /*s*/) override + { + return {}; + } + void StopOfferService(score::mw::com::impl::InstanceSpecifier /*s*/) override {} + score::Result NotifyUpdate(score::mw::com::impl::InstanceSpecifier /*s*/, + score::mw::com::impl::ServiceElementType /*t*/, + std::string /*n*/) override + { + return {}; + } + score::Result RegisterUpdateNotification(score::mw::com::impl::InstanceSpecifier /*s*/, + score::mw::com::impl::ServiceElementType /*t*/, + std::string /*n*/) override + { + return {}; + } + score::Result UnregisterUpdateNotification(score::mw::com::impl::InstanceSpecifier /*s*/, + score::mw::com::impl::ServiceElementType /*t*/, + std::string /*n*/) override + { + return {}; + } +}; + +} // namespace + +int main() +{ + std::fprintf(stderr, "=== app2 (VM-B): bidirectional gateway transport test ===\n"); + + // --- Setup: discover BAR, create provider, create transport --- + std::uint64_t paddr = 0U; + std::uint64_t size = 0U; + if (!DiscoverIvshmemBar(paddr, size)) + { + std::fprintf(stderr, "app2: failed to discover ivshmem BAR\n"); + return 1; + } + std::fprintf(stderr, + "app2: BAR paddr=0x%llx size=0x%llx\n", + static_cast(paddr), + static_cast(size)); + + // The entire BAR is usable for shm allocations (no reserved handshake page needed). + auto provider = std::make_shared(paddr, size); + score::memory::shared::SharedMemoryFactory::SetInterVMMemoryProvider(provider); + + TestGatewayCore gateway_core; + auto test_transport = std::make_unique(); + auto* test_transport_ptr = test_transport.get(); + QemuHypervisorTransport qemu_transport{gateway_core, std::move(test_transport), provider}; + if (!qemu_transport.Setup().has_value()) + { + std::fprintf(stderr, "app2: QemuHypervisorTransport::Setup failed\n"); + return 1; + } + + // ======================================================================== + // DESTINATION SIDE: Wait for service_a's CTRL to appear, then read and verify. + // The transport must first make service_a's CTRL+DATA visible on this VM. + // ======================================================================== + auto spec_a = InstanceSpecifier::Create(std::string{kServiceA}); + if (!spec_a.has_value()) + { + std::fprintf(stderr, "app2: failed to create InstanceSpecifier for service_a\n"); + return 1; + } + const auto paths_a = ResolveInterVmShmPaths(spec_a.value()); + + // Poll: wait until service_a's CTRL shm appears in the BAR directory. + // This means VM-A has created it. + std::fprintf(stderr, "app2: waiting for service_a CTRL to appear in BAR directory...\n"); + { + constexpr int kSleepMs = 50; + constexpr int kTimeoutMs = 60000; + int elapsed = 0; + while (!provider->LookupOffsetInDirectory(paths_a.control).has_value() && elapsed < kTimeoutMs) + { + std::this_thread::sleep_for(std::chrono::milliseconds(kSleepMs)); + elapsed += kSleepMs; + if (elapsed % 5000 == 0) + { + std::fprintf(stderr, "app2: still waiting for service_a CTRL... (%d/%d ms)\n", elapsed, kTimeoutMs); + std::fflush(stderr); + } + } + if (!provider->LookupOffsetInDirectory(paths_a.control).has_value()) + { + std::fprintf(stderr, "app2: timed out waiting for service_a CTRL in directory after %d ms\n", elapsed); + return 1; + } + } + std::fprintf(stderr, "app2: service_a CTRL found in BAR directory\n"); + + // Simulate receiving ProvideServiceRequest from VM-A's gateway. + // The transport binds service_a's CTRL+DATA to local shm names at the correct BAR offsets. + auto request_a = std::make_unique( + spec_a.value(), std::vector{}, sizeof(ServiceControl), kShmSize); + test_transport_ptr->DeliverMessage(std::move(request_a)); + + if (!gateway_core.provide_service_called) + { + std::fprintf(stderr, "app2: transport did not call GatewayCore::ProvideService for service_a\n"); + return 1; + } + std::fprintf(stderr, "app2: transport made service_a CTRL+DATA visible on this VM\n"); + + // Open service_a's CTRL shm and poll for readiness — like the proxy reading EventDataControl. + auto ctrl_a = score::memory::shared::SharedMemoryFactory::Open(paths_a.control, /*is_read_write=*/true); + if (ctrl_a == nullptr) + { + std::fprintf(stderr, "app2: Open for service_a CTRL failed\n"); + return 1; + } + auto* ctrl_a_ptr = static_cast(ctrl_a->getUsableBaseAddress()); + + std::fprintf(stderr, "app2: polling service_a CTRL for event_count >= 1...\n"); + if (!WaitForCtrl(ctrl_a_ptr->event_count, 1U)) + { + std::fprintf(stderr, + "app2: timed out waiting for service_a CTRL event_count (final value=%u)\n", + ctrl_a_ptr->event_count); + return 1; + } + std::atomic_thread_fence(std::memory_order_acquire); + std::fprintf(stderr, "app2: service_a event_count signal received\n"); + + // Open service_a's DATA shm and verify the payload. + auto data_a = score::memory::shared::SharedMemoryFactory::Open(paths_a.data, /*is_read_write=*/false); + if (data_a == nullptr) + { + std::fprintf(stderr, "app2: Open for service_a DATA failed\n"); + return 1; + } + + const auto* read_data = static_cast(data_a->getUsableBaseAddress()); + if (read_data[0] != kMagicA || read_data[1] != 100U || read_data[2] != 200U) + { + std::fprintf(stderr, + "app2: service_a verification FAILED (magic=0x%08x d[1]=%u d[2]=%u)\n", + read_data[0], + read_data[1], + read_data[2]); + return 1; + } + std::fprintf(stderr, "app2: service_a verified [magic=0x%08x, 100, 200] — read from VM-A OK\n", kMagicA); + + // Signal back to VM-A that we verified its data (via its CTRL shm verified field). + ctrl_a_ptr->verified = 1U; + + // ======================================================================== + // SOURCE SIDE: Create CTRL + DATA shm for service_b via SharedMemoryFactory::Create(). + // This is exactly what the LoLa skeleton does in production. + // ======================================================================== + auto spec_b = InstanceSpecifier::Create(std::string{kServiceB}); + if (!spec_b.has_value()) + { + std::fprintf(stderr, "app2: failed to create InstanceSpecifier for service_b\n"); + return 1; + } + const auto paths_b = ResolveInterVmShmPaths(spec_b.value()); + + // Create CTRL shm for service_b — holds the ServiceControl signaling structure. + auto ctrl_b = score::memory::shared::SharedMemoryFactory::Create( + paths_b.control, + [](std::shared_ptr /*res*/) {}, + sizeof(ServiceControl), + score::memory::shared::SharedMemoryFactory::WorldWritable{}); + if (ctrl_b == nullptr) + { + std::fprintf(stderr, "app2: SharedMemoryFactory::Create for service_b CTRL failed\n"); + return 1; + } + + // Create DATA shm for service_b — holds the actual payload. + auto data_b = score::memory::shared::SharedMemoryFactory::Create( + paths_b.data, + [](std::shared_ptr /*res*/) {}, + kShmSize, + score::memory::shared::SharedMemoryFactory::WorldWritable{}); + if (data_b == nullptr) + { + std::fprintf(stderr, "app2: SharedMemoryFactory::Create for service_b DATA failed\n"); + return 1; + } + + // Initialize CTRL to zero (no events yet). + auto* ctrl_b_ptr = static_cast(ctrl_b->getUsableBaseAddress()); + ctrl_b_ptr->event_count = 0U; + ctrl_b_ptr->verified = 0U; + + // Write payload into DATA shm. + auto* write_data = static_cast(data_b->getUsableBaseAddress()); + write_data[0] = kMagicB; + write_data[1] = 300U; + write_data[2] = 400U; + + // Signal readiness via CTRL shm — like the skeleton updating EventDataControl. + std::atomic_thread_fence(std::memory_order_release); + ctrl_b_ptr->event_count = 1U; + std::fprintf(stderr, "app2: wrote service_b [magic=0x%08x, 300, 400] and signalled via CTRL\n", kMagicB); + + // Wait for VM-A to verify our data (polls our CTRL verified field). + std::fprintf(stderr, "app2: waiting for VM-A to verify service_b...\n"); + if (!WaitForCtrl(ctrl_b_ptr->verified, 1U)) + { + std::fprintf( + stderr, "app2: timed out waiting for VM-A to verify service_b (final verified=%u)\n", ctrl_b_ptr->verified); + return 1; + } + std::fprintf(stderr, "app2: VM-A verified service_b successfully!\n"); + + std::fprintf(stderr, "app2: both directions verified successfully!\n"); + std::printf("verified\n"); + return 0; +} diff --git a/score/mw/com/gateway/transport_layer/qemu/test/integration_test/BUILD b/score/mw/com/gateway/transport_layer/qemu/test/integration_test/BUILD new file mode 100644 index 000000000..063140907 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/test/integration_test/BUILD @@ -0,0 +1,23 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("//quality/integration_testing:integration_testing.bzl", "dual_qemu_integration_test") + +exports_files(["dual_qemu_transport_config.json"]) + +dual_qemu_integration_test( + name = "test_qemu_transport", + srcs = ["test_qemu_transport.py"], + dual_config = "dual_qemu_transport_config.json", + filesystem_a = "//score/mw/com/gateway/transport_layer/qemu/test:qemu_transport_test_apps", + filesystem_b = "//score/mw/com/gateway/transport_layer/qemu/test:qemu_transport_test_apps", +) diff --git a/score/mw/com/gateway/transport_layer/qemu/test/integration_test/dual_qemu_transport_config.json b/score/mw/com/gateway/transport_layer/qemu/test/integration_test/dual_qemu_transport_config.json new file mode 100644 index 000000000..525a59c9a --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/test/integration_test/dual_qemu_transport_config.json @@ -0,0 +1,28 @@ +{ + "ivshmem": { "size": "4M" }, + "intervm_network": { "enabled": false, "host_port": 12345 }, + "vms": [ + { + "networks": [ + { "name": "vm_a_net", "ip_address": "127.0.0.1", "gateway": "127.0.0.1" } + ], + "ssh_port": 2222, + "qemu_num_cores": "1", + "qemu_ram_size": "1G", + "port_forwarding": [ + { "host_port": 2222, "guest_port": 22 } + ] + }, + { + "networks": [ + { "name": "vm_b_net", "ip_address": "127.0.0.1", "gateway": "127.0.0.1" } + ], + "ssh_port": 2223, + "qemu_num_cores": "1", + "qemu_ram_size": "1G", + "port_forwarding": [ + { "host_port": 2223, "guest_port": 22 } + ] + } + ] +} diff --git a/score/mw/com/gateway/transport_layer/qemu/test/integration_test/test_qemu_transport.py b/score/mw/com/gateway/transport_layer/qemu/test/integration_test/test_qemu_transport.py new file mode 100644 index 000000000..365bcaa85 --- /dev/null +++ b/score/mw/com/gateway/transport_layer/qemu/test/integration_test/test_qemu_transport.py @@ -0,0 +1,72 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""Bidirectional integration test for the QEMU ivshmem gateway transport layer. + +Both VMs run QemuHypervisorTransport simultaneously as source and destination: + - VM-A creates shm for service_a (payload [0xCAFEBABE, 100, 200]) → VM-B reads + - VM-B creates shm for service_b (payload [0xDEADBEEF, 300, 400]) → VM-A reads + +The handshake uses the last page of the ivshmem BAR with a phased protocol. +Both apps print "verified" on success. +""" +import logging +import threading +import time + +logger = logging.getLogger(__name__) + +APP1 = "/opt/qemu_transport_test/bin/app1" +APP2 = "/opt/qemu_transport_test/bin/app2" +VM_A_LABEL = "VM-A (src)" +VM_B_LABEL = "VM-B (dest)" +TIMEOUT_SECONDS = 180 + + +def _run(target, label, app_path, results): + rc, out = target.execute(app_path) + text = out.decode(errors="replace").strip() + logger.info("==================== %s ====================", label) + logger.info("%s (rc=%s)", text, rc) + print(f"\n[{label}] {text} (rc={rc})") + results[label] = (rc, text) + + +def test_qemu_ivshmem_transport(target_a, target_b): + """Bidirectional: VM-A writes service_a and reads service_b; VM-B writes service_b and reads service_a.""" + results = {} + threads = [ + threading.Thread(target=_run, args=(target_a, VM_A_LABEL, APP1, results), name=VM_A_LABEL), + threading.Thread(target=_run, args=(target_b, VM_B_LABEL, APP2, results), name=VM_B_LABEL), + ] + for t in threads: + t.start() + deadline = time.monotonic() + TIMEOUT_SECONDS + for t in threads: + remaining = deadline - time.monotonic() + if remaining > 0: + t.join(timeout=remaining) + alive_threads = [t.name for t in threads if t.is_alive()] + if alive_threads: + raise TimeoutError(f"Timed out after {TIMEOUT_SECONDS}s waiting for: {alive_threads}") + + missing_results = [label for label in [VM_A_LABEL, VM_B_LABEL] if label not in results] + if missing_results: + raise AssertionError(f"Missing test results for: {missing_results}") + + rc_a, text_a = results[VM_A_LABEL] + rc_b, text_b = results[VM_B_LABEL] + + assert rc_a == 0, f"source (VM-A) failed (rc={rc_a}): {text_a}" + assert rc_b == 0, f"destination (VM-B) failed (rc={rc_b}): {text_b}" + assert "verified" in text_a, f"source did not verify: {text_a!r}" + assert "verified" in text_b, f"destination did not verify: {text_b!r}" diff --git a/score/mw/com/gateway/transport_layer/transport_factory.cpp b/score/mw/com/gateway/transport_layer/transport_factory.cpp index 83037f159..de494e596 100644 --- a/score/mw/com/gateway/transport_layer/transport_factory.cpp +++ b/score/mw/com/gateway/transport_layer/transport_factory.cpp @@ -12,28 +12,65 @@ *******************************************************************************/ #include "score/mw/com/gateway/transport_layer/transport_factory.h" +#include "score/memory/shared/shared_memory_factory.h" +#include "score/mw/com/gateway/transport_layer/qemu/configuration/qemu_transport_config_parser.h" +#include "score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_bar_discovery.h" +#include "score/mw/com/gateway/transport_layer/qemu/ivshmem/ivshmem_typed_memory_provider.h" +#include "score/mw/com/gateway/transport_layer/qemu/qemu_hypervisor_transport.h" #include "score/mw/com/gateway/transport_layer/sample/bidirectional_transport.h" #include "score/mw/com/gateway/transport_layer/sample/configuration/sample_transport_config_parser.h" #include "score/mw/com/gateway/transport_layer/sample/sample_hypervisor_transport.h" +#include #include #include +#include namespace score::mw::com::gateway { +namespace +{ + +constexpr std::string_view kSampleHypervisorTransportId{"sample_hypervisor"}; +constexpr std::string_view kQemuHypervisorTransportId{"qemu_hypervisor"}; + +std::unique_ptr CreateBidirectionalTransport(const std::string& transport_config_path) +{ + return std::make_unique(ParseSampleTransportConfig(transport_config_path)); +} + +std::shared_ptr CreateIvshmemProvider(std::uint32_t preferred_bar_num) +{ + std::uint64_t paddr = 0U; + std::uint64_t size = 0U; + if (!qemu::ivshmem::DiscoverIvshmemBar(paddr, size, preferred_bar_num)) + { + std::terminate(); + } + + return std::make_shared(paddr, size); +} + +} // namespace std::unique_ptr TransportFactory::Create(GatewayCore& gateway_core, const std::string& transport_layer_id, const std::string& transport_config_path) { - if (transport_layer_id == "sample_hypervisor") + if (transport_layer_id == kSampleHypervisorTransportId) + { + return std::make_unique(gateway_core, + CreateBidirectionalTransport(transport_config_path)); + } + + if (transport_layer_id == kQemuHypervisorTransportId) { - // Parse sample-hypervisor-specific transport configuration - const HyperVisorSocketConfiguration socket_cfg = ParseSampleTransportConfig(transport_config_path); - (void)socket_cfg; + const auto qemu_config = qemu::ParseQemuTransportConfig(transport_config_path); + auto ivshmem_provider = CreateIvshmemProvider(qemu_config.GetPreferredIvshmemBarNum()); + score::memory::shared::SharedMemoryFactory::SetInterVMMemoryProvider(ivshmem_provider); - auto bidirectional = std::make_unique(socket_cfg); - return std::make_unique(gateway_core, std::move(bidirectional)); + return std::make_unique( + gateway_core, CreateBidirectionalTransport(transport_config_path), std::move(ivshmem_provider)); } // Unknown transport layer id — no implementation registered. From 1db3cefd9751a512d363e2b9d1a344b69182815d Mon Sep 17 00:00:00 2001 From: Rahul-Sutariya Date: Thu, 13 Aug 2026 17:39:33 +0530 Subject: [PATCH 2/2] dual_qemu: increase timeout for TCG based environment --- .../environments/dual_qemu/dual_qemu_process.py | 5 ++++- quality/integration_testing/integration_testing.bzl | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/quality/integration_testing/environments/dual_qemu/dual_qemu_process.py b/quality/integration_testing/environments/dual_qemu/dual_qemu_process.py index 78a976d8f..6453d5e54 100644 --- a/quality/integration_testing/environments/dual_qemu/dual_qemu_process.py +++ b/quality/integration_testing/environments/dual_qemu/dual_qemu_process.py @@ -78,7 +78,7 @@ def __init__( intervm=None, vm_index=0, max_boot_attempts=3, - boot_timeout=100, + boot_timeout=180, ): super().__init__(path_to_qemu_image, available_ram, available_cores, port_forwarding=port_forwarding) # Replace the base's default Qemu with our ivshmem-capable subclass. @@ -119,6 +119,9 @@ def start(self): self.stop() except Exception: # pylint: disable=broad-except logger.exception("Failed to stop the wedged QEMU before retrying") + if attempt < self._max_boot_attempts: + logger.info("Waiting 5 s before next boot attempt to let resources settle") + time.sleep(5) raise RuntimeError( f"VM never booted into a usable state after {self._max_boot_attempts} attempts: {last_error}" ) diff --git a/quality/integration_testing/integration_testing.bzl b/quality/integration_testing/integration_testing.bzl index 5e8f70d19..33bdab176 100644 --- a/quality/integration_testing/integration_testing.bzl +++ b/quality/integration_testing/integration_testing.bzl @@ -219,8 +219,11 @@ def dual_qemu_integration_test( # Two VMs require even more resources than a single one. if "size" not in kwargs: kwargs["size"] = "enormous" + + # Dual-QEMU boots two guests and can fall back to slower TCG in CI, so give + # it more wall-clock budget than a single-VM integration test. if "timeout" not in kwargs: - kwargs["timeout"] = "moderate" + kwargs["timeout"] = "long" # Driving two real QNX guests under KVM has rare, environment-induced boot # nondeterminism (e.g. a guest occasionally wedging during device bring-up).