@@ -241,6 +241,29 @@ uint32_t add_started_qpair(int fd, uint32_t& seq) {
241241 return qid;
242242}
243243
244+ // Add + START a MM qpair with the given direction mask and keyhole aperture,
245+ // returning its qid.
246+ uint32_t add_started_qpair_aperture (int fd, uint32_t dir_mask, uint32_t aperture_size,
247+ uint32_t & seq) {
248+ slash_qdma_qpair_add add{};
249+ add.size = sizeof (add);
250+ add.mode = kQdmaQModeMm ;
251+ add.dir_mask = dir_mask;
252+ add.aperture_size = aperture_size;
253+ slash_sysemu_socket_header h{kSlashQdmaIoctlQpairAdd , seq++, 0 , 0 };
254+ std::span<const uint8_t > p (reinterpret_cast <const uint8_t *>(&add), sizeof (add));
255+ auto r = send_request (fd, h, p, {});
256+ EXPECT_TRUE (r.has_value ());
257+ EXPECT_EQ (0 , ret_of (r.value ()));
258+ slash_qdma_qpair_add a{};
259+ std::memcpy (&a, r.value ().payload .data (), sizeof (a));
260+ uint32_t qid = a.qid ;
261+ auto st = do_q_op (fd, qid, SLASH_QDMA_QUEUE_OP_START , seq++);
262+ EXPECT_TRUE (st.has_value ());
263+ EXPECT_EQ (0 , ret_of (st.value ()));
264+ return qid;
265+ }
266+
244267// Obtain a working XFER fd for a single started qpair; returns the fd.
245268UniqueFd open_xfer (int ctl_fd, uint32_t qid, uint32_t & seq) {
246269 auto r = do_get_fd (ctl_fd, qid, seq++);
@@ -857,6 +880,136 @@ TEST_F(QdmaSubsystemTest, TransferWithDeletedQpairEnodev) {
857880 EXPECT_EQ (-ENODEV , ret_of (r.value ()));
858881}
859882
883+ // ═════════════════════════════════════════════════════════════════════════════
884+ // QPAIR_ADD: keyhole aperture validation
885+ // ═════════════════════════════════════════════════════════════════════════════
886+
887+ TEST_F (QdmaSubsystemTest, QpairAddRejectsNonPowerOfTwoAperture) {
888+ auto sub = make_subsystem ();
889+ ASSERT_TRUE (sub->setup ().has_value ());
890+ UniqueFd c = connect_client (sock_path_);
891+ ASSERT_TRUE (static_cast <bool >(c));
892+
893+ slash_qdma_qpair_add add{};
894+ add.size = sizeof (add);
895+ add.mode = kQdmaQModeMm ;
896+ add.dir_mask = 0x1 ;
897+ add.aperture_size = 4097 ; // not a power of two
898+ slash_sysemu_socket_header h{kSlashQdmaIoctlQpairAdd , 1 , 0 , 0 };
899+ std::span<const uint8_t > p (reinterpret_cast <const uint8_t *>(&add), sizeof (add));
900+ auto r = send_request (c.get (), h, p, {});
901+ ASSERT_TRUE (r.has_value ());
902+ EXPECT_EQ (-EINVAL , ret_of (r.value ()));
903+ EXPECT_EQ (0u , sub->qpair_count ());
904+ }
905+
906+ TEST_F (QdmaSubsystemTest, QpairAddAcceptsPowerOfTwoAperture) {
907+ auto sub = make_subsystem ();
908+ ASSERT_TRUE (sub->setup ().has_value ());
909+ UniqueFd c = connect_client (sock_path_);
910+ ASSERT_TRUE (static_cast <bool >(c));
911+
912+ slash_qdma_qpair_add add{};
913+ add.size = sizeof (add);
914+ add.mode = kQdmaQModeMm ;
915+ add.dir_mask = 0x1 ;
916+ add.aperture_size = 4096 ; // power of two
917+ slash_sysemu_socket_header h{kSlashQdmaIoctlQpairAdd , 1 , 0 , 0 };
918+ std::span<const uint8_t > p (reinterpret_cast <const uint8_t *>(&add), sizeof (add));
919+ auto r = send_request (c.get (), h, p, {});
920+ ASSERT_TRUE (r.has_value ());
921+ EXPECT_EQ (0 , ret_of (r.value ()));
922+ EXPECT_EQ (1u , sub->qpair_count ());
923+ }
924+
925+ // ═════════════════════════════════════════════════════════════════════════════
926+ // TRANSFER: keyhole aperture wrapping
927+ // ═════════════════════════════════════════════════════════════════════════════
928+
929+ // H2C on a keyhole queue funnels the whole transfer through a fixed aperture
930+ // window: the endpoint address wraps at the aperture boundary, so the model only
931+ // retains the LAST window's worth of data and nothing lands beyond the window.
932+ TEST_F (QdmaSubsystemTest, TransferH2cKeyholeWrapsWithinAperture) {
933+ auto sub = make_subsystem ();
934+ ASSERT_TRUE (sub->setup ().has_value ());
935+ UniqueFd c = connect_client (sock_path_);
936+ ASSERT_TRUE (static_cast <bool >(c));
937+ uint32_t seq = 1 ;
938+
939+ constexpr uint32_t kAperture = 1024 ;
940+ uint32_t qid = add_started_qpair_aperture (c.get (), 0x1 /* H2C*/ , kAperture , seq);
941+ UniqueFd xfer = open_xfer (c.get (), qid, seq);
942+
943+ // 4 aperture windows of distinct bytes.
944+ const std::size_t len = 4 * kAperture ;
945+ std::vector<uint8_t > src (len);
946+ for (std::size_t i = 0 ; i < len; ++i) src[i] = static_cast <uint8_t >(i & 0xFF );
947+ UniqueFd buf = make_filled_buf (src);
948+
949+ slash_qdma_subxfer sx{};
950+ sx.qpair_index = 0 ;
951+ sx.direction = SLASH_QDMA_XFER_H2C ;
952+ sx.buf_fd = 0 ;
953+ sx.buf_offset = 0 ;
954+ sx.dev_addr = kDevAddr ; // aperture-aligned
955+ sx.length = len;
956+ auto r = do_transfer (xfer.get (), {sx}, {buf.get ()}, 800 );
957+ ASSERT_TRUE (r.has_value ());
958+ EXPECT_EQ (static_cast <int32_t >(len), ret_of (r.value ()));
959+
960+ // The window holds the final segment (src[3*kAperture ..]).
961+ for (std::size_t j = 0 ; j < kAperture ; ++j) {
962+ EXPECT_EQ (src[3 * kAperture + j], server_->peek (kDevAddr + j)) << " byte " << j;
963+ }
964+ // Nothing was written past the aperture window.
965+ EXPECT_EQ (0 , server_->peek (kDevAddr + kAperture ));
966+ EXPECT_EQ (0 , server_->peek (kDevAddr + 2 * kAperture ));
967+ }
968+
969+ // C2H on a keyhole queue reads the same aperture window repeatedly, so a transfer
970+ // longer than the aperture fills the host buffer with the window pattern tiled.
971+ TEST_F (QdmaSubsystemTest, TransferC2hKeyholeWrapsWithinAperture) {
972+ auto sub = make_subsystem ();
973+ ASSERT_TRUE (sub->setup ().has_value ());
974+ UniqueFd c = connect_client (sock_path_);
975+ ASSERT_TRUE (static_cast <bool >(c));
976+ uint32_t seq = 1 ;
977+
978+ constexpr uint32_t kAperture = 1024 ;
979+ uint32_t qid = add_started_qpair_aperture (c.get (), 0x2 /* C2H*/ , kAperture , seq);
980+ UniqueFd xfer = open_xfer (c.get (), qid, seq);
981+
982+ // Seed only the aperture window in the model.
983+ std::vector<uint8_t > window (kAperture );
984+ for (std::size_t i = 0 ; i < kAperture ; ++i) window[i] = static_cast <uint8_t >(0xA0 ^ (i & 0xFF ));
985+ server_->poke_buffer (kDevAddr , window);
986+
987+ const std::size_t len = 3 * kAperture ;
988+ UniqueFd buf (::memfd_create (" dst" , MFD_CLOEXEC ));
989+ ASSERT_TRUE (static_cast <bool >(buf));
990+ ASSERT_EQ (0 , ::ftruncate (buf.get (), static_cast <off_t >(len)));
991+
992+ slash_qdma_subxfer sx{};
993+ sx.qpair_index = 0 ;
994+ sx.direction = SLASH_QDMA_XFER_C2H ;
995+ sx.buf_fd = 0 ;
996+ sx.buf_offset = 0 ;
997+ sx.dev_addr = kDevAddr ;
998+ sx.length = len;
999+ auto r = do_transfer (xfer.get (), {sx}, {buf.get ()}, 900 );
1000+ ASSERT_TRUE (r.has_value ());
1001+ EXPECT_EQ (static_cast <int32_t >(len), ret_of (r.value ()));
1002+
1003+ std::vector<uint8_t > got (len);
1004+ ASSERT_EQ (static_cast <ssize_t >(len), ::pread (buf.get (), got.data (), len, 0 ));
1005+ for (std::size_t seg = 0 ; seg < 3 ; ++seg) {
1006+ for (std::size_t j = 0 ; j < kAperture ; ++j) {
1007+ EXPECT_EQ (window[j], got[seg * kAperture + j])
1008+ << " seg " << seg << " byte " << j;
1009+ }
1010+ }
1011+ }
1012+
8601013// ═════════════════════════════════════════════════════════════════════════════
8611014// FD-index resolution
8621015// ═════════════════════════════════════════════════════════════════════════════
0 commit comments