Hi,

  I found a missing input validation issue in the CN9K (ON family)
  inbound IPsec SA creation path. When AES-XCBC-MAC is used as the
  authentication algorithm, the auth key length from the user-supplied
  xform is copied into a 16-byte fixed buffer without any length check,
  allowing an out-of-bounds write that can corrupt the adjacent traffic
  selector member within the same SA structure.


  This is distinct from the DES/3DES cipher key overflow previously
  reported in the shared helper functions (on_fill_ipsec_common_sa /
  ot/ow_ipsec_sa_common_param_fill). The vulnerable code here is in
  cnxk_on_ipsec_inb_sa_create() itself, affecting the auth key path.


  Confirmed against current HEAD (8dc80afd, 2026-03-05).


  == Root Cause ==


  In cnxk_on_ipsec_inb_sa_create() (cnxk_security.c:1140), when the
  auth algorithm is not AEAD/NULL/AES_GMAC, the function enters the
  else branch at line 1164 and extracts the auth key directly from
  the user-supplied xform (lines 1166-1167):


      auth_key = auth_xform->auth.key.data;
      auth_key_len = auth_xform->auth.key.length;


  Then for AES-XCBC-MAC, the key is copied with the user-supplied
  length into a 16-byte fixed buffer (lines 1186-1187):


      case RTE_CRYPTO_AUTH_AES_XCBC_MAC:
          memcpy(in_sa->aes_xcbc.key, auth_key, 
auth_key_len);
          ctx_len = offsetof(struct roc_ie_on_inb_sa,
                      
       aes_xcbc.selector);
          break;


&nbsp; There is no check that auth_key_len <= 16.


&nbsp; == Affected Structure Layout ==


&nbsp; The inbound SA union (roc_ie_on.h:201-225) for AES-XCBC is:


&nbsp; &nbsp; &nbsp; struct roc_ie_on_inb_sa {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; struct roc_ie_on_common_sa common_sa;&nbsp; 
/* w0-w7 */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; uint8_t udp_encap[8];&nbsp; &nbsp; &nbsp; 
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* w8&nbsp; &nbsp; */


&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* w9-w33, union of auth-algo-specific 
layouts */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; union {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; struct {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; uint8_t 
hmac_key[48];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; struct 
roc_ie_on_traffic_selector selector;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } sha1_or_gcm;


&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; struct {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; uint8_t 
key[16];&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;/* <-- 16 bytes */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; uint8_t 
unused[32];&nbsp; &nbsp; &nbsp; &nbsp; /* 32 bytes padding */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; struct 
roc_ie_on_traffic_selector selector;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } aes_xcbc;&nbsp; &nbsp; 
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* <-- active for XCBC 
*/


&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; struct {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; uint8_t 
hmac_key[64];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; uint8_t 
hmac_iv[64];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; struct 
roc_ie_on_traffic_selector selector;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } sha2;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; };
&nbsp; &nbsp; &nbsp; };


&nbsp; The destination key[16] is followed by unused[32] and then the
&nbsp; active traffic selector member:


&nbsp; &nbsp; &nbsp; struct roc_ie_on_traffic_selector {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; uint16_t src_port[2];&nbsp; &nbsp; &nbsp; 
&nbsp;/* port range */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; uint16_t dst_port[2];&nbsp; &nbsp; &nbsp; 
&nbsp;/* port range */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; union {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; struct {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; uint32_t 
src_addr[2];&nbsp; /* IPv4 address range */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; uint32_t 
dst_addr[2];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } ipv4;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; struct {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; uint8_t 
src_addr[32];&nbsp; /* IPv6 address range */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; uint8_t 
dst_addr[32];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } ipv6;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; };
&nbsp; &nbsp; &nbsp; };


&nbsp; So the memory layout from key[0] is:


&nbsp; &nbsp; &nbsp; offset 0..15:&nbsp; key[16]&nbsp; &nbsp; &nbsp; &nbsp; 
&nbsp;(destination buffer)
&nbsp; &nbsp; &nbsp; offset 16..47: unused[32]&nbsp; &nbsp; &nbsp; (padding, 
benign to overwrite)
&nbsp; &nbsp; &nbsp; offset 48+:&nbsp; &nbsp; selector&nbsp; &nbsp; &nbsp; 
&nbsp; (active traffic filter member)


&nbsp; Any auth_key_len &gt; 48 will write past unused[] and corrupt the
&nbsp; selector. The traffic selector controls which inbound IPsec
&nbsp; packets the hardware accepts based on source/destination IP
&nbsp; addresses and port ranges.


&nbsp; == The Subsequent roc_aes_xcbc_key_derive() Does Not Undo the Damage ==


&nbsp; After the memcpy, the code calls roc_aes_xcbc_key_derive() at
&nbsp; line 1195-1198:


&nbsp; &nbsp; &nbsp; if (auth_xform-&gt;auth.algo == 
RTE_CRYPTO_AUTH_AES_XCBC_MAC) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const uint8_t *auth_key = 
auth_xform-&gt;auth.key.data;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; roc_aes_xcbc_key_derive(auth_key, 
hmac_opad_ipad);
&nbsp; &nbsp; &nbsp; }


&nbsp; roc_aes_xcbc_key_derive() (roc_aes.c:203) writes exactly 48 bytes
&nbsp; (3 x 16-byte derived subkeys k1, k2, k3) into hmac_opad_ipad,
&nbsp; which overlaps the union starting at the sha2 variant. This
&nbsp; overwrites only the first 48 bytes of the union area, which
&nbsp; corresponds to key[16] + unused[32] in the aes_xcbc variant.


&nbsp; Critically, it does NOT touch anything at offset 48+, so bytes
&nbsp; written by the oversized memcpy into the selector region remain
&nbsp; intact and corrupted.


&nbsp; == Affected Call Chains ==


&nbsp; The vulnerable sink is reached from two entry points:


&nbsp; &nbsp; cn9k_eth_sec_session_create()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 
&nbsp; [cn9k_ethdev_sec.c:577]
&nbsp; &nbsp; &nbsp; inbound path (line 659):
&nbsp; &nbsp; &nbsp; &nbsp; -&gt; cnxk_on_ipsec_inb_sa_create()&nbsp; &nbsp; 
&nbsp;[cnxk_security.c:1140]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -&gt; memcpy(in_sa-&gt;aes_xcbc.key, 
auth_key, auth_key_len)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 
&nbsp; [line 1187]


&nbsp; The session_update path (cn9k_eth_sec_session_update, line 445)
&nbsp; only supports outbound, so it is not affected by this specific bug.
&nbsp; However, the outbound SA creation path (cnxk_on_ipsec_outb_sa_create)
&nbsp; also has a related issue: it calls roc_aes_xcbc_key_derive(auth_key,
&nbsp; ...) at line 1128, which reads exactly 16 bytes from auth_key via
&nbsp; aes_key_expand(). If auth_key.length < 16, this would be an
&nbsp; out-of-bounds read, though that is a separate issue.


&nbsp; Neither cn9k_eth_sec_session_create() nor cnxk_on_ipsec_inb_sa_create()
&nbsp; calls cnxk_ipsec_xform_verify() before reaching the memcpy.


&nbsp; == Why the Inline Path Is Unprotected ==


&nbsp; cnxk_ipsec_xform_verify() (drivers/crypto/cnxk/cnxk_ipsec.h:120)
&nbsp; correctly validates AES-XCBC key length via ipsec_xform_auth_verify()
&nbsp; (line 84-86):


&nbsp; &nbsp; &nbsp; if (crypto_xform-&gt;auth.algo == 
RTE_CRYPTO_AUTH_AES_XCBC_MAC &amp;&amp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; keylen == ROC_CPT_AES_XCBC_KEY_LENGTH)&nbsp; 
&nbsp;/* == 16 */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return 0;


&nbsp; But this is only called from the lookaside crypto paths
&nbsp; (drivers/crypto/cnxk/cn9k_ipsec.c, lines 212 and 296). The inline
&nbsp; ethdev path (cn9k_ethdev_sec.c) never calls it. The DPDK security
&nbsp; framework (rte_security_session_create) also performs no key length
&nbsp; validation.


&nbsp; == Concrete Example ==


&nbsp; AES-XCBC-MAC with a 64-byte auth key through the CN9K inbound path:


&nbsp; &nbsp; &nbsp; auth_xform.auth.algo = RTE_CRYPTO_AUTH_AES_XCBC_MAC;
&nbsp; &nbsp; &nbsp; auth_xform.auth.key.length = 64;
&nbsp; &nbsp; &nbsp; auth_xform.auth.key.data = key_64bytes;
&nbsp; &nbsp; &nbsp; ...
&nbsp; &nbsp; &nbsp; rte_security_session_create(ctx, &amp;conf, mp);


&nbsp; Call chain:


&nbsp; &nbsp; rte_security_session_create()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 
&nbsp; &nbsp; -- no validation
&nbsp; &nbsp; &nbsp; -&gt; cn9k_eth_sec_session_create()&nbsp; &nbsp; &nbsp; 
&nbsp; &nbsp;-- no xform_verify
&nbsp; &nbsp; &nbsp; &nbsp; -&gt; cnxk_on_ipsec_inb_sa_create()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -&gt; auth_key_len = 64&nbsp; &nbsp; &nbsp; 
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;[line 1167]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -&gt; memcpy(aes_xcbc.key, key, 64)&nbsp; 
&nbsp; &nbsp;[line 1187]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;bytes 0-15&nbsp; -&gt; 
key[16]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; OK
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;bytes 16-47 -&gt; 
unused[32]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;benign
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;bytes 48-63 -&gt; 
selector&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;OVERFLOW
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -&gt; roc_aes_xcbc_key_derive()&nbsp; &nbsp; 
&nbsp; &nbsp; &nbsp;[line 1198]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;writes 48 bytes to 
hmac_opad_ipad
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(covers key+unused, NOT 
selector)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -&gt; return ctx_len&nbsp; &nbsp; &nbsp; 
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; SUCCESS (positive value)


&nbsp; The corrupted SA is accepted. The traffic selector, which controls
&nbsp; hardware-level inbound packet filtering by IP address/port, now
&nbsp; contains attacker-controlled data from the oversized key.


&nbsp; == Suggested Fix ==


&nbsp; Add auth key length validation before the memcpy. The simplest
&nbsp; approach:


&nbsp; &nbsp; &nbsp; case RTE_CRYPTO_AUTH_AES_XCBC_MAC:
&nbsp; +&nbsp; &nbsp; &nbsp; &nbsp;if (auth_key_len != 
ROC_CPT_AES_XCBC_KEY_LENGTH) {
&nbsp; +&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;plt_err("Invalid AES-XCBC key 
length %d", auth_key_len);
&nbsp; +&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return -EINVAL;
&nbsp; +&nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; memcpy(in_sa-&gt;aes_xcbc.key, auth_key, 
auth_key_len);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ctx_len = offsetof(struct roc_ie_on_inb_sa,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 
&nbsp; &nbsp; &nbsp; &nbsp;aes_xcbc.selector);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break;


&nbsp; For completeness, the same pattern should also be applied to the
&nbsp; other auth algorithms in this function (SHA1_HMAC into hmac_key[48],
&nbsp; SHA2 variants into hmac_key[64]), as they have the same unchecked
&nbsp; memcpy pattern -- though their larger buffers make overflow into
&nbsp; the selector require proportionally larger invalid keys.


&nbsp; Alternatively, call cnxk_ipsec_xform_verify() (moved to the shared
&nbsp; common layer) from cn9k_eth_sec_session_create() before entering
&nbsp; cnxk_on_ipsec_inb_sa_create(). This would close all key length
&nbsp; validation gaps in the CN9K inline path at once.


&nbsp; == Impact ==


&nbsp; - Corrupts the hardware traffic selector in the inbound IPsec SA,
&nbsp; &nbsp; which controls IP address/port-based filtering of incoming
&nbsp; &nbsp; encrypted packets.
&nbsp; - The SA is accepted as successfully created (returns positive
&nbsp; &nbsp; ctx_len), so the application receives no error indication.
&nbsp; - Requires the application to supply an invalid key length.
&nbsp; &nbsp; Standard AES-XCBC keys are exactly 16 bytes. This represents a
&nbsp; &nbsp; missing defensive check at an API boundary.


Best regards,
Pengpeng Hou
[email protected]

Reply via email to