udhcpd: fix "not dying on SIGTERM"
authorDenys Vlasenko <vda.linux@googlemail.com>
Sat, 10 Mar 2018 18:01:48 +0000 (19:01 +0100)
committerDenys Vlasenko <vda.linux@googlemail.com>
Sat, 10 Mar 2018 18:41:54 +0000 (19:41 +0100)
Fixes:
commit 52a515d18724bbb34e3ccbbb0218efcc4eccc0a8
"udhcp: use poll() instead of select()"
Feb 16 2017

udhcp_sp_read() is meant to check whether signal pipe indeed has some data to read.
In the above commit, it was changed as follows:

- if (!FD_ISSET(signal_pipe.rd, rfds))
+ if (!pfds[0].revents)
return 0;

The problem is, the check was working for select() purely by accident.
Caught signal interrupts select()/poll() syscalls, they return with EINTR
(regardless of SA_RESTART flag in sigaction). _Then_ signal handler is invoked.
IOW: they can't see any changes to fd state caused by signal haldler
(in our case, signal handler makes signal pipe ready to be read).

For select(), it means that rfds[] bit array is unmodified, bit of signal
pipe's read fd is still set, and the above check "works": it thinks select()
says there is data to read.

This accident does not work for poll(): .revents stays clear, and we do not
try reading signal pipe as we should. In udhcpd, we fall through and block
in socket read. Further SIGTERM signals simply cause socket read to be
interrupted and then restarted (since SIGTERM handler has SA_RESTART=1).

Fixing this as follows: remove the check altogether. Set signal pipe read fd
to nonblocking mode. Always read it in udhcp_sp_read().
If read fails, assume it's EAGAIN and return 0 ("no signal seen").

udhcpd avoids reading signal pipe on every recvd packet by looping if EINTR
(using safe_poll()) - thus ensuring we have correct .revents for all fds -
and calling udhcp_sp_read() only if pfds[0].revents!=0.

udhcpc performs much fewer reads (typically it sleeps >99.999% of the time),
there is no need to optimize it: can call udhcp_sp_read() after each poll
unconditionally.

To robustify socket reads, unconditionally set pfds[1].revents=0
in udhcp_sp_fd_set() (which is before poll), and check it before reading
network socket in udhcpd.

TODO:
This might still fail: if pfds[1].revents=POLLIN, socket read may still block.
There are rare cases when select/poll indicates that data can be read,
but then actual read still blocks (one such case is UDP packets with
wrong checksum). General advise is, if you use a poll/select loop,
keep all your fds nonblocking.
Maybe we should also do that to our network sockets?

function                                             old     new   delta
udhcp_sp_setup                                        55      65     +10
udhcp_sp_fd_set                                       54      60      +6
udhcp_sp_read                                         46      36     -10
udhcpd_main                                         1451    1437     -14
udhcpc_main                                         2723    2708     -15
------------------------------------------------------------------------------
(add/remove: 0/0 grow/shrink: 2/3 up/down: 16/-39)            Total: -23 bytes

Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
examples/var_service/dhcpd_if/run
examples/var_service/dhcpd_if/udhcpc.conf [deleted file]
examples/var_service/dhcpd_if/udhcpd.conf [new file with mode: 0644]
networking/udhcp/common.h
networking/udhcp/d6_dhcpc.c
networking/udhcp/dhcpc.c
networking/udhcp/dhcpd.c
networking/udhcp/signalpipe.c

index de85dece046800f98488e78369a1979a2f63e4b8..a603bdc7122448327fa557502766975299b1afb2 100755 (executable)
@@ -11,13 +11,13 @@ echo "* Upping iface $if"
 ip link set dev $if up
 
 >>udhcpd.leases
-sed 's/^interface.*$/interface '"$if/" -i udhcpc.conf
+sed 's/^interface.*$/interface '"$if/" -i udhcpd.conf
 
 echo "* Starting udhcpd"
 exec \
 env - PATH="$PATH" \
 softlimit \
 setuidgid root \
-udhcpd -f -vv udhcpc.conf
+udhcpd -f -vv udhcpd.conf
 
 exit $?
diff --git a/examples/var_service/dhcpd_if/udhcpc.conf b/examples/var_service/dhcpd_if/udhcpc.conf
deleted file mode 100644 (file)
index a819259..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-# Directives with defaults:
-# start                192.168.0.20
-# end          192.168.0.254
-# interface    eth0
-# max_leases   235
-# auto_time    7200
-# decline_time 3600
-# conflict_time        3600
-# offer_time   60
-# min_lease    60
-# lease_file   /var/lib/misc/udhcpd.leases
-# pidfile      /var/run/udhcpd.pid
-# siaddr       0.0.0.0
-#
-# Directives with no defaults (or with empty defaults):
-# option/opt   NAME VALUE
-# notify_file  /path/to/script_to_run_after_leasefile_is_written
-#              (it is run with $1 = lease_file_name)
-# sname                dhcp_packet_sname_field_contents
-# boot_file    dhcp_packet_bootfile_field_contents
-# static_lease XX:XX:XX:XX:XX:XX IP.ADD.RE.SS
-
-interface if
-pidfile                /dev/null
-lease_file     udhcpd.leases
-option         subnet 255.255.255.0
-option         lease  3600
-#option                router 192.168.0.1
diff --git a/examples/var_service/dhcpd_if/udhcpd.conf b/examples/var_service/dhcpd_if/udhcpd.conf
new file mode 100644 (file)
index 0000000..a819259
--- /dev/null
@@ -0,0 +1,28 @@
+# Directives with defaults:
+# start                192.168.0.20
+# end          192.168.0.254
+# interface    eth0
+# max_leases   235
+# auto_time    7200
+# decline_time 3600
+# conflict_time        3600
+# offer_time   60
+# min_lease    60
+# lease_file   /var/lib/misc/udhcpd.leases
+# pidfile      /var/run/udhcpd.pid
+# siaddr       0.0.0.0
+#
+# Directives with no defaults (or with empty defaults):
+# option/opt   NAME VALUE
+# notify_file  /path/to/script_to_run_after_leasefile_is_written
+#              (it is run with $1 = lease_file_name)
+# sname                dhcp_packet_sname_field_contents
+# boot_file    dhcp_packet_bootfile_field_contents
+# static_lease XX:XX:XX:XX:XX:XX IP.ADD.RE.SS
+
+interface if
+pidfile                /dev/null
+lease_file     udhcpd.leases
+option         subnet 255.255.255.0
+option         lease  3600
+#option                router 192.168.0.1
index 04939e70773ef06b79df0cc2f07d50207df43a9c..13059f10637c8367786cfb15111e1404e9c4c33b 100644 (file)
@@ -314,7 +314,7 @@ int udhcp_send_kernel_packet(struct dhcp_packet *dhcp_pkt,
 
 void udhcp_sp_setup(void) FAST_FUNC;
 void udhcp_sp_fd_set(struct pollfd *pfds, int extra_fd) FAST_FUNC;
-int udhcp_sp_read(struct pollfd *pfds) FAST_FUNC;
+int udhcp_sp_read(void) FAST_FUNC;
 
 int udhcp_read_interface(const char *interface, int *ifindex, uint32_t *nip, uint8_t *mac) FAST_FUNC;
 
index 65ff5deab01c8971e4d9a193a4f1b72ba1e6e11e..28fc7fb83aac9eb9665d2527b4f2c1a584e801b3 100644 (file)
@@ -1378,13 +1378,12 @@ int udhcpc6_main(int argc UNUSED_PARAM, char **argv)
                        /* yah, I know, *you* say it would never happen */
                        timeout = INT_MAX;
                        continue; /* back to main loop */
-               } /* if select timed out */
+               } /* if poll timed out */
 
-               /* select() didn't timeout, something happened */
+               /* poll() didn't timeout, something happened */
 
                /* Is it a signal? */
-               /* note: udhcp_sp_read checks poll result before reading */
-               switch (udhcp_sp_read(pfds)) {
+               switch (udhcp_sp_read()) {
                case SIGUSR1:
                        client_config.first_secs = 0; /* make secs field count from 0 */
                        already_waited_sec = 0;
@@ -1419,7 +1418,7 @@ int udhcpc6_main(int argc UNUSED_PARAM, char **argv)
                }
 
                /* Is it a packet? */
-               if (listen_mode == LISTEN_NONE || !pfds[1].revents)
+               if (!pfds[1].revents)
                        continue; /* no */
 
                {
index 55f21c1870496dfdd729fd9927037b8dc70321a9..fd18325c19445a0c41433ee1fdb3cd77ae2e30e5 100644 (file)
@@ -1576,13 +1576,12 @@ int udhcpc_main(int argc UNUSED_PARAM, char **argv)
                        /* yah, I know, *you* say it would never happen */
                        timeout = INT_MAX;
                        continue; /* back to main loop */
-               } /* if select timed out */
+               } /* if poll timed out */
 
-               /* select() didn't timeout, something happened */
+               /* poll() didn't timeout, something happened */
 
                /* Is it a signal? */
-               /* note: udhcp_sp_read checks poll result before reading */
-               switch (udhcp_sp_read(pfds)) {
+               switch (udhcp_sp_read()) {
                case SIGUSR1:
                        client_config.first_secs = 0; /* make secs field count from 0 */
                        already_waited_sec = 0;
@@ -1617,7 +1616,7 @@ int udhcpc_main(int argc UNUSED_PARAM, char **argv)
                }
 
                /* Is it a packet? */
-               if (listen_mode == LISTEN_NONE || !pfds[1].revents)
+               if (!pfds[1].revents)
                        continue; /* no */
 
                {
index 238542bb0c363e26186ae5284eb13edcf311906b..f1368cc4d3bb95ce2c6e8abe9746862306c00da5 100644 (file)
@@ -915,20 +915,18 @@ int udhcpd_main(int argc UNUSED_PARAM, char **argv)
 
                udhcp_sp_fd_set(pfds, server_socket);
                tv = timeout_end - monotonic_sec();
-               retval = 0;
-               if (!server_config.auto_time || tv > 0) {
-                       retval = poll(pfds, 2, server_config.auto_time ? tv * 1000 : -1);
-               }
-               if (retval == 0) {
-                       write_leases();
-                       goto continue_with_autotime;
-               }
-               if (retval < 0 && errno != EINTR) {
-                       log1("error on select");
-                       continue;
+               /* Block here waiting for either signal or packet */
+               retval = safe_poll(pfds, 2, server_config.auto_time ? tv * 1000 : -1);
+               if (retval <= 0) {
+                       if (retval == 0) {
+                               write_leases();
+                               goto continue_with_autotime;
+                       }
+                       /* < 0 and not EINTR: should not happen */
+                       bb_perror_msg_and_die("poll");
                }
 
-               switch (udhcp_sp_read(pfds)) {
+               if (pfds[0].revents) switch (udhcp_sp_read()) {
                case SIGUSR1:
                        bb_error_msg("received %s", "SIGUSR1");
                        write_leases();
@@ -938,12 +936,16 @@ int udhcpd_main(int argc UNUSED_PARAM, char **argv)
                        bb_error_msg("received %s", "SIGTERM");
                        write_leases();
                        goto ret0;
-               case 0: /* no signal: read a packet */
-                       break;
-               default: /* signal or error (probably EINTR): back to select */
-                       continue;
                }
 
+               /* Is it a packet? */
+               if (!pfds[1].revents)
+                       continue; /* no */
+
+               /* Note: we do not block here, we block on poll() instead.
+                * Blocking here would prevent SIGTERM from working:
+                * socket read inside this call is restarted on caught signals.
+                */
                bytes = udhcp_recv_kernel_packet(&packet, server_socket);
                if (bytes < 0) {
                        /* bytes can also be -2 ("bad packet data") */
index b101b4ce4e1040abf2cff45f066139774fafad21..2ff78f0f25c788ee3c3a46488d611ec4b41ba099 100644 (file)
@@ -40,6 +40,7 @@ void FAST_FUNC udhcp_sp_setup(void)
        xpiped_pair(signal_pipe);
        close_on_exec_on(signal_pipe.rd);
        close_on_exec_on(signal_pipe.wr);
+       ndelay_on(signal_pipe.rd);
        ndelay_on(signal_pipe.wr);
        bb_signals(0
                + (1 << SIGUSR1)
@@ -61,20 +62,20 @@ void FAST_FUNC udhcp_sp_fd_set(struct pollfd pfds[2], int extra_fd)
                pfds[1].fd = extra_fd;
                pfds[1].events = POLLIN;
        }
+       /* this simplifies "is extra_fd ready?" tests elsewhere: */
+       pfds[1].revents = 0;
 }
 
 /* Read a signal from the signal pipe. Returns 0 if there is
  * no signal, -1 on error (and sets errno appropriately), and
  * your signal on success */
-int FAST_FUNC udhcp_sp_read(struct pollfd pfds[2])
+int FAST_FUNC udhcp_sp_read(void)
 {
        unsigned char sig;
 
-       if (!pfds[0].revents)
-               return 0;
-
+       /* Can't block here, fd is in nonblocking mode */
        if (safe_read(signal_pipe.rd, &sig, 1) != 1)
-               return -1;
+               return 0;
 
        return sig;
 }