Commit 0584091e76 for openssl.org
commit 0584091e765404434f8c5e449143a870dc026519
Author: Andrew Dinh <andrewd@openssl.org>
Date: Wed Jul 8 13:30:01 2026 +0700
Port script_21
Assisted-by: Claude:claude-sonnet-5
Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
Reviewed-by: Bob Beck <beck@openssl.org>
MergeDate: Thu Jul 30 06:12:40 2026
(Merged from https://github.com/openssl/openssl/pull/32003)
diff --git a/test/quic_multistream_test.c b/test/quic_multistream_test.c
index 8e74e83a70..ed124a5e46 100644
--- a/test/quic_multistream_test.c
+++ b/test/quic_multistream_test.c
@@ -2196,20 +2196,7 @@ err:
}
static const struct script_op script_21[] = {
- OP_S_SET_INJECT_PLAIN(script_21_inject_plain),
- OP_C_SET_ALPN("ossltest"),
- OP_C_CONNECT_WAIT(),
-
- OP_C_WRITE(DEFAULT, "apple", 5),
- OP_S_BIND_STREAM_ID(a, C_BIDI_ID(0)),
- OP_S_READ_EXPECT(a, "apple", 5),
-
- OP_SET_INJECT_WORD(QUIC_PKT_TYPE_1RTT, OSSL_QUIC_VLINT_MAX),
-
- OP_S_WRITE(a, "orange", 6),
-
- OP_C_EXPECT_CONN_CLOSE_INFO(OSSL_QUIC_ERR_FRAME_ENCODING_ERROR, 0, 0),
-
+ /* test moved to test/radix/quic_tests.c */
OP_END
};
diff --git a/test/radix/quic_ops.c b/test/radix/quic_ops.c
index 6d7daa0148..f7f11bd3b9 100644
--- a/test/radix/quic_ops.c
+++ b/test/radix/quic_ops.c
@@ -1107,6 +1107,198 @@ err:
return ok;
}
+/*
+ * Fault injection: intercepts a QUIC channel's outgoing packet in plaintext,
+ * before it is encrypted, so a script can tamper with its frames.
+ */
+typedef struct radix_fault_st RADIX_FAULT;
+
+typedef int (*radix_fault_plain_cb)(RADIX_FAULT *fault, QUIC_PKT_HDR *hdr,
+ unsigned char *buf, size_t len);
+
+static ossl_inline void *radix_fault_plain_cb_to_ptr(radix_fault_plain_cb cb)
+{
+ union {
+ radix_fault_plain_cb cb;
+ void *ptr;
+ } u;
+
+ u.cb = cb;
+ return u.ptr;
+}
+
+static ossl_inline radix_fault_plain_cb radix_fault_ptr_to_plain_cb(void *ptr)
+{
+ union {
+ radix_fault_plain_cb cb;
+ void *ptr;
+ } u;
+
+ u.ptr = ptr;
+ return u.cb;
+}
+
+struct radix_fault_st {
+ QUIC_PKT_HDR hdr;
+ OSSL_QTX_IOVEC io;
+ size_t buf_alloc;
+ radix_fault_plain_cb cb;
+ uint64_t word0, word1;
+};
+
+/* Fault injection against one channel at a time. */
+static RADIX_FAULT radix_fault;
+
+static int radix_fault_mutate(const QUIC_PKT_HDR *hdrin,
+ const OSSL_QTX_IOVEC *iovecin, size_t numin,
+ QUIC_PKT_HDR **hdrout,
+ const OSSL_QTX_IOVEC **iovecout,
+ size_t *numout,
+ void *arg)
+{
+ RADIX_FAULT *fault = arg;
+ size_t i, bufsz = 0;
+ unsigned char *cur;
+ int grow_allowance;
+
+ for (i = 0; i < numin; i++)
+ bufsz += iovecin[i].buf_len;
+
+ fault->io.buf_len = bufsz;
+
+ /*
+ * 1200 is the length of the QUIC payload used by the record layer, bufsz is
+ * what we got from the txp, 16 is the AEAD tag length and 14 is the
+ * long header allowance (assume zero token length).
+ */
+ grow_allowance = 1200 - (int)bufsz - 16 - 14;
+ grow_allowance -= hdrin->dst_conn_id.id_len;
+ grow_allowance -= hdrin->src_conn_id.id_len;
+ if (!TEST_int_ge(grow_allowance, 0))
+ return 0;
+ bufsz += grow_allowance;
+
+ OPENSSL_free((unsigned char *)fault->io.buf);
+ fault->io.buf = cur = OPENSSL_malloc(bufsz);
+ if (cur == NULL) {
+ fault->io.buf_len = 0;
+ fault->buf_alloc = 0;
+ return 0;
+ }
+ fault->buf_alloc = bufsz;
+
+ for (i = 0; i < numin; i++) {
+ memcpy(cur, iovecin[i].buf, iovecin[i].buf_len);
+ cur += iovecin[i].buf_len;
+ }
+
+ fault->hdr = *hdrin;
+
+ if (fault->cb != NULL
+ && !fault->cb(fault, &fault->hdr, (unsigned char *)fault->io.buf,
+ fault->io.buf_len))
+ return 0;
+
+ *hdrout = &fault->hdr;
+ *iovecout = &fault->io;
+ *numout = 1;
+
+ return 1;
+}
+
+static void radix_fault_finish(void *arg)
+{
+ RADIX_FAULT *fault = arg;
+
+ OPENSSL_free((unsigned char *)fault->io.buf);
+ fault->io.buf = NULL;
+ fault->io.buf_len = 0;
+ fault->buf_alloc = 0;
+}
+
+/* To be called from a radix_fault_plain_cb callback. */
+static int radix_fault_resize_plain_packet(RADIX_FAULT *fault, size_t newlen)
+{
+ unsigned char *buf;
+ size_t oldlen = fault->io.buf_len;
+
+ if (fault->buf_alloc == 0 || newlen > fault->buf_alloc)
+ return 0;
+
+ buf = (unsigned char *)fault->io.buf;
+
+ if (newlen > oldlen)
+ memset(buf + oldlen, 0, newlen - oldlen);
+
+ fault->io.buf_len = newlen;
+ fault->hdr.len = newlen;
+
+ return 1;
+}
+
+/*
+ * Prepend frame data into a packet. To be called from a
+ * radix_fault_plain_cb callback.
+ */
+static int radix_fault_prepend_frame(RADIX_FAULT *fault,
+ const unsigned char *frame, size_t frame_len)
+{
+ unsigned char *buf;
+ size_t old_len;
+
+ if (fault->buf_alloc == 0)
+ return 0;
+
+ /* Cast below is safe because we allocated the buffer. */
+ buf = (unsigned char *)fault->io.buf;
+ old_len = fault->io.buf_len;
+
+ if (!radix_fault_resize_plain_packet(fault, old_len + frame_len))
+ return 0;
+
+ memmove(buf + frame_len, buf, old_len);
+ memcpy(buf, frame, frame_len);
+
+ return 1;
+}
+
+DEF_FUNC(hf_set_inject_plain)
+{
+ int ok = 0;
+ SSL *ssl;
+ void *cbptr;
+ QUIC_CHANNEL *ch;
+
+ F_POP(cbptr);
+ REQUIRE_SSL(ssl);
+
+ if (!TEST_ptr(ch = ossl_quic_conn_get_channel(ssl)))
+ goto err;
+
+ OPENSSL_free((unsigned char *)radix_fault.io.buf);
+ memset(&radix_fault, 0, sizeof(radix_fault));
+ radix_fault.cb = radix_fault_ptr_to_plain_cb(cbptr);
+
+ if (!TEST_true(ossl_quic_channel_set_mutator(ch, radix_fault_mutate,
+ radix_fault_finish, &radix_fault)))
+ goto err;
+
+ ok = 1;
+err:
+ return ok;
+}
+
+DEF_FUNC(hf_set_inject_word)
+{
+ int ok = 0;
+
+ F_POP2(radix_fault.word0, radix_fault.word1);
+
+ ok = 1;
+err:
+ return ok;
+}
+
#define OP_UNBIND(name) \
(OP_PUSH_PZ(#name), \
OP_FUNC(hf_unbind))
@@ -1366,3 +1558,13 @@ err:
(OP_PUSH_U64(idx), \
OP_PUSH_U64(threshold), \
OP_FUNC(hf_wait_counter))
+
+#define OP_SET_INJECT_PLAIN(name, cb) \
+ (OP_SELECT_SSL(0, name), \
+ OP_PUSH_P(radix_fault_plain_cb_to_ptr(cb)), \
+ OP_FUNC(hf_set_inject_plain))
+
+#define OP_SET_INJECT_WORD(word0, word1) \
+ (OP_PUSH_U64(word0), \
+ OP_PUSH_U64(word1), \
+ OP_FUNC(hf_set_inject_word))
diff --git a/test/radix/quic_tests.c b/test/radix/quic_tests.c
index 40d3290adc..e213bfcad9 100644
--- a/test/radix/quic_tests.c
+++ b/test/radix/quic_tests.c
@@ -1745,8 +1745,106 @@ DEF_SCRIPT(script_20, "Multiple threads accept stream with socket forcibly close
OP_TRIGGER_COUNTER(1);
}
-DEF_SCRIPT(script_21, "place holder for multistrem script_21")
+/* 21. Fault injection - unknown frame in 1-RTT packet */
+static int script_21_inject_plain(RADIX_FAULT *fault, QUIC_PKT_HDR *hdr,
+ unsigned char *buf, size_t len)
{
+ int ok = 0;
+ WPACKET wpkt;
+ unsigned char frame_buf[21];
+ size_t written;
+
+ if (fault->word0 == 0 || hdr->type != fault->word0)
+ return 1;
+
+ if (!TEST_true(WPACKET_init_static_len(&wpkt, frame_buf,
+ sizeof(frame_buf), 0)))
+ return 0;
+
+ if (!TEST_true(WPACKET_quic_write_vlint(&wpkt, fault->word1)))
+ goto err;
+
+ switch (fault->word1) {
+ case OSSL_QUIC_FRAME_TYPE_PATH_CHALLENGE:
+ case OSSL_QUIC_FRAME_TYPE_PATH_RESPONSE:
+ case OSSL_QUIC_FRAME_TYPE_RETIRE_CONN_ID:
+ if (!TEST_true(WPACKET_put_bytes_u64(&wpkt, (uint64_t)0)))
+ goto err;
+ break;
+ case OSSL_QUIC_FRAME_TYPE_MAX_DATA:
+ case OSSL_QUIC_FRAME_TYPE_STREAMS_BLOCKED_UNI:
+ case OSSL_QUIC_FRAME_TYPE_STREAMS_BLOCKED_BIDI:
+ case OSSL_QUIC_FRAME_TYPE_MAX_STREAMS_BIDI:
+ case OSSL_QUIC_FRAME_TYPE_MAX_STREAMS_UNI:
+ case OSSL_QUIC_FRAME_TYPE_DATA_BLOCKED:
+ if (!TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)0)))
+ goto err;
+ break;
+ case OSSL_QUIC_FRAME_TYPE_STOP_SENDING:
+ case OSSL_QUIC_FRAME_TYPE_MAX_STREAM_DATA:
+ case OSSL_QUIC_FRAME_TYPE_STREAM_DATA_BLOCKED:
+ if (!TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)0))
+ || !TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)0)))
+ goto err;
+ break;
+ case OSSL_QUIC_FRAME_TYPE_STREAM:
+ case OSSL_QUIC_FRAME_TYPE_RESET_STREAM:
+ case OSSL_QUIC_FRAME_TYPE_CONN_CLOSE_APP:
+ if (!TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)0))
+ || !TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)0))
+ || !TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)0)))
+ goto err;
+ break;
+ case OSSL_QUIC_FRAME_TYPE_NEW_TOKEN:
+ if (!TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)1))
+ || !TEST_true(WPACKET_put_bytes_u8(&wpkt, (uint8_t)0)))
+ goto err;
+ break;
+ case OSSL_QUIC_FRAME_TYPE_NEW_CONN_ID:
+ /* seq number */
+ if (!TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)0))
+ /* retire prior to */
+ || !TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)0))
+ /* Connection id length, arbitrary at 1 bytes */
+ || !TEST_true(WPACKET_put_bytes_u8(&wpkt, (uint8_t)1))
+ /* The connection id */
+ || !TEST_true(WPACKET_put_bytes_u8(&wpkt, (uint8_t)0))
+ /* 16 bytes total for the stateless reset token */
+ || !TEST_true(WPACKET_memset(&wpkt, 0, 16)))
+ goto err;
+
+ break;
+ }
+
+ if (!TEST_true(WPACKET_get_total_written(&wpkt, &written))
+ || !radix_fault_prepend_frame(fault, frame_buf, written))
+ goto err;
+
+ ok = 1;
+err:
+ if (ok)
+ WPACKET_finish(&wpkt);
+ else
+ WPACKET_cleanup(&wpkt);
+ return ok;
+}
+
+DEF_SCRIPT(script_21, "Fault injection - unknown frame in 1-RTT packet")
+{
+ OP_SIMPLE_PAIR_CONN();
+ OP_ACCEPT_CONN_WAIT(L, S, 0);
+
+ OP_SET_INJECT_PLAIN(S, script_21_inject_plain);
+
+ OP_WRITE(C, "apple", 5);
+ OP_ACCEPT_STREAM_WAIT(S, Sa, 0);
+ OP_READ_EXPECT(Sa, "apple", 5);
+
+ OP_SET_INJECT_WORD(QUIC_PKT_TYPE_1RTT, OSSL_QUIC_VLINT_MAX);
+
+ OP_WRITE(Sa, "orange", 6);
+
+ OP_EXPECT_CONN_CLOSE_INFO(C, OSSL_QUIC_ERR_FRAME_ENCODING_ERROR, 0, 0);
}
DEF_SCRIPT(script_22, "place holder for multistrem script_22")