Bug 276732 - IPFW keep-state rules with untag do not go through parent rule cmd
Summary: IPFW keep-state rules with untag do not go through parent rule cmd
Status: Closed FIXED
Alias: None
Product: Base System
Classification: Unclassified
Component: kern (show other bugs)
Version: CURRENT
Hardware: Any Any
: --- Affects Some People
Assignee: John Baldwin
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2024-01-30 13:55 UTC by fodillemlinkarim
Modified: 2024-04-09 23:51 UTC (History)
3 users (show)

See Also:
jhb: mfc-stable14+
jhb: mfc-stable13+


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description fodillemlinkarim 2024-01-30 13:55:13 UTC
A FreeBSD user reported this one in the mailing list and I have a fix for the issue. I am convinced this bug is present in the oldest versions of FBSD.

TLDR, here is the fix for it (diff is against CURRENT):

diff --git a/sys/netpfil/ipfw/ip_fw2.c b/sys/netpfil/ipfw/ip_fw2.c
index d2b01fd..57c02dc 100644
--- a/sys/netpfil/ipfw/ip_fw2.c
+++ b/sys/netpfil/ipfw/ip_fw2.c
@@ -2887,7 +2887,8 @@ do {                                                              \
                                  l = f->cmd_len - f->act_ofs;
                                  cmdlen = 0;
                                  match = 1;
-                                 break;
+                                 continue;
+                                 break; /* not reached */
                                }
                                /*
                                 * Dynamic entry not found. If CHECK_STATE,

On 2023-11-08 2:26 a.m., Mikhail Holt wrote:
> Hello List,
>
> On a recent Stable 13 test host I, by accident, found that:
>
> /sbin/ipfw -q add 0031 allow              tcp from 192.168.64.0/24 to me dst-port ssh in via igb3 setup keep-state   WORKS
>
> /sbin/ipfw -q add 0031 allow log          tcp from 192.168.64.0/24 to me dst-port ssh in via igb3 setup keep-state   WORKS
>
> /sbin/ipfw -q add 0031 allow log tag   10 tcp from 192.168.64.0/24 to me dst-port ssh in via igb3 setup keep-state   WORKS
>
> /sbin/ipfw -q add 0031 allow log untag 10 tcp from 192.168.64.0/24 to me dst-port ssh in via igb3 setup keep-state   WORKS
>
> /sbin/ipfw -q add 0031 allow     untag 10 tcp from 192.168.64.0/24 to me dst-port ssh in via igb3 setup keep-state   DOES NOT WORK?
> - A dynamic rule is created as per the rules that work.
> - Packets are logged by a deny all rule which of course is never reached by the rules that work.

This is a very old bug in FreeBSD/ipfw O_PROBE_STATE and the good news is I have a fix for it. The fix is very easy and I will try to explain here what the bug is and what the fix is. The issue is only related to rules that have the F_NOT bit set and that are the beginning of the actions list of a keep-state rule. I'm also CCing some other folks that have authority in moving this forward so it hopefully gets into main eventually.

If you are like me, this is a perfect read for a Friday afternoon ;)

So here is how it goes:

When you create a keep-state rule, the userland part of ipfw will insert a O_PROBE_STATE action at the very beginning of the rule so when a packet hits the rule the kernel will PROBE the list of installed dynamic rules for a match before installing such a rule (we don't want to create more than one dynamic rule per parent rule). The code responsible for this looks like this in ipfw2.c:

        /*
         * generate O_PROBE_STATE if necessary
         */
        if (have_state && have_state->opcode != O_CHECK_STATE && !have_rstate) {
                fill_cmd(dst, O_PROBE_STATE, 0, have_state->arg1);
                dst = next_cmd(dst, &rblen);
        }  

Now ipfw userland also does something else when building the list of actions for a rule, it records the actions offset in the rule and put those actions together in a predetermined way, here is the section of ipfw2.c that does something like this:

        /*
         * start action section
         */
        rule->act_ofs = dst - rule->cmd;
           
        /* put back O_LOG, O_ALTQ, O_TAG if necessary */
log --> if (have_log) {
                i = F_LEN(have_log);
                CHECK_RBUFLEN(i);
                bcopy(have_log, dst, i * sizeof(uint32_t));
                dst += i;
        }
        if (have_altq) {
                i = F_LEN(have_altq);
                CHECK_RBUFLEN(i);
                bcopy(have_altq, dst, i * sizeof(uint32_t));
                dst += i;
        }
tag --> if (have_tag) {
                i = F_LEN(have_tag);
                CHECK_RBUFLEN(i);
                bcopy(have_tag, dst, i * sizeof(uint32_t));
                dst += i;   
        }

Nothing wrong with all this and if you are still with me (kudos to you) please take a moment to notice the 'log' action is _before_ the 'tag' action. This will be important later to understand why some rules in your example works and why some don't, although you may already have a clue...

Now let's transport ourselves into the kernel code, precisely in sys/netpfil/ipfw/ip_fw2.c around line 2865. Here we are inside the O_PROBE_STATE case and notice how after we find a dynamic entry how the kernel will 'reset' the cmd pointer to the action part of the parent rule by doing something like this:

                                       /*
                                         * Found dynamic entry, jump to the
                                         * 'action' part of the parent rule
                                         * by setting f, cmd, l and clearing
                                         * cmdlen.
                                         */
                                        f = q;
                                        f_pos = dyn_info.f_pos;
cmd pointer is reset -->                cmd = ACTION_PTR(f);
                                        l = f->cmd_len - f->act_ofs;
                                        cmdlen = 0;
                                        match = 1;
                                        break;
                                }

At this precise moment (pointed by the -->), if we refer to your example below, we would be hitting the dynamic rule entry and have cmd pointer reset to the ACTION part of the 'untag 10' action you have entered. We then set a few things and break from the switch statement. Now this gets us at the end of the switch statement where FreeBSD does something like this (around line 3337):

                        /*
                         * if we get here with l=0, then match is irrelevant.
                         */

cmd is 'untag' -->      if (cmd->len & F_NOT)
                                match = !match;

                        if (match) {
                                if (cmd->len & F_OR)
                                        skip_or = 1;
                        } else {
                                if (!(cmd->len & F_OR)) /* not an OR block, */
exits too early -->                     break;          /* try next rule    */
                        }

                }       /* end of inner loop, scan opcodes */

If you look at the check for F_NOT bit (which is cleverly folded in the len part of the cmd) you realize that check is actually made against the 'untag' action and since untag is actually NOT tag it will match and change match into !match. The code will not try to go through the actual action and it will exit the loop too early.

Essentially, by resetting the cmd action pointer we should give a chance for O_TAG (F_NOT) to be called and not simply turn match into a no match. The fix for this is very simple and goes like this:

diff --git a/sys/netpfil/ipfw/ip_fw2.c b/sys/netpfil/ipfw/ip_fw2.c
index d2b01fd..57c02dc 100644
--- a/sys/netpfil/ipfw/ip_fw2.c
+++ b/sys/netpfil/ipfw/ip_fw2.c
@@ -2887,7 +2887,8 @@ do {                                                              \
                                  l = f->cmd_len - f->act_ofs;
                                  cmdlen = 0;
                                  match = 1;
-                                 break;
+                                 continue;
+                                 break; /* not reached */
                                }
                                /*
                                 * Dynamic entry not found. If CHECK_STATE,


Now why did it work for you when you used log? Well I'm sure you remember that, because O_LOG is inserted _before_ O_TAG and that O_LOG doesn't have the F_NOT bit set then match is still 1 at the end of the loop and the actual cmd (LOG in this case) will have a chance to execute.

Finally, if your still reading, I think this bug has been in ipfw for a very long time, great catch buddy!

Best,

Karim.
Comment 1 John Baldwin freebsd_committer freebsd_triage 2024-02-09 20:22:45 UTC
I agree with the diagnosis.  I suspect though that the bug is a bit bigger as currently we always skip over the first bug
Comment 2 John Baldwin freebsd_committer freebsd_triage 2024-02-09 20:28:06 UTC
(Hit Enter too soon, ignore previous comment)

I agree with the diagnosis.  I suspect though that the bug is a bit bigger as currently we always skip over the first action opcode.  The fact that 'match' is set to 1 allows this to "work" if the first action is "accept" which is usually the action for keep-state rules.  However, I suspect that if you have a 'log' action on a keep-state rule we don't actually log packets that match an existing dynamic rule since we skip over the "log" opcode due to this bug.

A bit more background: in this set of loops in the kernel, you can think of 'cmd' as being a program counter (PC) for an ISA and 'cmdlen' is the implicit PC increment to perform after handling the current opcode.  Since this action is triggering the equivalent of a branch, it resets 'cmd' and 'l' as is done at the start of the inner for loop and sets 'cmdlen' to 0 to avoid turn the implicit PC increment at the end of the for loop into a nop.

I think though that the patch should drop the 'match = 1' as that is now just noise.  Also, there is no need to keep the dead 'break' statement.  I've cc'd ae@ to see if he has any thoughts, but if there's no other feedback in the next week or so I'll commit the tweaked fix.
Comment 3 fodillemlinkarim 2024-02-09 21:28:13 UTC
Fine by me, the break and match were left there to stay consistent with other parts of the that file that behave in a similar fashion, for example the O_COUNT and O_SKIPTO cases.
Comment 4 Andrey V. Elsukov freebsd_committer freebsd_triage 2024-02-13 02:48:40 UTC
(In reply to John Baldwin from comment #2)

I agree. I just hope the change will not become a big surprise for someone when it start to work :)
Comment 5 John Baldwin freebsd_committer freebsd_triage 2024-02-15 18:23:58 UTC
(In reply to fodillemlinkarim from comment #3)
Do you have a full name you'd like me to use as the author of the git commit for this fix?  So far what I would use is "Karim <fodillemlinkarim@gmail.com>"
Comment 6 fodillemlinkarim 2024-02-15 21:18:41 UTC
(In reply to John Baldwin from comment #5)

Please use  Karim Fodil-Lemelin

Thanks
Comment 7 fodillemlinkarim 2024-02-15 21:28:48 UTC
(In reply to John Baldwin from comment #5)

Since this was work founded by my employer XipLink Inc. might as well use my work email: kfl@xiplink.com

Thanks.
Comment 8 commit-hook freebsd_committer freebsd_triage 2024-02-16 02:11:03 UTC
A commit in branch main references this bug:

URL: https://cgit.FreeBSD.org/src/commit/?id=62b1faa3b7495de22a3225e42dabe6ce8c371e86

commit 62b1faa3b7495de22a3225e42dabe6ce8c371e86
Author:     Karim Fodil-Lemelin <kfl@xiplink.com>
AuthorDate: 2024-02-16 01:57:51 +0000
Commit:     John Baldwin <jhb@FreeBSD.org>
CommitDate: 2024-02-16 01:57:51 +0000

    ipfw: Skip to the start of the loop when following a keep-state rule

    When a packet matches an existing dynamic rule for a keep-state rule,
    the matching engine advances the "instruction pointer" to the action
    portion of the rule skipping over the match conditions.  However, the
    code was merely breaking out of the switch statement rather than doing
    a continue, so the remainder of the loop body after the switch was
    still executed.  If the first action opcode contains an F_NOT but not
    an F_OR (such as an "untag" action), then match is toggled to 0, and
    the code exits the inner loop via a break which aborts processing of
    the actions.

    To fix, just use a continue instead of a break.

    PR:             276732
    Reviewed by:    jhb, ae
    MFC after:      2 weeks

 sys/netpfil/ipfw/ip_fw2.c | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)
Comment 9 commit-hook freebsd_committer freebsd_triage 2024-04-08 19:05:27 UTC
A commit in branch stable/14 references this bug:

URL: https://cgit.FreeBSD.org/src/commit/?id=d2c8cb41d1a2e531737a6da4cb9958bc497477c3

commit d2c8cb41d1a2e531737a6da4cb9958bc497477c3
Author:     Karim Fodil-Lemelin <kfl@xiplink.com>
AuthorDate: 2024-02-16 01:57:51 +0000
Commit:     John Baldwin <jhb@FreeBSD.org>
CommitDate: 2024-04-08 17:57:53 +0000

    ipfw: Skip to the start of the loop when following a keep-state rule

    When a packet matches an existing dynamic rule for a keep-state rule,
    the matching engine advances the "instruction pointer" to the action
    portion of the rule skipping over the match conditions.  However, the
    code was merely breaking out of the switch statement rather than doing
    a continue, so the remainder of the loop body after the switch was
    still executed.  If the first action opcode contains an F_NOT but not
    an F_OR (such as an "untag" action), then match is toggled to 0, and
    the code exits the inner loop via a break which aborts processing of
    the actions.

    To fix, just use a continue instead of a break.

    PR:             276732
    Reviewed by:    jhb, ae
    MFC after:      2 weeks

    (cherry picked from commit 62b1faa3b7495de22a3225e42dabe6ce8c371e86)

 sys/netpfil/ipfw/ip_fw2.c | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)
Comment 10 commit-hook freebsd_committer freebsd_triage 2024-04-08 20:26:37 UTC
A commit in branch stable/13 references this bug:

URL: https://cgit.FreeBSD.org/src/commit/?id=3798c6487a21454020493517b613cda9a1753faf

commit 3798c6487a21454020493517b613cda9a1753faf
Author:     Karim Fodil-Lemelin <kfl@xiplink.com>
AuthorDate: 2024-02-16 01:57:51 +0000
Commit:     John Baldwin <jhb@FreeBSD.org>
CommitDate: 2024-04-08 17:57:57 +0000

    ipfw: Skip to the start of the loop when following a keep-state rule

    When a packet matches an existing dynamic rule for a keep-state rule,
    the matching engine advances the "instruction pointer" to the action
    portion of the rule skipping over the match conditions.  However, the
    code was merely breaking out of the switch statement rather than doing
    a continue, so the remainder of the loop body after the switch was
    still executed.  If the first action opcode contains an F_NOT but not
    an F_OR (such as an "untag" action), then match is toggled to 0, and
    the code exits the inner loop via a break which aborts processing of
    the actions.

    To fix, just use a continue instead of a break.

    PR:             276732
    Reviewed by:    jhb, ae
    MFC after:      2 weeks

    (cherry picked from commit 62b1faa3b7495de22a3225e42dabe6ce8c371e86)

 sys/netpfil/ipfw/ip_fw2.c | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)