DPDK-dev Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [DPDK/cryptodev Bug 2084] crypto/openssl: 3DES-CTR reads past segment that ends at cipher offset
@ 2026-10-27 16:21 bugzilla
  0 siblings, 0 replies; only message in thread
From: bugzilla @ 2026-10-27 16:21 UTC (permalink / raw)
  To: dev

https://bugs.dpdk.org/show_bug.cgi?id=2084

            Bug ID: 2084
           Summary: crypto/openssl: 3DES-CTR reads past segment that ends
                    at cipher offset
           Product: DPDK
           Version: 26.11
          Hardware: All
                OS: All
            Status: UNCONFIRMED
          Severity: normal
          Priority: Normal
         Component: cryptodev
          Assignee: dev@dpdk.org
          Reporter: stephen@networkplumber.org
  Target Milestone: ---

Created attachment 432
  --> https://bugs.dpdk.org/attachment.cgi?id=432&action=edit
AI proposed patch

============================================================================
ANT-2026-CUYNXISN  [NOT SECURITY: ordinary bug]
crypto/openssl: 3DES-CTR reads past segment that ends at cipher offset
Location: drivers/crypto/openssl/rte_openssl_pmd.c:1205  in
process_openssl_cipher_des3ctr()
Commit:   9a45341f18a1ab47af7123e63d942b90c1b79f38
============================================================================


The crypto_openssl PMD returns wrong data for 3DES-CTR ops that use a chained
source mbuf and a contiguous destination (out of place) when the cipher range
starts exactly at the end of a source segment, or when the range includes a
zero-length segment. The op still completes with
`RTE_CRYPTO_OP_STATUS_SUCCESS`, but instead of the payload in the next mbuf,
the driver ciphers whatever memory follows the current segment's data. That is
usually leftovers from an earlier packet in the same mbuf, or the neighbouring
mempool object when the range is longer than the tailroom.

The problem is in `process_openssl_cipher_des3ctr()` in
`drivers/crypto/openssl/rte_openssl_pmd.c`. The segment-selection loop uses
`offset > rte_pktmbuf_data_len(m)`, so an offset equal to a segment's length
stops on that segment with `l = 0` bytes left. The byte loop reads `*src`
before checking `l`, then decrements it to -1. Because `!l` never becomes true
again, the switch to `m->next` never happens and all `cipher.data.length` bytes
are read linearly.

The PMD accepts this layout, since it advertises
`RTE_CRYPTODEV_FF_OOP_SGL_IN_LB_OUT` and only rejects a segmented destination.
An application that always puts the header and the payload in separate mbufs
will therefore get wrong output on every packet. AES-CTR and the other
EVP-based ciphers in the same driver handle the same op correctly. No library
or example in the tree submits such ops.

The attached reproducer builds a 16-byte header plus a 31-byte payload in two
chained mbufs and encrypts it with `cipher.data.offset = 16`. With the first
segment split at 20 bytes, the decrypted result is the original payload. With
the split at 16 bytes, the result is 31 `S` bytes from a previously freed
packet. No crash or ASan report occurs. A patch that advances to the next
non-empty segment before reading each byte is attached, along with two new
blockcipher test cases.

## Details

This is an ordinary bug, not a security issue: the mbuf layout and cipher
offset that trigger it are chosen by the
application itself, and no library, example or guest-facing path in the tree
builds such an op.

`process_openssl_cipher_des3ctr()` keeps reading from the first source segment
when the cipher range starts exactly at
the end of that segment: it never moves to the next mbuf of the chain. The op
completes with
`RTE_CRYPTO_OP_STATUS_SUCCESS`, but what was ciphered is the memory behind the
segment's data, not the payload.

Shown on `crypto_openssl` at commit 9a45341f18 (default build): a packet of a
16-byte header and a 31-byte payload,
held in two chained mbufs, is encrypted out of place into a contiguous mbuf
with `cipher.data.offset = 16`,
`cipher.data.length = 31`, and the result is decrypted again as the peer would
do.

| cipher | first segment | peer decrypts |
|---|---|---|
| `RTE_CRYPTO_CIPHER_AES_CTR` | 16 bytes | `the payload of this very packet` |
| `RTE_CRYPTO_CIPHER_3DES_CTR` | 20 bytes | `the payload of this very packet` |
| `RTE_CRYPTO_CIPHER_3DES_CTR` | 16 bytes | `SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS` |

`S` is the content of an unrelated packet that used the first mbuf before and
was freed.

The code, `drivers/crypto/openssl/rte_openssl_pmd.c:1181`:

```c
        for (m = mbuf_src; m != NULL && offset > rte_pktmbuf_data_len(m);
                        m = m->next)
                offset -= rte_pktmbuf_data_len(m);
        …
        src = rte_pktmbuf_mtod_offset(m, uint8_t *, offset);
        l = rte_pktmbuf_data_len(m) - offset;
        …
        for (n = 0; n < srclen; n++) {
                …
                dst[n] = *(src++) ^ ebuf[n % 8];        /* 1205 */

                l--;
                if (!l) {
                        m = m->next;
                        if (m) {
                                src = rte_pktmbuf_mtod(m, uint8_t *);
                                l = rte_pktmbuf_data_len(m);
                        }
                }
        }
```

The first loop compares with `>`, so with `offset == data_len` it stops on the
segment that ends at the offset: `src`
points one byte past that segment's data and `l`, the number of bytes left in
the segment, is 0. The byte loop reads
`*src` before it looks at `l`, then decrements `l` to -1, and `!l` is never
true again. All `srclen` bytes are taken
linearly from `src`; nothing bounds the read to the mbuf's buffer.

In the example the offset 16 equals the length of the first segment, so the 31
bytes come from the tailroom of the
header mbuf, which still holds the previous packet. With the first segment 20
bytes long, `l` starts at 4, reaches 0
after four bytes and the switch to the second mbuf happens as intended.

Two more observations with the same program, adapted to other segment layouts:

- A zero-length segment inside the range has the same effect, because the inner
assignment
  `l = rte_pktmbuf_data_len(m)` also produces `l = 0`: with segments of 20, 0
and 27 bytes the peer gets `the ` followed
  by 27 bytes that are not the payload.
- With a 3000-byte range and the default 2048-byte data room, the output is the
3000 bytes of memory that follow the
  header in the first mbuf; 968 of them lie beyond the end of that mbuf's
buffer (`buf_addr + buf_len`), in the
  neighbouring object of the mempool. No crash results.

The other segment walkers of this file (`process_openssl_encryption_update()`
at line 974 and its siblings) start with
the same `offset > data_len` loop, but they pass `l` as a length to
`EVP_*Update()` and then advance with
`for (m = m->next; …)`, so `l = 0` is harmless there. That is why AES-CTR gives
the right result on the same op.

The layout is one the PMD accepts. `process_openssl_cipher_op()` only rejects a
segmented destination
(`rte_openssl_pmd.c:1699`); a chained `m_src` with a contiguous `m_dst` goes on
to `process_openssl_cipher_des3ctr()`
at line 1724, and the device advertises `RTE_CRYPTODEV_FF_OOP_SGL_IN_LB_OUT`
(line 4185). The op's offset and length
are consistent with the mbuf chain.

## Impact

Affected are applications that give `crypto_openssl` a 3DES-CTR op (cipher only
or chained with auth, both go through
`process_openssl_cipher_op()`) with a chained source and a contiguous
destination, when the cipher range starts at
the beginning of a segment or spans an empty segment. No devargs or build
options are involved; the default build is
affected. The mbuf layout and the offset are the application's own choice.
Nothing in `lib/` or `examples/` submits
such ops: `lib/ipsec` and `lib/pdcp` do not accept 3DES-CTR, vhost crypto
always builds a single-segment source,
`l2fwd-crypto` works in place and `test-crypto-perf` uses cipher offset 0.

Where the layout puts the boundary at the cipher offset by design, e.g. a
header mbuf followed by a payload mbuf, the
op goes wrong on every packet, with a success status. On encryption the output
is the keystream XORed with whatever
sits behind the first segment's data (leftovers of earlier packets in that
mbuf, and the neighbouring mempool object
when the range is longer than the tailroom), so the receiver never gets the
payload. On decryption the application
receives bytes derived from that memory instead of the plaintext. The
destination is written correctly sized; there
is no out-of-bounds write, and no crash was observed. The ASan build returns
the same bytes without a report, because
all mbufs of a pool sit in one allocation.

3DES-CTR together with out-of-place operation on chained mbufs is an uncommon
combination; an application that uses
it with such a layout sees wrong output on its first test. AES-CTR and the
other EVP-based ciphers of the same PMD are
not affected.

## Reproducing

`repro` compiles `repro.d/encrypt_chained_packet.c` with gcc against the shared
libraries of the default build
`/src/build` (`pkg-config` from `meson-uninstalled`), then runs it twice with
`--no-huge -m 512 --no-pci --vdev crypto_openssl`, loading
`librte_mempool_ring.so` and `librte_crypto_openssl.so`
with `-d`. No devargs, build options or source changes are involved.

Files:

- `repro` (setup and verdict): builds the application, runs it once as a
control and once on the boundary layout,
  prints both results and decides.
- `repro.d/encrypt_chained_packet.c` (the application developer's part): an
ordinary cryptodev user. It checks that
  the device advertises `RTE_CRYPTODEV_FF_OOP_SGL_IN_LB_OUT`, creates a
cipher-only `RTE_CRYPTO_CIPHER_3DES_CTR`
  session, and lets one packet of another flow (256 `S` bytes) pass through an
mbuf that it then frees. It builds the
  packet, 16 `H` header bytes followed by the payload from the command line, in
two chained mbufs split after the
  number of bytes given on the command line; the first of these mbufs is the
one the other flow just freed. It
  encrypts the packet out of place into a contiguous mbuf with
`cipher.data.offset = 16` and
  `cipher.data.length = strlen(payload)`, decrypts that mbuf with the same
session as the peer would, and prints
  `peer decrypts: <payload bytes>`. Every op must complete with
`RTE_CRYPTO_OP_STATUS_SUCCESS`, or the program exits
  with an error.

The payload is `the payload of this very packet` (31 bytes). The two runs
differ only in where the packet is split:

- control: first segment of 20 bytes (header plus 4 payload bytes); the peer
must get the payload back, otherwise
  the script stops with an error because the setup is broken;
- trigger: first segment of 16 bytes, so the segment ends exactly at
`cipher.data.offset`.

Expected output on the affected tree:

```
CONTROL  3des-ctr, first segment 20 bytes: peer decrypts 'the payload of this
very packet'
OBSERVED 3des-ctr, first segment 16 bytes: peer decrypts
'SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS'
BUG
```

`BUG` is printed when the trigger run returns only `S` bytes: the 31 bytes
behind the header in the first mbuf, left
there by the freed packet, were encrypted instead of the payload in the second
mbuf. With the attached patch applied
(rebuilding `drivers/librte_crypto_openssl.so.27.0`) both runs return the
payload and the script prints `OK`. It takes
about 2.5 seconds.

## Suggested fix

The attached patch changes `process_openssl_cipher_des3ctr()` in
`drivers/crypto/openssl/rte_openssl_pmd.c` to move
to the next segment before reading a byte instead of after. At the top of the
byte loop a `while (l == 0)` advances
`m` to the next mbuf that still holds data and resets `src` and `l` from it; if
the chain ends before `srclen` bytes
were processed it jumps to `process_cipher_des3ctr_err`, so the op fails with
`RTE_CRYPTO_OP_STATUS_ERROR`. The old
switch after `l--` is removed. This covers the boundary case, empty segments,
and a range that runs past the end of
the chain, which the current loop also reads linearly and reports as success;
that case now fails instead. Valid
input is ciphered as before. The other segment walkers of the file pass `l` as
a length to OpenSSL and are left
alone.

The patch also adds two blockcipher test cases to
`app/test/test_cryptodev_des_test_vectors.h`: 3DES-128-CTR
encryption and decryption, out of place with an SGL source and a linear
destination, two segments of 256 bytes and
`cipher_offset = 256`, with the IV advanced by the 32 skipped blocks so the
existing ciphertext vector applies. Both
fail on the unpatched driver and pass with the fix in
`cryptodev_openssl_autotest`.

```
Fixes: 8f675fc72d5c ("crypto/openssl: support scatter-gather")
Cc: stable@dpdk.org
```

-- 
You are receiving this mail because:
You are the assignee for the bug.

^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~2026-10-27 16:21 UTC | newest]

Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-27 16:21 [DPDK/cryptodev Bug 2084] crypto/openssl: 3DES-CTR reads past segment that ends at cipher offset bugzilla

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox