sword in the stone   Hayne of Tintagel
Mac OS X  /  Programming  /  Bug with 'ipfw zero'

Bug with 'ipfw zero' ?

There seems to be a bug in the ipfw command in OS X 10.2.6.
The command 'sudo ipfw zero' doesn't work unless you supply a rule number. The man page says that the rule number is optional. If you don't supply a rule number, it is supposed to zero the counters for all rules. But instead it gives an error:
"ipfw: setsockopt(IP_FW_ZERO): Invalid argument".

What follows is my analysis of the problem from looking at the Darwin source code (version 10.2.6).

The ipfw command souce code is in network_cmds/ipfw.tproj/ipfw.c where the relevant function is:

static void
zero (ac, av)
        int ac;
        char **av;
{
        struct ip_fw rule;
        memset(&rule, 0, sizeof rule);
        rule.version = IP_FW_CURRENT_API_VERSION;  

        av++; ac--;

        if (!ac) {
                /* clear all entries */
                if (setsockopt(s, IPPROTO_IP, IP_FW_ZERO,
                                  &rule, sizeof rule) < 0)
                        err(EX_UNAVAILABLE, "setsockopt(%s)", "IP_FW_ZERO");
                if (!do_quiet)
                        printf("Accounting cleared.\n");
        } else {
                int failed = EX_OK;

                while (ac) {
                        /* Rule number */
                        if (isdigit(**av)) {
                                rule.fw_number = atoi(*av); av++; ac--;
                                if (setsockopt(s, IPPROTO_IP,
                                    IP_FW_ZERO, &rule, sizeof rule)) {
                                        warn("rule %u: setsockopt(%s)",
                                                 rule.fw_number, "IP_FW_ZERO");
                                        failed = EX_UNAVAILABLE;
                                }
                                else if (!do_quiet)
                                        printf("Entry %d cleared\n",
                                            rule.fw_number);
                        } else
                                show_usage("invalid rule number ``%s''", *av);
                }
                if (failed != EX_OK)
                        exit(failed);
        }
}
The 'zero' function is called with the argc & argv from main() and hence 'ac' is 1 if no rule number was given in the 'ipfw zero' command. So we see that in this case, it invokes 'setsockopt' with the sopt_val a pointer to a 'struct ip_fw' that is all zeros except for the 'version' field.

The 'setsockopt' call for IP_FW_ZERO is implemented by the 'ip_fw_ctl' function in the file IPFirewall/IPFirewall.kmodproj/ip_fw.c

static int
ip_fw_ctl(struct sockopt *sopt)
{
        int error, s;
        int command;
        size_t size;
        u_int32_t apiVersion;
        struct ip_fw_chain *fcp;
        struct ip_fw frwl, *buf;
        struct ip_old_fw *oldRulePtr;

        /*
         * Disallow modifications in really-really secure mode, but still allow
         * the logging counters to be reset.
         */
        if (securelevel >= 3 &&
                (sopt->sopt_name == IP_FW_ADD
                || sopt->sopt_name == IP_OLD_FW_ADD ||
             (sopt->sopt_dir == SOPT_SET && sopt->sopt_name != IP_FW_RESETLOG &&
             sopt->sopt_name != IP_OLD_FW_RESETLOG)))
                        return (EPERM);

        error = CopyInRule(sopt, &frwl, &apiVersion, &command);
        if (error) return error;

        switch (command) {
        case IP_FW_GET:
                // snip - code omitted
                break;
                
        case IP_FW_FLUSH:
                // snip - code omitted
                break;
                
        case IP_FW_ZERO:
                error = zero_entry(sopt->sopt_val ? &frwl : 0);
                break;
        
        case IP_FW_ADD:
        	// snip - code omitted
                break;
                
        case IP_FW_DEL:
        	// snip - code omitted
                break;
                
        case IP_FW_RESETLOG:
        	// snip - code omitted
                break;
                
        default:
                printf("ip_fw_ctl invalid option %d\n", command);
                error = EINVAL ;
        }

        return (error);
}
Since (as noted above) we are calling 'ip_fw_ctl' with a socket option value which points to a 'struct ipfw' that is all zeros except for the 'version' field, and since (in our case) 'CopyInRule' merely copies the socket option value into the 'frwl' variable, 'sopt->sopt_val' is not zero and hence this routine invokes 'zero_entry' with a non-zero pointer to a 'struct ipfw'.

The function 'zero_entry' is in the same file:

static int
zero_entry(struct ip_fw *frwl)
{
        struct ip_fw_chain *fcp;
        int s, cleared;

        if (frwl == 0) {
                s = splnet();
                LIST_FOREACH(fcp, &ip_fw_chain_head, next) {     
                        fcp->rule->fw_bcnt = fcp->rule->fw_pcnt = 0;  
                        fcp->rule->fw_loghighest = fcp->rule->fw_logamount;
                        fcp->rule->timestamp = 0;
                }       
                splx(s);
        }
        else {
                cleared = 0;

                /*
                 *      It's possible to insert multiple chain entries with the
                 *      same number, so we don't stop after finding the first
                 *      match if zeroing a specific entry.
                 */
                LIST_FOREACH(fcp, &ip_fw_chain_head, next)
                        if (frwl->fw_number == fcp->rule->fw_number) {
                                s = splnet();
                                while (fcp && frwl->fw_number
                                              == fcp->rule->fw_number) {
                                        fcp->rule->fw_bcnt 
                                             = fcp->rule->fw_pcnt = 0;
                                        fcp->rule->fw_loghighest =
                                            fcp->rule->fw_logamount;
                                        fcp->rule->timestamp = 0;
                                        fcp = LIST_NEXT(fcp, next);
                                }
                                splx(s);
                                cleared = 1;
                                break;
                        }
                if (!cleared)   /* we didn't find any matching rules */
                        return (EINVAL);
        }

        if (fw_verbose) {
                if (frwl)
                        log(LOG_AUTHPRIV | LOG_NOTICE,
                            "ipfw: Entry %d cleared.\n", frwl->fw_number);
                else
                        log(LOG_AUTHPRIV | LOG_NOTICE,
                            "ipfw: Accounting cleared.\n");
        }

        return (0);
}
Since we are calling 'zero_entry' with a non-zero pointer, it goes into the "else" block and (since the structure was zeroed), frwl->fw_number is 0, so none of the rules will match and hence we trigger the "if (!cleared)" and get an error. This is the explanation of the observed behaviour when 'ipfw zero' is executed without giving a rule number.

So it would seem to be a bug that was probably introduced when the code moved from the "old" to the "new" API. This is evidenced by the following comment in the function 'CopyInRule' (which is in the file IPFirewall/IPFirewall.kmodproj/IPFirewall.c):

"In the old-style API, it was legal to not pass in a rule structure for certain firewall operations (e.g. flush, reset log). If that's the situation, we pretend we received a blank structure."

Suggested fix

It seems that what needs to be fixed is in the function 'zero_entry'.
The line:
if (frwl == 0) {
should be changed to be:
if (frwl->fw_number == 0) {
Note that it is likely that a similar fix needs to be made for 'ipfw resetlog' as well.