Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

netfilter: x_tables: speed up jump target validation

The dummy ruleset I used to test the original validation change was broken,
most rules were unreachable and were not tested by mark_source_chains().

In some cases rulesets that used to load in a few seconds now require
several minutes.

sample ruleset that shows the behaviour:

echo "*filter"
for i in $(seq 0 100000);do
printf ":chain_%06x - [0:0]\n" $i
done
for i in $(seq 0 100000);do
printf -- "-A INPUT -j chain_%06x\n" $i
printf -- "-A INPUT -j chain_%06x\n" $i
printf -- "-A INPUT -j chain_%06x\n" $i
done
echo COMMIT

[ pipe result into iptables-restore ]

This ruleset will be about 74mbyte in size, with ~500k searches
though all 500k[1] rule entries. iptables-restore will take forever
(gave up after 10 minutes)

Instead of always searching the entire blob for a match, fill an
array with the start offsets of every single ipt_entry struct,
then do a binary search to check if the jump target is present or not.

After this change ruleset restore times get again close to what one
gets when reverting 36472341017529e (~3 seconds on my workstation).

[1] every user-defined rule gets an implicit RETURN, so we get
300k jumps + 100k userchains + 100k returns -> 500k rule entries

Fixes: 36472341017529e ("netfilter: x_tables: validate targets of jumps")
Reported-by: Jeff Wu <wujiafu@gmail.com>
Tested-by: Jeff Wu <wujiafu@gmail.com>
Signed-off-by: Florian Westphal <fw@strlen.de>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>

authored by

Florian Westphal and committed by
Pablo Neira Ayuso
f4dc7771 3101e0fc

+127 -64
+4
include/linux/netfilter/x_tables.h
··· 250 250 unsigned int target_offset, 251 251 unsigned int next_offset); 252 252 253 + unsigned int *xt_alloc_entry_offsets(unsigned int size); 254 + bool xt_find_jump_offset(const unsigned int *offsets, 255 + unsigned int target, unsigned int size); 256 + 253 257 int xt_check_match(struct xt_mtchk_param *, unsigned int size, u_int8_t proto, 254 258 bool inv_proto); 255 259 int xt_check_target(struct xt_tgchk_param *, unsigned int size, u_int8_t proto,
+25 -22
net/ipv4/netfilter/arp_tables.c
··· 299 299 memcmp(&e->arp, &uncond, sizeof(uncond)) == 0; 300 300 } 301 301 302 - static bool find_jump_target(const struct xt_table_info *t, 303 - const struct arpt_entry *target) 304 - { 305 - struct arpt_entry *iter; 306 - 307 - xt_entry_foreach(iter, t->entries, t->size) { 308 - if (iter == target) 309 - return true; 310 - } 311 - return false; 312 - } 313 - 314 302 /* Figures out from what hook each rule can be called: returns 0 if 315 303 * there are loops. Puts hook bitmask in comefrom. 316 304 */ 317 305 static int mark_source_chains(const struct xt_table_info *newinfo, 318 - unsigned int valid_hooks, void *entry0) 306 + unsigned int valid_hooks, void *entry0, 307 + unsigned int *offsets) 319 308 { 320 309 unsigned int hook; 321 310 ··· 377 388 XT_STANDARD_TARGET) == 0 && 378 389 newpos >= 0) { 379 390 /* This a jump; chase it. */ 391 + if (!xt_find_jump_offset(offsets, newpos, 392 + newinfo->number)) 393 + return 0; 380 394 e = (struct arpt_entry *) 381 395 (entry0 + newpos); 382 - if (!find_jump_target(newinfo, e)) 383 - return 0; 384 396 } else { 385 397 /* ... this is a fallthru */ 386 398 newpos = pos + e->next_offset; ··· 533 543 const struct arpt_replace *repl) 534 544 { 535 545 struct arpt_entry *iter; 546 + unsigned int *offsets; 536 547 unsigned int i; 537 548 int ret = 0; 538 549 ··· 546 555 newinfo->underflow[i] = 0xFFFFFFFF; 547 556 } 548 557 558 + offsets = xt_alloc_entry_offsets(newinfo->number); 559 + if (!offsets) 560 + return -ENOMEM; 549 561 i = 0; 550 562 551 563 /* Walk through entries, checking offsets. */ ··· 559 565 repl->underflow, 560 566 repl->valid_hooks); 561 567 if (ret != 0) 562 - break; 568 + goto out_free; 569 + if (i < repl->num_entries) 570 + offsets[i] = (void *)iter - entry0; 563 571 ++i; 564 572 if (strcmp(arpt_get_target(iter)->u.user.name, 565 573 XT_ERROR_TARGET) == 0) 566 574 ++newinfo->stacksize; 567 575 } 568 576 if (ret != 0) 569 - return ret; 577 + goto out_free; 570 578 579 + ret = -EINVAL; 571 580 if (i != repl->num_entries) 572 - return -EINVAL; 581 + goto out_free; 573 582 574 583 /* Check hooks all assigned */ 575 584 for (i = 0; i < NF_ARP_NUMHOOKS; i++) { ··· 580 583 if (!(repl->valid_hooks & (1 << i))) 581 584 continue; 582 585 if (newinfo->hook_entry[i] == 0xFFFFFFFF) 583 - return -EINVAL; 586 + goto out_free; 584 587 if (newinfo->underflow[i] == 0xFFFFFFFF) 585 - return -EINVAL; 588 + goto out_free; 586 589 } 587 590 588 - if (!mark_source_chains(newinfo, repl->valid_hooks, entry0)) 589 - return -ELOOP; 591 + if (!mark_source_chains(newinfo, repl->valid_hooks, entry0, offsets)) { 592 + ret = -ELOOP; 593 + goto out_free; 594 + } 595 + kvfree(offsets); 590 596 591 597 /* Finally, each sanity check must pass */ 592 598 i = 0; ··· 609 609 return ret; 610 610 } 611 611 612 + return ret; 613 + out_free: 614 + kvfree(offsets); 612 615 return ret; 613 616 } 614 617
+24 -21
net/ipv4/netfilter/ip_tables.c
··· 373 373 else return verdict; 374 374 } 375 375 376 - static bool find_jump_target(const struct xt_table_info *t, 377 - const struct ipt_entry *target) 378 - { 379 - struct ipt_entry *iter; 380 - 381 - xt_entry_foreach(iter, t->entries, t->size) { 382 - if (iter == target) 383 - return true; 384 - } 385 - return false; 386 - } 387 - 388 376 /* Figures out from what hook each rule can be called: returns 0 if 389 377 there are loops. Puts hook bitmask in comefrom. */ 390 378 static int 391 379 mark_source_chains(const struct xt_table_info *newinfo, 392 - unsigned int valid_hooks, void *entry0) 380 + unsigned int valid_hooks, void *entry0, 381 + unsigned int *offsets) 393 382 { 394 383 unsigned int hook; 395 384 ··· 447 458 XT_STANDARD_TARGET) == 0 && 448 459 newpos >= 0) { 449 460 /* This a jump; chase it. */ 461 + if (!xt_find_jump_offset(offsets, newpos, 462 + newinfo->number)) 463 + return 0; 450 464 e = (struct ipt_entry *) 451 465 (entry0 + newpos); 452 - if (!find_jump_target(newinfo, e)) 453 - return 0; 454 466 } else { 455 467 /* ... this is a fallthru */ 456 468 newpos = pos + e->next_offset; ··· 684 694 const struct ipt_replace *repl) 685 695 { 686 696 struct ipt_entry *iter; 697 + unsigned int *offsets; 687 698 unsigned int i; 688 699 int ret = 0; 689 700 ··· 697 706 newinfo->underflow[i] = 0xFFFFFFFF; 698 707 } 699 708 709 + offsets = xt_alloc_entry_offsets(newinfo->number); 710 + if (!offsets) 711 + return -ENOMEM; 700 712 i = 0; 701 713 /* Walk through entries, checking offsets. */ 702 714 xt_entry_foreach(iter, entry0, newinfo->size) { ··· 709 715 repl->underflow, 710 716 repl->valid_hooks); 711 717 if (ret != 0) 712 - return ret; 718 + goto out_free; 719 + if (i < repl->num_entries) 720 + offsets[i] = (void *)iter - entry0; 713 721 ++i; 714 722 if (strcmp(ipt_get_target(iter)->u.user.name, 715 723 XT_ERROR_TARGET) == 0) 716 724 ++newinfo->stacksize; 717 725 } 718 726 727 + ret = -EINVAL; 719 728 if (i != repl->num_entries) 720 - return -EINVAL; 729 + goto out_free; 721 730 722 731 /* Check hooks all assigned */ 723 732 for (i = 0; i < NF_INET_NUMHOOKS; i++) { ··· 728 731 if (!(repl->valid_hooks & (1 << i))) 729 732 continue; 730 733 if (newinfo->hook_entry[i] == 0xFFFFFFFF) 731 - return -EINVAL; 734 + goto out_free; 732 735 if (newinfo->underflow[i] == 0xFFFFFFFF) 733 - return -EINVAL; 736 + goto out_free; 734 737 } 735 738 736 - if (!mark_source_chains(newinfo, repl->valid_hooks, entry0)) 737 - return -ELOOP; 739 + if (!mark_source_chains(newinfo, repl->valid_hooks, entry0, offsets)) { 740 + ret = -ELOOP; 741 + goto out_free; 742 + } 743 + kvfree(offsets); 738 744 739 745 /* Finally, each sanity check must pass */ 740 746 i = 0; ··· 757 757 return ret; 758 758 } 759 759 760 + return ret; 761 + out_free: 762 + kvfree(offsets); 760 763 return ret; 761 764 } 762 765
+24 -21
net/ipv6/netfilter/ip6_tables.c
··· 402 402 else return verdict; 403 403 } 404 404 405 - static bool find_jump_target(const struct xt_table_info *t, 406 - const struct ip6t_entry *target) 407 - { 408 - struct ip6t_entry *iter; 409 - 410 - xt_entry_foreach(iter, t->entries, t->size) { 411 - if (iter == target) 412 - return true; 413 - } 414 - return false; 415 - } 416 - 417 405 /* Figures out from what hook each rule can be called: returns 0 if 418 406 there are loops. Puts hook bitmask in comefrom. */ 419 407 static int 420 408 mark_source_chains(const struct xt_table_info *newinfo, 421 - unsigned int valid_hooks, void *entry0) 409 + unsigned int valid_hooks, void *entry0, 410 + unsigned int *offsets) 422 411 { 423 412 unsigned int hook; 424 413 ··· 476 487 XT_STANDARD_TARGET) == 0 && 477 488 newpos >= 0) { 478 489 /* This a jump; chase it. */ 490 + if (!xt_find_jump_offset(offsets, newpos, 491 + newinfo->number)) 492 + return 0; 479 493 e = (struct ip6t_entry *) 480 494 (entry0 + newpos); 481 - if (!find_jump_target(newinfo, e)) 482 - return 0; 483 495 } else { 484 496 /* ... this is a fallthru */ 485 497 newpos = pos + e->next_offset; ··· 714 724 const struct ip6t_replace *repl) 715 725 { 716 726 struct ip6t_entry *iter; 727 + unsigned int *offsets; 717 728 unsigned int i; 718 729 int ret = 0; 719 730 ··· 727 736 newinfo->underflow[i] = 0xFFFFFFFF; 728 737 } 729 738 739 + offsets = xt_alloc_entry_offsets(newinfo->number); 740 + if (!offsets) 741 + return -ENOMEM; 730 742 i = 0; 731 743 /* Walk through entries, checking offsets. */ 732 744 xt_entry_foreach(iter, entry0, newinfo->size) { ··· 739 745 repl->underflow, 740 746 repl->valid_hooks); 741 747 if (ret != 0) 742 - return ret; 748 + goto out_free; 749 + if (i < repl->num_entries) 750 + offsets[i] = (void *)iter - entry0; 743 751 ++i; 744 752 if (strcmp(ip6t_get_target(iter)->u.user.name, 745 753 XT_ERROR_TARGET) == 0) 746 754 ++newinfo->stacksize; 747 755 } 748 756 757 + ret = -EINVAL; 749 758 if (i != repl->num_entries) 750 - return -EINVAL; 759 + goto out_free; 751 760 752 761 /* Check hooks all assigned */ 753 762 for (i = 0; i < NF_INET_NUMHOOKS; i++) { ··· 758 761 if (!(repl->valid_hooks & (1 << i))) 759 762 continue; 760 763 if (newinfo->hook_entry[i] == 0xFFFFFFFF) 761 - return -EINVAL; 764 + goto out_free; 762 765 if (newinfo->underflow[i] == 0xFFFFFFFF) 763 - return -EINVAL; 766 + goto out_free; 764 767 } 765 768 766 - if (!mark_source_chains(newinfo, repl->valid_hooks, entry0)) 767 - return -ELOOP; 769 + if (!mark_source_chains(newinfo, repl->valid_hooks, entry0, offsets)) { 770 + ret = -ELOOP; 771 + goto out_free; 772 + } 773 + kvfree(offsets); 768 774 769 775 /* Finally, each sanity check must pass */ 770 776 i = 0; ··· 787 787 return ret; 788 788 } 789 789 790 + return ret; 791 + out_free: 792 + kvfree(offsets); 790 793 return ret; 791 794 } 792 795
+50
net/netfilter/x_tables.c
··· 702 702 } 703 703 EXPORT_SYMBOL(xt_check_entry_offsets); 704 704 705 + /** 706 + * xt_alloc_entry_offsets - allocate array to store rule head offsets 707 + * 708 + * @size: number of entries 709 + * 710 + * Return: NULL or kmalloc'd or vmalloc'd array 711 + */ 712 + unsigned int *xt_alloc_entry_offsets(unsigned int size) 713 + { 714 + unsigned int *off; 715 + 716 + off = kcalloc(size, sizeof(unsigned int), GFP_KERNEL | __GFP_NOWARN); 717 + 718 + if (off) 719 + return off; 720 + 721 + if (size < (SIZE_MAX / sizeof(unsigned int))) 722 + off = vmalloc(size * sizeof(unsigned int)); 723 + 724 + return off; 725 + } 726 + EXPORT_SYMBOL(xt_alloc_entry_offsets); 727 + 728 + /** 729 + * xt_find_jump_offset - check if target is a valid jump offset 730 + * 731 + * @offsets: array containing all valid rule start offsets of a rule blob 732 + * @target: the jump target to search for 733 + * @size: entries in @offset 734 + */ 735 + bool xt_find_jump_offset(const unsigned int *offsets, 736 + unsigned int target, unsigned int size) 737 + { 738 + int m, low = 0, hi = size; 739 + 740 + while (hi > low) { 741 + m = (low + hi) / 2u; 742 + 743 + if (offsets[m] > target) 744 + hi = m; 745 + else if (offsets[m] < target) 746 + low = m + 1; 747 + else 748 + return true; 749 + } 750 + 751 + return false; 752 + } 753 + EXPORT_SYMBOL(xt_find_jump_offset); 754 + 705 755 int xt_check_target(struct xt_tgchk_param *par, 706 756 unsigned int size, u_int8_t proto, bool inv_proto) 707 757 {