1 /* SPDX-License-Identifier: BSD-3-Clause 2 * Copyright(C) 2021 Marvell. 3 */ 4 5 #include <rte_common.h> 6 #include <rte_cryptodev.h> 7 #include <rte_esp.h> 8 #include <rte_ip.h> 9 #include <rte_security.h> 10 #include <rte_tcp.h> 11 #include <rte_udp.h> 12 13 #include "test.h" 14 #include "test_cryptodev_security_ipsec.h" 15 16 #define IV_LEN_MAX 16 17 18 extern struct ipsec_test_data pkt_aes_256_gcm; 19 20 int 21 test_ipsec_sec_caps_verify(struct rte_security_ipsec_xform *ipsec_xform, 22 const struct rte_security_capability *sec_cap, 23 bool silent) 24 { 25 /* Verify security capabilities */ 26 27 if (ipsec_xform->options.esn == 1 && sec_cap->ipsec.options.esn == 0) { 28 if (!silent) 29 RTE_LOG(INFO, USER1, "ESN is not supported\n"); 30 return -ENOTSUP; 31 } 32 33 if (ipsec_xform->options.udp_encap == 1 && 34 sec_cap->ipsec.options.udp_encap == 0) { 35 if (!silent) 36 RTE_LOG(INFO, USER1, "UDP encapsulation is not supported\n"); 37 return -ENOTSUP; 38 } 39 40 if (ipsec_xform->options.udp_ports_verify == 1 && 41 sec_cap->ipsec.options.udp_ports_verify == 0) { 42 if (!silent) 43 RTE_LOG(INFO, USER1, "UDP encapsulation ports " 44 "verification is not supported\n"); 45 return -ENOTSUP; 46 } 47 48 if (ipsec_xform->options.copy_dscp == 1 && 49 sec_cap->ipsec.options.copy_dscp == 0) { 50 if (!silent) 51 RTE_LOG(INFO, USER1, "Copy DSCP is not supported\n"); 52 return -ENOTSUP; 53 } 54 55 if (ipsec_xform->options.copy_flabel == 1 && 56 sec_cap->ipsec.options.copy_flabel == 0) { 57 if (!silent) 58 RTE_LOG(INFO, USER1, "Copy Flow Label is not supported\n"); 59 return -ENOTSUP; 60 } 61 62 if (ipsec_xform->options.copy_df == 1 && 63 sec_cap->ipsec.options.copy_df == 0) { 64 if (!silent) 65 RTE_LOG(INFO, USER1, "Copy DP bit is not supported\n"); 66 return -ENOTSUP; 67 } 68 69 if (ipsec_xform->options.dec_ttl == 1 && 70 sec_cap->ipsec.options.dec_ttl == 0) { 71 if (!silent) 72 RTE_LOG(INFO, USER1, "Decrement TTL is not supported\n"); 73 return -ENOTSUP; 74 } 75 76 if (ipsec_xform->options.ecn == 1 && sec_cap->ipsec.options.ecn == 0) { 77 if (!silent) 78 RTE_LOG(INFO, USER1, "ECN is not supported\n"); 79 return -ENOTSUP; 80 } 81 82 if (ipsec_xform->options.stats == 1 && 83 sec_cap->ipsec.options.stats == 0) { 84 if (!silent) 85 RTE_LOG(INFO, USER1, "Stats is not supported\n"); 86 return -ENOTSUP; 87 } 88 89 if ((ipsec_xform->direction == RTE_SECURITY_IPSEC_SA_DIR_EGRESS) && 90 (ipsec_xform->options.iv_gen_disable == 1) && 91 (sec_cap->ipsec.options.iv_gen_disable != 1)) { 92 if (!silent) 93 RTE_LOG(INFO, USER1, 94 "Application provided IV is not supported\n"); 95 return -ENOTSUP; 96 } 97 98 if ((ipsec_xform->direction == RTE_SECURITY_IPSEC_SA_DIR_INGRESS) && 99 (ipsec_xform->options.tunnel_hdr_verify > 100 sec_cap->ipsec.options.tunnel_hdr_verify)) { 101 if (!silent) 102 RTE_LOG(INFO, USER1, 103 "Tunnel header verify is not supported\n"); 104 return -ENOTSUP; 105 } 106 107 if (ipsec_xform->options.ip_csum_enable == 1 && 108 sec_cap->ipsec.options.ip_csum_enable == 0) { 109 if (!silent) 110 RTE_LOG(INFO, USER1, 111 "Inner IP checksum is not supported\n"); 112 return -ENOTSUP; 113 } 114 115 if (ipsec_xform->options.l4_csum_enable == 1 && 116 sec_cap->ipsec.options.l4_csum_enable == 0) { 117 if (!silent) 118 RTE_LOG(INFO, USER1, 119 "Inner L4 checksum is not supported\n"); 120 return -ENOTSUP; 121 } 122 123 return 0; 124 } 125 126 int 127 test_ipsec_crypto_caps_aead_verify( 128 const struct rte_security_capability *sec_cap, 129 struct rte_crypto_sym_xform *aead) 130 { 131 const struct rte_cryptodev_symmetric_capability *sym_cap; 132 const struct rte_cryptodev_capabilities *crypto_cap; 133 int j = 0; 134 135 while ((crypto_cap = &sec_cap->crypto_capabilities[j++])->op != 136 RTE_CRYPTO_OP_TYPE_UNDEFINED) { 137 if (crypto_cap->op == RTE_CRYPTO_OP_TYPE_SYMMETRIC && 138 crypto_cap->sym.xform_type == aead->type && 139 crypto_cap->sym.aead.algo == aead->aead.algo) { 140 sym_cap = &crypto_cap->sym; 141 if (rte_cryptodev_sym_capability_check_aead(sym_cap, 142 aead->aead.key.length, 143 aead->aead.digest_length, 144 aead->aead.aad_length, 145 aead->aead.iv.length) == 0) 146 return 0; 147 } 148 } 149 150 return -ENOTSUP; 151 } 152 153 void 154 test_ipsec_td_in_from_out(const struct ipsec_test_data *td_out, 155 struct ipsec_test_data *td_in) 156 { 157 memcpy(td_in, td_out, sizeof(*td_in)); 158 159 /* Populate output text of td_in with input text of td_out */ 160 memcpy(td_in->output_text.data, td_out->input_text.data, 161 td_out->input_text.len); 162 td_in->output_text.len = td_out->input_text.len; 163 164 /* Populate input text of td_in with output text of td_out */ 165 memcpy(td_in->input_text.data, td_out->output_text.data, 166 td_out->output_text.len); 167 td_in->input_text.len = td_out->output_text.len; 168 169 td_in->ipsec_xform.direction = RTE_SECURITY_IPSEC_SA_DIR_INGRESS; 170 171 if (td_in->aead) { 172 td_in->xform.aead.aead.op = RTE_CRYPTO_AEAD_OP_DECRYPT; 173 } else { 174 td_in->xform.chain.auth.auth.op = RTE_CRYPTO_AUTH_OP_VERIFY; 175 td_in->xform.chain.cipher.cipher.op = 176 RTE_CRYPTO_CIPHER_OP_DECRYPT; 177 } 178 } 179 180 static bool 181 is_ipv4(void *ip) 182 { 183 struct rte_ipv4_hdr *ipv4 = ip; 184 uint8_t ip_ver; 185 186 ip_ver = (ipv4->version_ihl & 0xf0) >> RTE_IPV4_IHL_MULTIPLIER; 187 if (ip_ver == IPVERSION) 188 return true; 189 else 190 return false; 191 } 192 193 static void 194 test_ipsec_csum_init(void *ip, bool l3, bool l4) 195 { 196 struct rte_ipv4_hdr *ipv4; 197 struct rte_tcp_hdr *tcp; 198 struct rte_udp_hdr *udp; 199 uint8_t next_proto; 200 uint8_t size; 201 202 if (is_ipv4(ip)) { 203 ipv4 = ip; 204 size = sizeof(struct rte_ipv4_hdr); 205 next_proto = ipv4->next_proto_id; 206 207 if (l3) 208 ipv4->hdr_checksum = 0; 209 } else { 210 size = sizeof(struct rte_ipv6_hdr); 211 next_proto = ((struct rte_ipv6_hdr *)ip)->proto; 212 } 213 214 if (l4) { 215 switch (next_proto) { 216 case IPPROTO_TCP: 217 tcp = (struct rte_tcp_hdr *)RTE_PTR_ADD(ip, size); 218 tcp->cksum = 0; 219 break; 220 case IPPROTO_UDP: 221 udp = (struct rte_udp_hdr *)RTE_PTR_ADD(ip, size); 222 udp->dgram_cksum = 0; 223 break; 224 default: 225 return; 226 } 227 } 228 } 229 230 void 231 test_ipsec_td_prepare(const struct crypto_param *param1, 232 const struct crypto_param *param2, 233 const struct ipsec_test_flags *flags, 234 struct ipsec_test_data *td_array, 235 int nb_td) 236 237 { 238 struct ipsec_test_data *td; 239 int i; 240 241 memset(td_array, 0, nb_td * sizeof(*td)); 242 243 for (i = 0; i < nb_td; i++) { 244 td = &td_array[i]; 245 /* Copy template for packet & key fields */ 246 memcpy(td, &pkt_aes_256_gcm, sizeof(*td)); 247 248 /* Override fields based on param */ 249 250 if (param1->type == RTE_CRYPTO_SYM_XFORM_AEAD) 251 td->aead = true; 252 else 253 td->aead = false; 254 255 td->xform.aead.aead.algo = param1->alg.aead; 256 td->xform.aead.aead.key.length = param1->key_length; 257 258 if (flags->iv_gen) 259 td->ipsec_xform.options.iv_gen_disable = 0; 260 261 if (flags->sa_expiry_pkts_soft) 262 td->ipsec_xform.life.packets_soft_limit = 263 IPSEC_TEST_PACKETS_MAX - 1; 264 265 if (flags->ip_csum) { 266 td->ipsec_xform.options.ip_csum_enable = 1; 267 test_ipsec_csum_init(&td->input_text.data, true, false); 268 } 269 270 if (flags->l4_csum) { 271 td->ipsec_xform.options.l4_csum_enable = 1; 272 test_ipsec_csum_init(&td->input_text.data, false, true); 273 } 274 275 } 276 277 RTE_SET_USED(param2); 278 } 279 280 void 281 test_ipsec_td_update(struct ipsec_test_data td_inb[], 282 const struct ipsec_test_data td_outb[], 283 int nb_td, 284 const struct ipsec_test_flags *flags) 285 { 286 int i; 287 288 for (i = 0; i < nb_td; i++) { 289 memcpy(td_inb[i].output_text.data, td_outb[i].input_text.data, 290 td_outb[i].input_text.len); 291 td_inb[i].output_text.len = td_outb->input_text.len; 292 293 if (flags->icv_corrupt) { 294 int icv_pos = td_inb[i].input_text.len - 4; 295 td_inb[i].input_text.data[icv_pos] += 1; 296 } 297 298 if (flags->sa_expiry_pkts_hard) 299 td_inb[i].ipsec_xform.life.packets_hard_limit = 300 IPSEC_TEST_PACKETS_MAX - 1; 301 302 if (flags->udp_encap) 303 td_inb[i].ipsec_xform.options.udp_encap = 1; 304 305 if (flags->udp_ports_verify) 306 td_inb[i].ipsec_xform.options.udp_ports_verify = 1; 307 308 td_inb[i].ipsec_xform.options.tunnel_hdr_verify = 309 flags->tunnel_hdr_verify; 310 311 if (flags->ip_csum) 312 td_inb[i].ipsec_xform.options.ip_csum_enable = 1; 313 314 if (flags->l4_csum) 315 td_inb[i].ipsec_xform.options.l4_csum_enable = 1; 316 317 /* Clear outbound specific flags */ 318 td_inb[i].ipsec_xform.options.iv_gen_disable = 0; 319 } 320 } 321 322 void 323 test_ipsec_display_alg(const struct crypto_param *param1, 324 const struct crypto_param *param2) 325 { 326 if (param1->type == RTE_CRYPTO_SYM_XFORM_AEAD) 327 printf("\t%s [%d]\n", 328 rte_crypto_aead_algorithm_strings[param1->alg.aead], 329 param1->key_length); 330 331 RTE_SET_USED(param2); 332 } 333 334 static int 335 test_ipsec_tunnel_hdr_len_get(const struct ipsec_test_data *td) 336 { 337 int len = 0; 338 339 if (td->ipsec_xform.direction == RTE_SECURITY_IPSEC_SA_DIR_EGRESS) { 340 if (td->ipsec_xform.mode == RTE_SECURITY_IPSEC_SA_MODE_TUNNEL) { 341 if (td->ipsec_xform.tunnel.type == 342 RTE_SECURITY_IPSEC_TUNNEL_IPV4) 343 len += sizeof(struct rte_ipv4_hdr); 344 else 345 len += sizeof(struct rte_ipv6_hdr); 346 } 347 } 348 349 return len; 350 } 351 352 static int 353 test_ipsec_iv_verify_push(struct rte_mbuf *m, const struct ipsec_test_data *td) 354 { 355 static uint8_t iv_queue[IV_LEN_MAX * IPSEC_TEST_PACKETS_MAX]; 356 uint8_t *iv_tmp, *output_text = rte_pktmbuf_mtod(m, uint8_t *); 357 int i, iv_pos, iv_len; 358 static int index; 359 360 if (td->aead) 361 iv_len = td->xform.aead.aead.iv.length - td->salt.len; 362 else 363 iv_len = td->xform.chain.cipher.cipher.iv.length; 364 365 iv_pos = test_ipsec_tunnel_hdr_len_get(td) + sizeof(struct rte_esp_hdr); 366 output_text += iv_pos; 367 368 TEST_ASSERT(iv_len <= IV_LEN_MAX, "IV length greater than supported"); 369 370 /* Compare against previous values */ 371 for (i = 0; i < index; i++) { 372 iv_tmp = &iv_queue[i * IV_LEN_MAX]; 373 374 if (memcmp(output_text, iv_tmp, iv_len) == 0) { 375 printf("IV repeated"); 376 return TEST_FAILED; 377 } 378 } 379 380 /* Save IV for future comparisons */ 381 382 iv_tmp = &iv_queue[index * IV_LEN_MAX]; 383 memcpy(iv_tmp, output_text, iv_len); 384 index++; 385 386 if (index == IPSEC_TEST_PACKETS_MAX) 387 index = 0; 388 389 return TEST_SUCCESS; 390 } 391 392 static int 393 test_ipsec_l3_csum_verify(struct rte_mbuf *m) 394 { 395 uint16_t actual_cksum, expected_cksum; 396 struct rte_ipv4_hdr *ip; 397 398 ip = rte_pktmbuf_mtod(m, struct rte_ipv4_hdr *); 399 400 if (!is_ipv4((void *)ip)) 401 return TEST_SKIPPED; 402 403 actual_cksum = ip->hdr_checksum; 404 405 ip->hdr_checksum = 0; 406 407 expected_cksum = rte_ipv4_cksum(ip); 408 409 if (actual_cksum != expected_cksum) 410 return TEST_FAILED; 411 412 return TEST_SUCCESS; 413 } 414 415 static int 416 test_ipsec_l4_csum_verify(struct rte_mbuf *m) 417 { 418 uint16_t actual_cksum = 0, expected_cksum = 0; 419 struct rte_ipv4_hdr *ipv4; 420 struct rte_ipv6_hdr *ipv6; 421 struct rte_tcp_hdr *tcp; 422 struct rte_udp_hdr *udp; 423 void *ip, *l4; 424 425 ip = rte_pktmbuf_mtod(m, void *); 426 427 if (is_ipv4(ip)) { 428 ipv4 = ip; 429 l4 = RTE_PTR_ADD(ipv4, sizeof(struct rte_ipv4_hdr)); 430 431 switch (ipv4->next_proto_id) { 432 case IPPROTO_TCP: 433 tcp = (struct rte_tcp_hdr *)l4; 434 actual_cksum = tcp->cksum; 435 tcp->cksum = 0; 436 expected_cksum = rte_ipv4_udptcp_cksum(ipv4, l4); 437 break; 438 case IPPROTO_UDP: 439 udp = (struct rte_udp_hdr *)l4; 440 actual_cksum = udp->dgram_cksum; 441 udp->dgram_cksum = 0; 442 expected_cksum = rte_ipv4_udptcp_cksum(ipv4, l4); 443 break; 444 default: 445 break; 446 } 447 } else { 448 ipv6 = ip; 449 l4 = RTE_PTR_ADD(ipv6, sizeof(struct rte_ipv6_hdr)); 450 451 switch (ipv6->proto) { 452 case IPPROTO_TCP: 453 tcp = (struct rte_tcp_hdr *)l4; 454 actual_cksum = tcp->cksum; 455 tcp->cksum = 0; 456 expected_cksum = rte_ipv6_udptcp_cksum(ipv6, l4); 457 break; 458 case IPPROTO_UDP: 459 udp = (struct rte_udp_hdr *)l4; 460 actual_cksum = udp->dgram_cksum; 461 udp->dgram_cksum = 0; 462 expected_cksum = rte_ipv6_udptcp_cksum(ipv6, l4); 463 break; 464 default: 465 break; 466 } 467 } 468 469 if (actual_cksum != expected_cksum) 470 return TEST_FAILED; 471 472 return TEST_SUCCESS; 473 } 474 475 static int 476 test_ipsec_td_verify(struct rte_mbuf *m, const struct ipsec_test_data *td, 477 bool silent, const struct ipsec_test_flags *flags) 478 { 479 uint8_t *output_text = rte_pktmbuf_mtod(m, uint8_t *); 480 uint32_t skip, len = rte_pktmbuf_pkt_len(m); 481 int ret; 482 483 /* For tests with status as error for test success, skip verification */ 484 if (td->ipsec_xform.direction == RTE_SECURITY_IPSEC_SA_DIR_INGRESS && 485 (flags->icv_corrupt || 486 flags->sa_expiry_pkts_hard || 487 flags->tunnel_hdr_verify)) 488 return TEST_SUCCESS; 489 490 if (td->ipsec_xform.direction == RTE_SECURITY_IPSEC_SA_DIR_EGRESS && 491 flags->udp_encap) { 492 const struct rte_ipv4_hdr *iph4; 493 const struct rte_ipv6_hdr *iph6; 494 495 if (td->ipsec_xform.tunnel.type == 496 RTE_SECURITY_IPSEC_TUNNEL_IPV4) { 497 iph4 = (const struct rte_ipv4_hdr *)output_text; 498 if (iph4->next_proto_id != IPPROTO_UDP) { 499 printf("UDP header is not found\n"); 500 return TEST_FAILED; 501 } 502 } else { 503 iph6 = (const struct rte_ipv6_hdr *)output_text; 504 if (iph6->proto != IPPROTO_UDP) { 505 printf("UDP header is not found\n"); 506 return TEST_FAILED; 507 } 508 } 509 510 len -= sizeof(struct rte_udp_hdr); 511 output_text += sizeof(struct rte_udp_hdr); 512 } 513 514 if (len != td->output_text.len) { 515 printf("Output length (%d) not matching with expected (%d)\n", 516 len, td->output_text.len); 517 return TEST_FAILED; 518 } 519 520 skip = test_ipsec_tunnel_hdr_len_get(td); 521 522 len -= skip; 523 output_text += skip; 524 525 if ((td->ipsec_xform.direction == RTE_SECURITY_IPSEC_SA_DIR_INGRESS) && 526 flags->ip_csum) { 527 if (m->ol_flags & RTE_MBUF_F_RX_IP_CKSUM_GOOD) 528 ret = test_ipsec_l3_csum_verify(m); 529 else 530 ret = TEST_FAILED; 531 532 if (ret == TEST_FAILED) 533 printf("Inner IP checksum test failed\n"); 534 535 return ret; 536 } 537 538 if ((td->ipsec_xform.direction == RTE_SECURITY_IPSEC_SA_DIR_INGRESS) && 539 flags->l4_csum) { 540 if (m->ol_flags & RTE_MBUF_F_RX_L4_CKSUM_GOOD) 541 ret = test_ipsec_l4_csum_verify(m); 542 else 543 ret = TEST_FAILED; 544 545 if (ret == TEST_FAILED) 546 printf("Inner L4 checksum test failed\n"); 547 548 return ret; 549 } 550 551 552 if (memcmp(output_text, td->output_text.data + skip, len)) { 553 if (silent) 554 return TEST_FAILED; 555 556 printf("TestCase %s line %d: %s\n", __func__, __LINE__, 557 "output text not as expected\n"); 558 559 rte_hexdump(stdout, "expected", td->output_text.data + skip, 560 len); 561 rte_hexdump(stdout, "actual", output_text, len); 562 return TEST_FAILED; 563 } 564 565 return TEST_SUCCESS; 566 } 567 568 static int 569 test_ipsec_res_d_prepare(struct rte_mbuf *m, const struct ipsec_test_data *td, 570 struct ipsec_test_data *res_d) 571 { 572 uint8_t *output_text = rte_pktmbuf_mtod(m, uint8_t *); 573 uint32_t len = rte_pktmbuf_pkt_len(m); 574 575 memcpy(res_d, td, sizeof(*res_d)); 576 memcpy(res_d->input_text.data, output_text, len); 577 res_d->input_text.len = len; 578 579 res_d->ipsec_xform.direction = RTE_SECURITY_IPSEC_SA_DIR_INGRESS; 580 if (res_d->aead) { 581 res_d->xform.aead.aead.op = RTE_CRYPTO_AEAD_OP_DECRYPT; 582 } else { 583 printf("Only AEAD supported\n"); 584 return TEST_SKIPPED; 585 } 586 587 return TEST_SUCCESS; 588 } 589 590 int 591 test_ipsec_post_process(struct rte_mbuf *m, const struct ipsec_test_data *td, 592 struct ipsec_test_data *res_d, bool silent, 593 const struct ipsec_test_flags *flags) 594 { 595 int ret; 596 597 if (flags->iv_gen && 598 td->ipsec_xform.direction == RTE_SECURITY_IPSEC_SA_DIR_EGRESS) { 599 ret = test_ipsec_iv_verify_push(m, td); 600 if (ret != TEST_SUCCESS) 601 return ret; 602 } 603 604 /* 605 * In case of known vector tests & all inbound tests, res_d provided 606 * would be NULL and output data need to be validated against expected. 607 * For inbound, output_text would be plain packet and for outbound 608 * output_text would IPsec packet. Validate by comparing against 609 * known vectors. 610 * 611 * In case of combined mode tests, the output_text from outbound 612 * operation (ie, IPsec packet) would need to be inbound processed to 613 * obtain the plain text. Copy output_text to result data, 'res_d', so 614 * that inbound processing can be done. 615 */ 616 617 if (res_d == NULL) 618 return test_ipsec_td_verify(m, td, silent, flags); 619 else 620 return test_ipsec_res_d_prepare(m, td, res_d); 621 } 622 623 int 624 test_ipsec_status_check(struct rte_crypto_op *op, 625 const struct ipsec_test_flags *flags, 626 enum rte_security_ipsec_sa_direction dir, 627 int pkt_num) 628 { 629 int ret = TEST_SUCCESS; 630 631 if (dir == RTE_SECURITY_IPSEC_SA_DIR_INGRESS && 632 flags->sa_expiry_pkts_hard && 633 pkt_num == IPSEC_TEST_PACKETS_MAX) { 634 if (op->status != RTE_CRYPTO_OP_STATUS_ERROR) { 635 printf("SA hard expiry (pkts) test failed\n"); 636 return TEST_FAILED; 637 } else { 638 return TEST_SUCCESS; 639 } 640 } 641 642 if ((dir == RTE_SECURITY_IPSEC_SA_DIR_INGRESS) && 643 flags->tunnel_hdr_verify) { 644 if (op->status != RTE_CRYPTO_OP_STATUS_ERROR) { 645 printf("Tunnel header verify test case failed\n"); 646 return TEST_FAILED; 647 } else { 648 return TEST_SUCCESS; 649 } 650 } 651 652 if (dir == RTE_SECURITY_IPSEC_SA_DIR_INGRESS && flags->icv_corrupt) { 653 if (op->status != RTE_CRYPTO_OP_STATUS_ERROR) { 654 printf("ICV corruption test case failed\n"); 655 ret = TEST_FAILED; 656 } 657 } else { 658 if (op->status != RTE_CRYPTO_OP_STATUS_SUCCESS) { 659 printf("Security op processing failed [pkt_num: %d]\n", 660 pkt_num); 661 ret = TEST_FAILED; 662 } 663 } 664 665 if (flags->sa_expiry_pkts_soft && pkt_num == IPSEC_TEST_PACKETS_MAX) { 666 if (!(op->aux_flags & 667 RTE_CRYPTO_OP_AUX_FLAGS_IPSEC_SOFT_EXPIRY)) { 668 printf("SA soft expiry (pkts) test failed\n"); 669 ret = TEST_FAILED; 670 } 671 } 672 673 return ret; 674 } 675