Component and version
NethSecurity 8.8.0 — firewall4 (firewall4-2025.03.17~b6e51575-r2)
Actual behavior
On firewalls with many port forwards and many zones, applying the firewall configuration fails halfway through building the rules. The new rules are then never loaded: rules are applied all-or-nothing, so the incomplete set is discarded and the firewall keeps enforcing its previous rules. The command still reports success.
The practical consequence: an administrator can close a port, remove an access rule, or add a port forward, see no error at all, and reasonably believe the change is active — when it is not. The firewall keeps applying the old rules until someone notices.
fw4 reload prints:
Runtime error: Unable to open source file /usr/share/firewall4/templates/redirect.uc: No file descriptors available
In main(), file /usr/share/firewall4/templates/ruleset.uc, line 338, byte 53:
called from function include ([C])
called from function render_ruleset (/usr/share/firewall4/main.uc:58:72)
/dev/stdin:1698:3-3: Error: syntax error, unexpected end of file
Note that fw4 reload still exits with status 0, so scripts and automation cannot detect the failure.
Seen on a production firewall with 315 configuration sections (66 port forwards, 23 zones).
Expected behavior
Either the configuration is applied successfully, or the command fails visibly with a non-zero exit status.
Cause
While building the rules, one file is opened for each rule template used, and none are closed until the whole set is finished. Every port forward also multiplies this, because each "reflection" zone listed on a port forward is rendered separately.
Large configurations therefore exceed the limit of 1024 open files per process. Measured on the test configuration below: 1060 files needed against 1024 allowed — only ~36 over. This means affected systems sit just barely over the threshold, and adding a single port forward can be enough to trigger it. It also makes the problem easy to miss in testing: one fewer port forward, or a WAN interface without an address, keeps it just under the limit and everything appears fine.
Steps to reproduce
Requires a test machine with a spare, unused network card, and a WAN interface that has an IP address.
-
Save the script below as /tmp/gen.sh and run it (it backs up the current configuration to /root/firewall.bak first):
-
Apply the configuration:
-
The error above is printed. Confirm the new rules were not applied — the count is unchanged from before the reload:
nft list table inet fw4 | grep -c "dnat ip to"
-
Restore:
cp /root/firewall.bak /etc/config/firewall && fw4 reload
Script to generate the test configuration (320 sections)
#!/bin/sh
# Generates a large firewall configuration that reproduces the
# "No file descriptors available" failure of `fw4 reload`.
#
# Run on a TEST machine only. Backs up the current config first.
# Restore with: cp /root/firewall.bak /etc/config/firewall && fw4 reload
set -e
SPARE_NIC="${SPARE_NIC:-eth2}" # any spare, unused network card
cp /etc/config/firewall /root/firewall.bak
echo "Current configuration saved to /root/firewall.bak"
# A second local network. The extra zones below are attached to it: zones
# without a real network card produce far fewer rules and will NOT trigger
# the problem.
uci set network.lan2=interface
uci set network.lan2.device="$SPARE_NIC"
uci set network.lan2.proto='static'
uci set network.lan2.ipaddr='10.10.30.2'
uci set network.lan2.netmask='255.255.255.0'
uci commit network
ifup lan2
ZONES="dmz guest ipsec openvpn rwopenvpn vlan101 vlan102 vlan103 vlan104 vlan105
vlan106 vlan107 vlan108 vlan109 vlan110 vlan111 vlan112 vlan113 vlan114 vlan115 vlan116"
# 21 extra zones
for z in $ZONES; do
uci add firewall zone >/dev/null
uci set firewall.@zone[-1].name="$z"
uci add_list firewall.@zone[-1].network='lan2'
uci set firewall.@zone[-1].input='ACCEPT'
uci set firewall.@zone[-1].output='ACCEPT'
uci set firewall.@zone[-1].forward='ACCEPT'
done
ALLZONES="lan wan $ZONES"
# 66 port forwards, each reflected into 3 zones (this is the main multiplier)
i=0
for port in $(seq 20001 20066); do
i=$((i + 1))
uci add firewall redirect >/dev/null
uci set firewall.@redirect[-1].name="ns_pf${i}"
uci set firewall.@redirect[-1].src='wan'
uci set firewall.@redirect[-1].dest='lan'
uci set firewall.@redirect[-1].target='DNAT'
uci set firewall.@redirect[-1].proto='tcp'
uci set firewall.@redirect[-1].src_dport="$port"
uci set firewall.@redirect[-1].dest_ip='192.168.100.50'
uci set firewall.@redirect[-1].dest_port="$port"
uci set firewall.@redirect[-1].reflection='1'
for off in 0 7 13; do
z=$(echo $ALLZONES | tr ' ' '\n' | sed -n "$(((i + off) % 23 + 1))p")
uci add_list firewall.@redirect[-1].reflection_zone="$z"
done
done
# 185 zone-to-zone traffic rules
count=0
for s in $ALLZONES; do
for d in $ALLZONES; do
[ "$s" = "$d" ] && continue
count=$((count + 1))
[ $count -gt 185 ] && break 2
uci add firewall forwarding >/dev/null
uci set firewall.@forwarding[-1].src="$s"
uci set firewall.@forwarding[-1].dest="$d"
done
done
# 18 traffic rules
for i in $(seq 1 18); do
uci add firewall rule >/dev/null
uci set firewall.@rule[-1].name="ns_rule_${i}"
uci set firewall.@rule[-1].src='wan'
uci set firewall.@rule[-1].proto='tcp'
uci set firewall.@rule[-1].dest_port="$((40000 + i))"
uci set firewall.@rule[-1].target='ACCEPT'
done
# 12 IP sets
for i in $(seq 1 12); do
uci add firewall ipset >/dev/null
uci set firewall.@ipset[-1].name="ns_ipset_${i}"
uci add_list firewall.@ipset[-1].match='src_net'
done
# 8 NAT rules
for i in $(seq 1 8); do
uci add firewall nat >/dev/null
uci set firewall.@nat[-1].name="ns_nat_${i}"
uci set firewall.@nat[-1].target='SNAT'
uci set firewall.@nat[-1].snat_ip="192.168.100.$((200 + i))"
uci set firewall.@nat[-1].src='lan'
done
uci commit firewall
echo "Generated $(grep -c '^config' /etc/config/firewall) configuration sections."
echo "Now run: fw4 reload"
Workaround
Allow the process to open more files. Run on the affected firewall:
cp /sbin/fw4 /root/fw4.orig-backup
grep -q 'ulimit -HSn 8192' /sbin/fw4 || sed -i '/^set -o pipefail/a ulimit -HSn 8192 2>/dev/null' /sbin/fw4
Then fw4 reload completes and the configuration is applied correctly.
Tested: it is safe to run more than once, and it still works when the caller passes down a lower limit.
This is only a temporary workaround — /sbin/fw4 ships with the firmware, so the change is lost on the next update.
Suggested fix
Ship the same ulimit line in the firewall4 package so it survives updates.
The underlying defect is that template files are held open until the whole set of rules has been built; they could be released as soon as each template has been read. That part is upstream in ucode. Raising the limit is a much smaller change and is enough to resolve this in practice, though it raises a ceiling rather than removing the cause (8192 covers roughly 2500 configuration sections).
Resources
Related upstream issue: openwrt/openwrt#24598
Component and version
NethSecurity 8.8.0 —
firewall4(firewall4-2025.03.17~b6e51575-r2)Actual behavior
On firewalls with many port forwards and many zones, applying the firewall configuration fails halfway through building the rules. The new rules are then never loaded: rules are applied all-or-nothing, so the incomplete set is discarded and the firewall keeps enforcing its previous rules. The command still reports success.
The practical consequence: an administrator can close a port, remove an access rule, or add a port forward, see no error at all, and reasonably believe the change is active — when it is not. The firewall keeps applying the old rules until someone notices.
fw4 reloadprints:Note that
fw4 reloadstill exits with status 0, so scripts and automation cannot detect the failure.Seen on a production firewall with 315 configuration sections (66 port forwards, 23 zones).
Expected behavior
Either the configuration is applied successfully, or the command fails visibly with a non-zero exit status.
Cause
While building the rules, one file is opened for each rule template used, and none are closed until the whole set is finished. Every port forward also multiplies this, because each "reflection" zone listed on a port forward is rendered separately.
Large configurations therefore exceed the limit of 1024 open files per process. Measured on the test configuration below: 1060 files needed against 1024 allowed — only ~36 over. This means affected systems sit just barely over the threshold, and adding a single port forward can be enough to trigger it. It also makes the problem easy to miss in testing: one fewer port forward, or a WAN interface without an address, keeps it just under the limit and everything appears fine.
Steps to reproduce
Requires a test machine with a spare, unused network card, and a WAN interface that has an IP address.
Save the script below as
/tmp/gen.shand run it (it backs up the current configuration to/root/firewall.bakfirst):Apply the configuration:
The error above is printed. Confirm the new rules were not applied — the count is unchanged from before the reload:
Restore:
cp /root/firewall.bak /etc/config/firewall && fw4 reloadScript to generate the test configuration (320 sections)
Workaround
Allow the process to open more files. Run on the affected firewall:
Then
fw4 reloadcompletes and the configuration is applied correctly.Tested: it is safe to run more than once, and it still works when the caller passes down a lower limit.
This is only a temporary workaround —
/sbin/fw4ships with the firmware, so the change is lost on the next update.Suggested fix
Ship the same
ulimitline in thefirewall4package so it survives updates.The underlying defect is that template files are held open until the whole set of rules has been built; they could be released as soon as each template has been read. That part is upstream in
ucode. Raising the limit is a much smaller change and is enough to resolve this in practice, though it raises a ceiling rather than removing the cause (8192 covers roughly 2500 configuration sections).Resources
Related upstream issue: openwrt/openwrt#24598