Hi,
I found a missing input validation issue in the CN20K (OW family)
inline IPsec session creation path. When DES_CBC or 3DES_CBC is used
as the cipher algorithm, the key length from the user-supplied crypto
xform is never validated before being copied into a fixed-size buffer,
allowing an out-of-bounds write that corrupts adjacent SA structure
members.
Confirmed against current HEAD (8dc80afd, 2026-03-05).
== Affected Call Chains ==
There are four distinct entry points that reach the vulnerable sink,
covering both session create and session update, inbound and outbound:
cn20k_eth_sec_session_create()
[cn20k_ethdev_sec.c:686]
inbound path -> cnxk_ow_ipsec_inb_sa_fill()
[cnxk_security.c:1495]
-> ow_ipsec_sa_common_param_fill() [line 1509]
outbound path -> cnxk_ow_ipsec_outb_sa_fill()
[cnxk_security.c:1605]
-> ow_ipsec_sa_common_param_fill() [line 1619]
cn20k_eth_sec_session_update()
[cn20k_ethdev_sec.c:1030]
inbound path -> cnxk_ow_ipsec_inb_sa_fill()
[line 1065]
outbound path -> cnxk_ow_ipsec_outb_sa_fill()
[line 1095]
None of these paths call cnxk_ipsec_xform_verify() before reaching
the SA fill functions.
== Root Cause ==
In ow_ipsec_sa_common_param_fill() (cnxk_security.c:1210), the
3DES_CBC cipher algorithm handler only sets the encryption type
without recording or validating the key length (lines 1300-1302):
case RTE_CRYPTO_CIPHER_3DES_CBC:
w2->s.enc_type = ROC_IE_SA_ENC_3DES_CBC;
break;
Immediately after the switch, the key pointer and length are taken
directly from the user-supplied xform (lines 1307-1308):
key = cipher_xfrm->cipher.key.data;
length = cipher_xfrm->cipher.key.length;
The key is then copied unconditionally at line 1366-1368:
if (key != NULL && length != 0) {
/* Copy encryption key */
memcpy(cipher_key, key, length);
...
}
The only length validation that follows is restricted to AES-family
algorithms (lines 1374-1392):
if (w2->s.enc_type == ROC_IE_SA_ENC_AES_CBC ||
w2->s.enc_type == ROC_IE_SA_ENC_AES_CCM ||
w2->s.enc_type == ROC_IE_SA_ENC_AES_CTR ||
w2->s.enc_type == ROC_IE_SA_ENC_AES_GCM ||
w2->s.enc_type == ROC_IE_SA_ENC_AES_CCM ||
w2->s.auth_type ==
ROC_IE_SA_AUTH_AES_GMAC) {
switch (length) {
case ROC_CPT_AES128_KEY_LEN: ...
case ROC_CPT_AES192_KEY_LEN: ...
case ROC_CPT_AES256_KEY_LEN: ...
default: return -EINVAL;
}
}
For 3DES_CBC (enc_type == ROC_IE_SA_ENC_3DES_CBC), this validation
block is never entered. The function returns 0 regardless of key
length.
== Affected Structure Layouts ==
The destination cipher_key parameter points into the SA structure.
In both OW SA variants, cipher_key[32] is immediately followed by
active members:
Outbound SA (roc_ie_ow.h, struct roc_ow_ipsec_outb_sa):
/* Word4 - Word7 */
uint8_t cipher_key[ROC_CTX_MAX_CKEY_LEN]; /*
32 bytes */
/* Word8 - Word9 */
union roc_ow_ipsec_outb_iv iv;
/* adjacent */
The outbound fill function passes sa->cipher_key and sa->iv.s.salt
as separate arguments (cnxk_security.c:1619):
rc = ow_ipsec_sa_common_param_fill(
&w2,
sa->cipher_key, sa->iv.s.salt,
sa->hmac_opad_ipad,
ipsec_xfrm, crypto_xfrm);
Inbound SA (roc_ie_ow.h, struct roc_ow_ipsec_inb_sa):
/* Word4 - Word7 */
uint8_t cipher_key[ROC_CTX_MAX_CKEY_LEN]; /*
32 bytes */
/* Word8 - Word9 */
union {
struct {
uint32_t rsvd8;
uint8_t salt[4];
/* active */
} s;
uint64_t u64;
} w8;
The inbound fill function passes sa->cipher_key and sa->w8.s.salt
(cnxk_security.c:1509):
rc = ow_ipsec_sa_common_param_fill(
&w2,
sa->cipher_key, sa->w8.s.salt,
sa->hmac_opad_ipad,
ipsec_xfrm, crypto_xfrm);
Static assertions confirm the layout (roc_ie_ow.h:525-526):
PLT_STATIC_ASSERT(offsetof(struct roc_ow_ipsec_outb_sa,
cipher_key) == 4 * sizeof(uint64_t));
PLT_STATIC_ASSERT(offsetof(struct roc_ow_ipsec_outb_sa,
iv) == 8 *
sizeof(uint64_t));
cipher_key occupies word4-7 (offset 32, size 32), iv starts at
word8 (offset 64). Any key length > 32 overwrites iv.
== Why the Inline Path Is Unprotected ==
The validation function cnxk_ipsec_xform_verify() exists in
drivers/crypto/cnxk/cnxk_ipsec.h and correctly rejects 3DES keys
that are not exactly 24 bytes (line 47-49):
if (crypto_xform->cipher.algo ==
RTE_CRYPTO_CIPHER_3DES_CBC &&
crypto_xform->cipher.key.length == 24)
return 0;
However, this function is defined as static inline in the crypto
(lookaside) driver header and is called only from the lookaside
session creation paths in drivers/crypto/cnxk/cn20k_ipsec.c
(lines 286, 400).
The inline IPsec path in drivers/net/cnxk/cn20k_ethdev_sec.c never
calls cnxk_ipsec_xform_verify(). There is no other key length
validation between the API entry point and the memcpy.
The DPDK security framework (rte_security_session_create() in
lib/security/rte_security.c:70) also performs no key length
validation; it passes the conf directly to the driver callback.
== Concrete Example ==
3DES_CBC with a 40-byte key through the CN20K outbound path:
cipher_xform.cipher.algo = RTE_CRYPTO_CIPHER_3DES_CBC;
cipher_xform.cipher.key.length = 40;
cipher_xform.cipher.key.data = key_40bytes;
...
rte_security_session_create(ctx, &conf, mp);
Call chain:
rte_security_session_create()
-- no validation
-> cn20k_eth_sec_session_create()
-- no xform_verify
-> cnxk_ow_ipsec_outb_sa_fill()
-> ow_ipsec_sa_common_param_fill()
-> 3DES_CBC: sets enc_type
only [line 1300-1302]
-> key = ...; length = 40
[line 1307-1308]
-> memcpy(cipher_key, key,
40) [line 1368]
first 32 bytes ->
cipher_key[32] OK
next 8 bytes
-> iv (outb) or w8/salt (inb) OVERFLOW
-> AES check skipped (enc_type is
3DES) [line 1374]
-> return 0
SUCCESS
The corrupted SA is accepted. For outbound, the iv used for packet
encryption is now partially overwritten with key material. For
inbound, the salt used for GCM/GMAC nonce construction is corrupted.
== Note on Related Paths ==
The same root cause (missing DES/3DES key length validation in the
shared helper, combined with no cnxk_ipsec_xform_verify() call from
the inline path) also affects:
- ON family: on_fill_ipsec_common_sa() line 988, CN9K
path
- OT family: ot_ipsec_sa_common_param_fill() line 174, CN10K path
These share the same fix strategy and could be addressed together.
== Suggested Fix ==
Option A (minimal, local):
Add DES/3DES validation in ow_ipsec_sa_common_param_fill() before
the memcpy, after the cipher algorithm switch:
if (cipher_xfrm != NULL) {
switch (cipher_xfrm->cipher.algo) {
...
case RTE_CRYPTO_CIPHER_3DES_CBC:
w2->s.enc_type =
ROC_IE_SA_ENC_3DES_CBC;
+ if
(cipher_xfrm->cipher.key.length != 24)
+ return -EINVAL;
break;
case RTE_CRYPTO_CIPHER_DES_CBC:
+ if
(cipher_xfrm->cipher.key.length != 8)
+ return -EINVAL;
...
}
}
Apply the same pattern to ot_ipsec_sa_common_param_fill() and
on_fill_ipsec_common_sa() / on_ipsec_sa_ctl_set().
Option B (comprehensive):
Move cnxk_ipsec_xform_verify() from drivers/crypto/cnxk/cnxk_ipsec.h
to the shared layer (drivers/common/cnxk/), and add calls in
cn9k/cn10k/cn20k_eth_sec_session_create() and session_update()
before the SA fill calls. This closes the validation gap for all
algorithm/key combinations at once.
== Impact ==
- Corrupts iv (outbound) or salt (inbound) fields in the hardware
IPsec SA, causing incorrect cryptographic operations or hardware
errors on the Octeon CN20K crypto engine.
- SA creation returns success, so the application proceeds to use a
corrupted SA with no error indication.
- Requires the application to supply an invalid key length, which
would not occur in correct usage. This is a missing defensive
check
at an API boundary, not an externally exploitable vulnerability.
Best regards,
Pengpeng Hou
[email protected]