On Mon, 2021-08-09 at 13:19 +0200, Ingo Schwarze wrote: > Hi, > > as mentioned earlier, deraadt@ reported that sftp(1) ignores Ctrl-C. > Fixing that without longjmp(3) requires making editline(3) better > behaved. > > Currently, when read(2) from the terminal gets interrupted by a > signal, editline(3) ignores the (first) signal and unconditionally > calls read(2) a second time. That seems wrong: if the user hits > Ctrl-C, it is sane to assume that they meant it, not that they > want it ignored. > > The following patch causes el_gets(3) and el_wgets(3) to return > failure when read(2)ing from the terminal is interrupted by a > signal other than SIGCONT or SIGWINCH. That allows the calling > program to decide what to do, usually either exit the program or > provide a fresh prompt to the user. > > If i know how to grep(1), the following programs in the base system > use -ledit: > > * bc(1) > It behaves well with the patch: Ctrl-C discards the current > input line and starts a new input line. > The reason why this already works even without the patch > is that bc(1) does very scary stuff inside the signal handler: > pass a file-global EditLine pointer on the heap to el_line(3) > and access fields inside the returned struct. Needless to > say that no signal handler should do such things... > > * cdio(1) > Behaviour is acceptable and unchanged with the patch: > Ctrl-C exits cdio(1). > > * ftp(1) > It behaves well with the patch: Ctrl-C discards the current > input line and provides a fresh prompt. > The reason why it already works without the patch is that ftp(1) > uses setjmp(3)/longjmp(3) to forcefully grab back control > from el_gets(3) without waiting for it to return. > > * lldb(1) > It misbehaves with or without the patch and ignores Ctrl-C. > I freely admit that i don't feel too enthusiastic about > debugging that beast. > > * sftp(1) > Behaviour is improved with the patch: Ctrl-C now exits sftp(1). > If desired, i can supply a very simple follow-up patch to sftp.c > to instead behave like ftp(1) and bc(1), i.e. discard the > current input line and provide a fresh prompt. > Neither doing undue work in the signal handler nor longjmp(3) > will be required for that (if this patch gets committed). > > * bgplgsh(8), fsdb(8) > I have no idea how to test those. Does anyone think that testing > either of them would be required? > > Regarding the patch below, note that differentiating EINTR behaviour > by signal number is not needed because the calling code in read_char() > already handles SIGCONT and SIGWINCH, so those two never arrive in > read__fixio() in the first place. > > Also note that deraadt@ pointed out in private mail to me that the > fact that read__fixio() clears FIONBIO is probably a symptom of > botched editline(3) API design. That might be worth fixing, too, > as far as that is feasible, but it is unrelated to the sftp(1) > Ctrl-C issue; let's address one topic at a time. > > Any feedback is welcome. > > Yours, > Ingo > > P.S. > I decided to Cc: tech@ again because this patch might affect > even people who are not specifically interested in editline(3), > and i have no intention to cause unpleasant surprises. > If we're stripping down fixio to a single function, why not inline the whole thing?
Also, as far as my brain explains things to me, it could theoretically be possible that the signal handler kicks in right after we return a -1 from read(2), making it possible to get something like an EIO and entering the sig switch statement. Assuming that a signal handler doesn't clobber our errno (which libedit doesn't do, but who knows what bad code is out there) we should probably only check the signal type when we know we have an EINTR. Finally, I don't understand why we only have a single retry on EAGAIN? If the application keeps resetting the FIONBIO in such a furious way that it happens more then once in a single call (no idea how that could happen) it might be an uphill battle, but we just burn away cpu cycles. It is not and should probably not be treated like something fatal. Diff below does this. (minus 27 LoC) The following ports use libedit (there might be a better way of finding them, but this works): $ find . -name Makefile | xargs grep -F '.include <bsd.port.mk>' | \ > cut -d: -f1 | while read file; do \ > (cd $(dirname $file); make show=WANTLIB | \ > grep -q '\<edit\>' && echo $(dirname $file)); \> > done 2> /dev/null ./devel/clang-tools-extra ./devel/llvm ./mail/dcc ./mail/nmh ./emulators/gsplus ./emulators/nono ./lang/brainfuck ./lang/eltclsh ./lang/lua/5.1 ./lang/lua/5.2 ./lang/lua/5.3 ./lang/swi-prolog ./math/gbc ./net/dnsdist ./net/honeyd ./net/icinga/core2 ./net/knot ./net/ntp ./security/kc ./security/pivy ./shells/dash ./shells/nsh ./sysutils/ipmitool So if we decide which of our interpretations should take precedence it might be a good idea to put it into snaps for a while. martijn@ Index: read.c =================================================================== RCS file: /cvs/src/lib/libedit/read.c,v retrieving revision 1.45 diff -u -p -r1.45 read.c --- read.c 9 Aug 2021 09:11:26 -0000 1.45 +++ read.c 9 Aug 2021 12:10:42 -0000 @@ -66,7 +66,6 @@ struct el_read_t { int read_errno; }; -static int read__fixio(int, int); static int read_char(EditLine *, wchar_t *); static int read_getcmd(EditLine *, el_action_t *, wchar_t *); static void read_clearmacros(struct macros *); @@ -132,29 +131,6 @@ el_read_getfn(struct el_read_t *el_read) } -/* read__fixio(): - * Try to recover from a read error - */ -static int -read__fixio(int fd, int e) -{ - int zero = 0; - - switch (e) { - case EAGAIN: - if (ioctl(fd, FIONBIO, &zero) == -1) - return -1; - return 0; - - case EINTR: - return 0; - - default: - return -1; - } -} - - /* el_push(): * Push a macro */ @@ -234,33 +210,30 @@ static int read_char(EditLine *el, wchar_t *cp) { ssize_t num_read; - int tried = 0; char cbuf[MB_LEN_MAX]; int cbp = 0; - int save_errno = errno; + int zero = 0; again: el->el_signal->sig_no = 0; while ((num_read = read(el->el_infd, cbuf + cbp, 1)) == -1) { - int e = errno; - switch (el->el_signal->sig_no) { - case SIGCONT: - el_set(el, EL_REFRESH); - /*FALLTHROUGH*/ - case SIGWINCH: - sig_set(el); - goto again; - default: - break; - } - if (!tried && read__fixio(el->el_infd, e) == 0) { - errno = save_errno; - tried = 1; - } else { - errno = e; - *cp = L'\0'; - return -1; + if (errno == EINTR) { + switch (el->el_signal->sig_no) { + case SIGCONT: + el_set(el, EL_REFRESH); + /*FALLTHROUGH*/ + case SIGWINCH: + sig_set(el); + goto again; + default: + break; + } + } else if (errno == EAGAIN) { + if (ioctl(el->el_infd, FIONBIO, &zero) != -1) + goto again; } + *cp = L'\0'; + return -1; } /* Test for EOF */