There's a (subtle) bug in anchor creation/handling I haven't quite
pinned down yet:

Nested brace anchors with names end up being loaded under a different
name if their ruleset is empty:

        $ pfctl -aa1 -vnf-
        anchor a2 {
        }
        match
        ^D
        anchor "a1/a2" all
        match all

        $ pfctl -vnf-
        anchor a1 {
                anchor a2 {
                }
                match
        }
        ^D
        anchor "a1" all {
          anchor "_1/a2" all
            match all
        }

But it works fine without nesting or anonymous brace anchors:

        $ pfctl -vnf-
        $ pfctl -vnf-
        anchor a1 {
        }
        anchor a2 {
                anchor {
                }
        }
        anchor {
        }
        ^D
        anchor "a1" all
        anchor "a2" all {
          anchor all
        }
        anchor all

Due to how the parser works, an anchor's ruleset `{ ... }' is fully
parsed before the anchor rule `anchor a' itself, so a temporary anchor
gets created to load the ruleset, continue parsing and eventually move
it place the anchor rule completes (see sbin/pfctl/parse.y:836).

That's where these "_1/a2" come from; anchor names starting with an
underscore are reservered for internal use and therefore rejected on
user-input.  Any attempt to fill them will fail, hence one must at least
completely refill the parent anchor to get rid of them.


Still looking into the underlying problem, I'd like to generally prevent
such usage of empty brace-delimited blocks as they're of no use and will
only cause trouble.

* `anchor a {}' is equivalent to `anchor a'
* `anchor {}' cannot be filled with `pfctl -a' as it has no name and
  `anchor' is invalid, so that is even more useless

If one ever used brace anchors with empty rulesets, above things would
happen.  If not, anchors are either filled right away or braces are left
out in the first place, so I'd argue throwing a syntax error here is a
breaking syntax change without actual breakage - quite the opposite.


Thanks to benno for going into yacc details with me.
pfctl regress passes.

Feedback? Objections? OK?

Index: share/man/man5/pf.conf.5
===================================================================
RCS file: /cvs/src/share/man/man5/pf.conf.5,v
retrieving revision 1.577
diff -u -p -r1.577 pf.conf.5
--- share/man/man5/pf.conf.5    12 Jul 2018 05:54:49 -0000      1.577
+++ share/man/man5/pf.conf.5    30 Dec 2018 19:54:52 -0000
@@ -1822,7 +1822,7 @@ An anchor rule can also contain a filter
 in a brace-delimited block.
 In that case, no separate loading of rules into the anchor
 is required.
-Brace delimited blocks may contain rules or other brace-delimited blocks.
+Brace delimited blocks must contain rules or other brace-delimited blocks.
 When an anchor is populated this way the anchor name becomes optional.
 Since the parser specification for anchor names is a string,
 double quote characters
Index: sbin/pfctl/parse.y
===================================================================
RCS file: /cvs/src/sbin/pfctl/parse.y,v
retrieving revision 1.688
diff -u -p -r1.688 parse.y
--- sbin/pfctl/parse.y  15 Nov 2018 03:22:01 -0000      1.688
+++ sbin/pfctl/parse.y  30 Dec 2018 19:58:16 -0000
@@ -501,6 +501,7 @@ int parseport(char *, struct range *r, i
 %type  <v.weight>              optweight
 %type  <v.i>                   dir af optimizer syncookie_val
 %type  <v.i>                   sourcetrack flush unaryop statelock
+%type  <v.i>                   pfa_anchorlist
 %type  <v.b>                   action
 %type  <v.b>                   flags flag blockspec prio
 %type  <v.range>               portplain portstar portrange
@@ -813,11 +814,11 @@ anchorname        : STRING                        { $$ = 
$1; }
                | /* empty */                   { $$ = NULL; }
                ;
 
-pfa_anchorlist : /* empty */
-               | pfa_anchorlist '\n'
-               | pfa_anchorlist pfrule '\n'
-               | pfa_anchorlist anchorrule '\n'
-               | pfa_anchorlist include '\n'
+pfa_anchorlist : /* empty */                           { $$ = 0; }
+               | pfa_anchorlist '\n'                   { $$ = 1; }
+               | pfa_anchorlist pfrule '\n'            { $$ = 1; }
+               | pfa_anchorlist anchorrule '\n'        { $$ = 1; }
+               | pfa_anchorlist include '\n'           { $$ = 1; }
                ;
 
 pfa_anchor     : '{'
@@ -844,6 +845,10 @@ pfa_anchor : '{'
                        pf->anchor = rs->anchor;
                } '\n' pfa_anchorlist '}'
                {
+                       if (!$4) {
+                               yyerror("brace anchors must not be empty");
+                               YYERROR;
+                       }
                        pf->alast = pf->anchor;
                        pf->asd--;
                        pf->anchor = pf->astack[pf->asd];

Reply via email to