Date: Sat, 8 Dec 2001 07:45:40 -0500 (EST) From: Dan Pelleg <peldan@yahoo.com> To: FreeBSD-gnats-submit@freebsd.org Subject: kern/32600: [PATCH] incorrect handling of parent rules in ipfw Message-ID: <200112081245.fB8Cjel13266@palraz.wburn>
next in thread | raw e-mail | index | archive | help
>Number: 32600 >Category: kern >Synopsis: [PATCH] incorrect handling of parent rules in ipfw >Confidential: no >Severity: serious >Priority: low >Responsible: freebsd-bugs >State: open >Quarter: >Keywords: >Date-Required: >Class: sw-bug >Submitter-Id: current-users >Arrival-Date: Sat Dec 08 04:50:00 PST 2001 >Closed-Date: >Last-Modified: >Originator: Dan Pelleg >Release: FreeBSD 4.4-STABLE i386 >Organization: >Environment: System: FreeBSD p 4.4-STABLE FreeBSD 4.4-STABLE #0: Sat Nov 10 15:10:25 EST 2001 d@k:/usr/obj/usr/src/sys/P i386 >Description: when I started to use limit rules, I noticed ipfw was emitting lots of messages: OUCH! cannot remove rule, count 2 Inspecting ip_fw.c, this is caused when a parent rule with a nonzero count is detected to have expired. Here is one scenario how this can legally happen: For example, lookup_dyn_parent() increases the expiry by dyn_short_lifetime, whereas add_dyn_rule() will create it with time_second + dyn_syn_lifetime. So, when a second limit rule is created, the parent's expire field is not extended by enough time to match this second child. Luigi Rizzo confirmed this analysis and on his advice I patched to ignore the expire field altogether for limit rules. Note that the (userland) ipfw still takes the expire field into account, as exemplified by the following scenario: Suppose you have a "limit" rule, and a rule is created for it, and a minute later 4 more rules are created for it. So the count is 5. Now terminate the first connection. Suppose you have net.inet.ip.fw.*lifetime set to huge values like I do. After a while, the LIMIT rule will expire, and "ipfw -d show" will not show it anymore. However, only when it gets freed will the count value for its parent be decreased. So, in the meantime, when you do a "ipfw -d" show, you see "PARENT 5", but only 4 rules that match it. Another thing that can happen is for the PARENT rule to expire, and then you see LIMIT rules listed but not their parent. At that point I added a patch to ipfw to list PARENT rules with count > 0 regardless of their expire value. (this still doesn't solve the problem of the number of counts for the parent being larger than the number of children ipfw lists; I started solving this one by having ipfw update the reference counter in the parent, and then realized that ip_fw.c doesn't pass parent pointers - at least not usable ones - the patch includes a comment about this). Now, running this patched code gave me kernel panics in add_dyn_rule(), called from install_state(). I came up with this explanation for them: Suppose you have plenty of rules (ie, conn_limit of them) for a parent, but they are all expired. This happens to me when I use Mozilla, which opens dozens of connections, and then leave that window alone for a while. In the DYN_LIMIT case of install_state(), the lookup_dyn_parent finds the parent, which is found out to have too large a count. Then EXPIRE_DYN_CHAIN is called, the count goes down to zero in the first pass, and the parent is removed in the second. But install_state is still holding a pointer to the freed structure, and later passes it on to add_dyn_rule(). I fixed this by re-looking the parent up after the expiry code, which has solved the problem. >How-To-Repeat: see above. >Fix: apply provided patch. *** sys/netinet/ip_fw.c.orig Sun Nov 18 18:29:23 2001 --- sys/netinet/ip_fw.c Mon Nov 26 07:03:08 2001 *************** *** 649,655 **** /* remove a refcount to the parent */ \ if (q->dyn_type == DYN_LIMIT) \ q->parent->count--; \ ! DEB(printf("-- unlink entry 0x%08x %d -> 0x%08x %d, %d left\n", \ (q->id.src_ip), (q->id.src_port), \ (q->id.dst_ip), (q->id.dst_port), dyn_count-1 ); ) \ if (prev != NULL) \ --- 649,656 ---- /* remove a refcount to the parent */ \ if (q->dyn_type == DYN_LIMIT) \ q->parent->count--; \ ! DEB(printf("-- unlink entry %p 0x%08x %d -> 0x%08x %d, %d left\n", \ ! q, \ (q->id.src_ip), (q->id.src_port), \ (q->id.dst_ip), (q->id.dst_port), dyn_count-1 ); ) \ if (prev != NULL) \ *************** *** 694,710 **** * and possibly more in the future. */ int zap = ( rule == NULL || rule == q->rule); ! if (zap) ! zap = force || TIME_LEQ( q->expire , time_second ); /* do not zap parent in first pass, record we need a second pass */ if (q->dyn_type == DYN_LIMIT_PARENT) { max_pass = 1; /* we need a second pass */ if (zap == 1 && (pass == 0 || q->count != 0) ) { zap = 0 ; ! if (pass == 1) /* should not happen */ ! printf("OUCH! cannot remove rule, count %d\n", ! q->count); } } if (zap) { UNLINK_DYN_RULE(prev, ipfw_dyn_v[i], q); --- 695,718 ---- * and possibly more in the future. */ int zap = ( rule == NULL || rule == q->rule); ! /* do not zap parent in first pass, record we need a second pass */ if (q->dyn_type == DYN_LIMIT_PARENT) { max_pass = 1; /* we need a second pass */ if (zap == 1 && (pass == 0 || q->count != 0) ) { zap = 0 ; ! if (force && pass == 1) { /* should not happen */ ! printf("OUCH! cannot remove rule %p 0x%08x %d -> 0x%08x %d, count %d, bucket %d\n", ! q, ! (q->id.src_ip), (q->id.src_port), ! (q->id.dst_ip), (q->id.dst_port), ! q->count, ! i); ! } } + } else { + if (zap) + zap = force || TIME_LEQ( q->expire , time_second ); } if (zap) { UNLINK_DYN_RULE(prev, ipfw_dyn_v[i], q); *************** *** 882,891 **** r->next = ipfw_dyn_v[i] ; ipfw_dyn_v[i] = r ; dyn_count++ ; ! DEB(printf("-- add entry 0x%08x %d -> 0x%08x %d, total %d\n", (r->id.src_ip), (r->id.src_port), (r->id.dst_ip), (r->id.dst_port), ! dyn_count ); ) return r; } --- 890,901 ---- r->next = ipfw_dyn_v[i] ; ipfw_dyn_v[i] = r ; dyn_count++ ; ! DEB(printf("-- add entry %p 0x%08x %d -> 0x%08x %d, total %d, bucket %d\n", ! r, (r->id.src_ip), (r->id.src_port), (r->id.dst_ip), (r->id.dst_port), ! dyn_count, ! i); ) return r; } *************** *** 988,995 **** --- 998,1017 ---- } if (parent->count >= conn_limit) { EXPIRE_DYN_CHAIN(rule); /* try to expire some */ + /* + The expiry might have removed the parent too. + We lookup again, which will re-create if necessary. + */ + parent = lookup_dyn_parent(&id, rule); + if (parent == NULL) { + printf("add parent failed\n"); + return 1; + } if (parent->count >= conn_limit) { + if (last_log != time_second) { + last_log = time_second ; printf("drop session, too many entries\n"); + } return 1; } } *************** *** 1929,1934 **** --- 1951,1962 ---- bcopy(p, dst, sizeof *p); (int)dst->rule = p->rule->fw_number ; /* + * we should really set the parent field + * to the corresponding parent in the new + * structure. For now, just set it to NULL. + */ + dst->parent = NULL ; + /* * store a non-null value in "next". The userland * code will interpret a NULL here as a marker * for the last dynamic rule. *** sbin/ipfw/ipfw.c.orig Sun Nov 18 18:42:51 2001 --- sbin/ipfw/ipfw.c Sat Nov 24 12:18:27 2001 *************** *** 813,822 **** (struct ipfw_dyn_rule *)&rules[num]; struct in_addr a; struct protoent *pe; printf("## Dynamic rules:\n"); for (;; d++) { ! if (d->expire == 0 && !do_expired) { if (d->next == NULL) break; continue; --- 813,830 ---- (struct ipfw_dyn_rule *)&rules[num]; struct in_addr a; struct protoent *pe; + int skip; printf("## Dynamic rules:\n"); for (;; d++) { ! /* determine whether to skip this rule */ ! skip = !do_expired; ! if( d->dyn_type == DYN_LIMIT_PARENT) { ! skip = skip && d->count == 0; ! } else { ! skip = skip && d->expire == 0; ! } ! if(skip) { if (d->next == NULL) break; continue; >Release-Note: >Audit-Trail: >Unformatted: To Unsubscribe: send mail to majordomo@FreeBSD.org with "unsubscribe freebsd-bugs" in the body of the message
Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?200112081245.fB8Cjel13266>