From 81d56e19b47668306b8d12f38b9725895ec6b728 Mon Sep 17 00:00:00 2001 From: pexaxep Date: Mon, 19 Oct 2026 19:47:14 +0300 Subject: [PATCH 1/2] Fixed: output pipe deadlock when pipe command output exceeds the socket buffer cligen_output wrote the whole command output to the pipe socketpair with a blocking write(), and only read the pipe command (eg grep) output after the command output was complete. If the pipe command produced more output than fits in the socket buffer it blocked writing, stopped reading its input, and both sides waited for each other forever. Write with select/send and drain the pipe command output to stdout while the socket is not writable. --- cligen_io.c | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/cligen_io.c b/cligen_io.c index 024fc57..e57ab12 100644 --- a/cligen_io.c +++ b/cligen_io.c @@ -47,6 +47,7 @@ #include #include #include +#include #include #include @@ -106,6 +107,57 @@ cli_pipe_output_socket_set(int s) return 0; } +/*! Write to pipe socket without deadlock against the pipe child + * + * The child (eg grep) reads from and writes to the same socketpair. If this process + * blocks in write() while the child blocks writing its (large) output back, both + * wait for each other. Therefore, while the socket is not writable, drain what the + * child has produced to stdout. + * @retval 0 OK + * @retval -1 Error, or child closed its side (EPIPE) + */ +static int +pipe_socket_write(int s, + const char *buf, + size_t len) +{ + size_t off = 0; + ssize_t n; + fd_set rfds, wfds; + char rbuf[4097]; + + while (off < len){ + FD_ZERO(&rfds); + FD_ZERO(&wfds); + FD_SET(s, &rfds); + FD_SET(s, &wfds); + if (select(s+1, &rfds, &wfds, NULL, NULL) < 0){ + if (errno == EINTR) + continue; + return -1; + } + if (FD_ISSET(s, &rfds)){ + if ((n = recv(s, rbuf, sizeof(rbuf)-1, MSG_DONTWAIT)) > 0){ + rbuf[n] = '\0'; + cligen_output_basic(stdout, rbuf, n); + } + else if (n == 0){ + errno = EPIPE; + return -1; + } + else if (errno != EAGAIN && errno != EINTR) + return -1; + } + if (FD_ISSET(s, &wfds)){ + if ((n = send(s, buf+off, len-off, MSG_DONTWAIT|MSG_NOSIGNAL)) > 0) + off += n; + else if (n < 0 && errno != EAGAIN && errno != EINTR) + return -1; + } + } + return 0; +} + /*! Reset cligen_output to initial state * * For new output or when 'q' is pressed that sets d_line to -1 @@ -291,7 +343,7 @@ cligen_output(FILE *f, if (cli_pipe_output_socket_get(&s) < 0) goto done; if (s != -1){ - if (write(s, inbuf, inbuflen) < 0){ + if (pipe_socket_write(s, inbuf, inbuflen) < 0){ switch (errno){ case EPIPE: break; From fda2f9056a062441d05dacbeea15e6644d5c53c7 Mon Sep 17 00:00:00 2001 From: pexaxep Date: Mon, 19 Oct 2026 19:47:14 +0300 Subject: [PATCH 2/2] Added regression test for output pipe deadlock test_pipe.sh: pipe command output (~1MB) larger than the socket buffer must not deadlock with the writer. --- test/test_pipe.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/test_pipe.sh b/test/test_pipe.sh index 684a2cc..18bf437 100755 --- a/test/test_pipe.sh +++ b/test/test_pipe.sh @@ -9,6 +9,11 @@ fspec=$dir/spec.cli lines='"line6 def", "line7 def", "line1 abc", "line2 abc", "line3 abc", "line4 abc", "line5 def"' +# Large output: every line matches grep, total much larger than the socketpair buffer +biglines="" +for i in $(seq 1 30000); do biglines="$biglines\"line$i abc xxxxxxxxxxxxxxxxxxxx\", "; done +biglines="${biglines}\"last abc\"" + cat > $fspec < $fspec <, callback(); + big, output_fn($biglines); treename="|mypipe"; pipetree=""; @@ -92,6 +98,13 @@ expectpart "$(echo "set5 (lag\\\|hostif)-*" | $cligen_file -f $fspec 2>&1)" 0 "v newtest "Escaped vertical bar quotes" expectpart "$(echo "set5 \"(lag|hostif)-*\"" | $cligen_file -f $fspec 2>&1)" 0 "value:(lag|hostif)-*" +# Pipe child output larger than the socket buffer must not deadlock with the writer +newtest "Large output: grep matching every line" +expectpart "$(echo "big \| grep abc" | timeout 30 $cligen_file -f $fspec 2>&1 | grep -c xxxxxxxx)" 0 "30000" + +newtest "Large output: cat" +expectpart "$(echo "big \| cat" | timeout 30 $cligen_file -f $fspec 2>&1 | grep -c xxxxxxxx)" 0 "30000" + newtest "endtest" endtest