From 035486c7500c09616a6c1040d8e70923532a5c2d Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Mon, 31 Jul 2017 04:09:19 +0200 Subject: [PATCH] ash: significant overhaul of redirect saving logic New code is similar to what hush is doing. Make CLOSED to -1: same as dash. popredir() loses "restore" parameter: same as dash. COPYFD_RESTORE bit is no longer necessary. This change fixes this interactive bug: $ ls -l /proc/$$/fd 10>&- ash: can't set tty process group: Bad file descriptor ash: can't set tty process group: Bad file descriptor [1]+ Done(2) ls -l /proc/${\$}/fd 10>&4294967295 function old new delta unwindredir 29 27 -2 tryexec 154 152 -2 evaltree 503 501 -2 evalcommand 1369 1367 -2 cmdloop 187 185 -2 redirect 1029 1018 -11 popredir 153 123 -30 need_to_remember 36 - -36 is_hidden_fd 68 - -68 ------------------------------------------------------------------------------ (add/remove: 0/2 grow/shrink: 0/7 up/down: 0/-155) Total: -155 bytes text data bss dec hex filename 914572 485 6848 921905 e1131 busybox_old 914553 485 6848 921886 e111e busybox_unstripped Signed-off-by: Denys Vlasenko --- shell/ash.c | 332 ++++++++++-------- shell/ash_test/ash-redir/redir_script.tests | 9 +- .../ash-redir/redir_to_bad_fd255.right | 2 + .../ash-redir/redir_to_bad_fd255.tests | 3 + .../ash_test/ash-redir/redir_to_bad_fd3.right | 2 + .../ash_test/ash-redir/redir_to_bad_fd3.tests | 3 + shell/hush_test/hush-redir/redir_script.tests | 9 +- .../hush-redir/redir_to_bad_fd255.right | 1 + .../hush-redir/redir_to_bad_fd255.tests | 3 + .../hush-redir/redir_to_bad_fd3.right | 1 + .../hush-redir/redir_to_bad_fd3.tests | 3 + 11 files changed, 225 insertions(+), 143 deletions(-) create mode 100644 shell/ash_test/ash-redir/redir_to_bad_fd255.right create mode 100755 shell/ash_test/ash-redir/redir_to_bad_fd255.tests create mode 100644 shell/ash_test/ash-redir/redir_to_bad_fd3.right create mode 100755 shell/ash_test/ash-redir/redir_to_bad_fd3.tests create mode 100644 shell/hush_test/hush-redir/redir_to_bad_fd255.right create mode 100755 shell/hush_test/hush-redir/redir_to_bad_fd255.tests create mode 100644 shell/hush_test/hush-redir/redir_to_bad_fd3.right create mode 100755 shell/hush_test/hush-redir/redir_to_bad_fd3.tests diff --git a/shell/ash.c b/shell/ash.c index 1e720aec4..ac676c2dc 100644 --- a/shell/ash.c +++ b/shell/ash.c @@ -5192,7 +5192,7 @@ stoppedjobs(void) #undef EMPTY #undef CLOSED #define EMPTY -2 /* marks an unused slot in redirtab */ -#define CLOSED -3 /* marks a slot of previously-closed fd */ +#define CLOSED -1 /* marks a slot of previously-closed fd */ /* * Handle here documents. Normally we fork off a process to write the @@ -5349,74 +5349,158 @@ dup2_or_raise(int from, int to) } return newfd; } +static int +fcntl_F_DUPFD(int fd, int avoid_fd) +{ + int newfd; + repeat: + newfd = fcntl(fd, F_DUPFD, avoid_fd + 1); + if (newfd < 0) { + if (errno == EBUSY) + goto repeat; + if (errno == EINTR) + goto repeat; + } + return newfd; +} +static int +xdup_CLOEXEC_and_close(int fd, int avoid_fd) +{ + int newfd; + repeat: + newfd = fcntl(fd, F_DUPFD, avoid_fd + 1); + if (newfd < 0) { + if (errno == EBUSY) + goto repeat; + if (errno == EINTR) + goto repeat; + /* fd was not open? */ + if (errno == EBADF) + return fd; + ash_msg_and_raise_perror("%d", newfd); + } + fcntl(newfd, F_SETFD, FD_CLOEXEC); + close(fd); + return newfd; +} /* Struct def and variable are moved down to the first usage site */ -struct two_fd_t { - int orig, copy; +struct squirrel { + int orig_fd; + int moved_to; }; struct redirtab { struct redirtab *next; int pair_count; - struct two_fd_t two_fd[]; + struct squirrel two_fd[]; }; #define redirlist (G_var.redirlist) -enum { - COPYFD_RESTORE = (int)~(INT_MAX), -}; -static int -need_to_remember(struct redirtab *rp, int fd) +static void +add_squirrel_closed(struct redirtab *sq, int fd) { int i; - if (!rp) /* remembering was not requested */ - return 0; + if (!sq) + return; - for (i = 0; i < rp->pair_count; i++) { - if (rp->two_fd[i].orig == fd) { - /* already remembered */ - return 0; + for (i = 0; sq->two_fd[i].orig_fd != EMPTY; i++) { + /* If we collide with an already moved fd... */ + if (fd == sq->two_fd[i].orig_fd) { + /* Examples: + * "echo 3>FILE 3>&- 3>FILE" + * "echo 3>&- 3>FILE" + * No need for last redirect to insert + * another "need to close 3" indicator. + */ + TRACE(("redirect_fd %d: already moved or closed\n", fd)); + return; } } - return 1; + TRACE(("redirect_fd %d: previous fd was closed\n", fd)); + sq->two_fd[i].orig_fd = fd; + sq->two_fd[i].moved_to = CLOSED; } -/* "hidden" fd is a fd used to read scripts, or a copy of such */ static int -is_hidden_fd(struct redirtab *rp, int fd) +save_fd_on_redirect(int fd, int avoid_fd, struct redirtab *sq) { - int i; - struct parsefile *pf; + int i, new_fd; + + if (avoid_fd < 9) /* the important case here is that it can be -1 */ + avoid_fd = 9; - if (fd == -1) +#if JOBS + if (fd == ttyfd) { + /* Testcase: "ls -l /proc/$$/fd 10>&-" should work */ + ttyfd = xdup_CLOEXEC_and_close(ttyfd, avoid_fd); + TRACE(("redirect_fd %d: matches ttyfd, moving it to %d\n", fd, ttyfd)); + return 1; /* "we closed fd" */ + } +#endif + /* Are we called from redirect(0)? E.g. redirect + * in a forked child. No need to save fds, + * we aren't going to use them anymore, ok to trash. + */ + if (!sq) return 0; - /* Check open scripts' fds */ - pf = g_parsefile; - while (pf) { - /* We skip pf_fd == 0 case because of the following case: - * $ ash # running ash interactively - * $ . ./script.sh - * and in script.sh: "exec 9>&0". - * Even though top-level pf_fd _is_ 0, - * it's still ok to use it: "read" builtin uses it, - * why should we cripple "exec" builtin? - */ - if (pf->pf_fd > 0 && fd == pf->pf_fd) { - return 1; + + /* If this one of script's fds? */ + if (fd != 0) { + struct parsefile *pf = g_parsefile; + while (pf) { + /* We skip fd == 0 case because of the following: + * $ ash # running ash interactively + * $ . ./script.sh + * and in script.sh: "exec 9>&0". + * Even though top-level pf_fd _is_ 0, + * it's still ok to use it: "read" builtin uses it, + * why should we cripple "exec" builtin? + */ + if (fd == pf->pf_fd) { + pf->pf_fd = xdup_CLOEXEC_and_close(fd, avoid_fd); + return 1; /* "we closed fd" */ + } + pf = pf->prev; } - pf = pf->prev; } - if (!rp) - return 0; - /* Check saved fds of redirects */ - fd |= COPYFD_RESTORE; - for (i = 0; i < rp->pair_count; i++) { - if (rp->two_fd[i].copy == fd) { - return 1; + /* Check whether it collides with any open fds (e.g. stdio), save fds as needed */ + + /* First: do we collide with some already moved fds? */ + for (i = 0; sq->two_fd[i].orig_fd != EMPTY; i++) { + /* If we collide with an already moved fd... */ + if (fd == sq->two_fd[i].moved_to) { + new_fd = fcntl_F_DUPFD(fd, avoid_fd); + sq->two_fd[i].moved_to = new_fd; + TRACE(("redirect_fd %d: already busy, moving to %d\n", fd, new_fd)); + if (new_fd < 0) /* what? */ + xfunc_die(); + return 0; /* "we did not close fd" */ + } + if (fd == sq->two_fd[i].orig_fd) { + /* Example: echo Hello >/dev/null 1>&2 */ + TRACE(("redirect_fd %d: already moved\n", fd)); + return 0; /* "we did not close fd" */ } } - return 0; + + /* If this fd is open, we move and remember it; if it's closed, new_fd = CLOSED (-1) */ + new_fd = fcntl_F_DUPFD(fd, avoid_fd); + TRACE(("redirect_fd %d: previous fd is moved to %d (-1 if it was closed)\n", fd, new_fd)); + if (new_fd < 0) { + if (errno != EBADF) + xfunc_die(); + /* new_fd = CLOSED; - already is -1 */ + } + sq->two_fd[i].moved_to = new_fd; + sq->two_fd[i].orig_fd = fd; + + /* if we move stderr, let "set -x" code know */ + if (fd == preverrout_fd) + preverrout_fd = new_fd; + + return 0; /* "we did not close fd" */ } /* @@ -5431,109 +5515,80 @@ redirect(union node *redir, int flags) { struct redirtab *sv; int sv_pos; - int fd; - int newfd; if (!redir) return; + + sv_pos = 0; sv = NULL; INT_OFF; if (flags & REDIR_PUSH) sv = redirlist; - sv_pos = 0; do { - int right_fd = -1; + int fd; + int newfd; + int close_fd; + int closed; + fd = redir->nfile.fd; if (redir->nfile.type == NTOFD || redir->nfile.type == NFROMFD) { - right_fd = redir->ndup.dupfd; - //bb_error_msg("doing %d > %d", fd, right_fd); - /* redirect from/to same file descriptor? */ - if (right_fd == fd) - continue; - /* "echo >&10" and 10 is a fd opened to a sh script? */ - if (is_hidden_fd(sv, right_fd)) { - errno = EBADF; /* as if it is closed */ - ash_msg_and_raise_perror("%d", right_fd); - } - newfd = -1; + //bb_error_msg("doing %d > %d", fd, newfd); + newfd = redir->ndup.dupfd; + close_fd = -1; } else { newfd = openredirect(redir); /* always >= 0 */ if (fd == newfd) { - /* Descriptor wasn't open before redirect. - * Mark it for close in the future */ - if (need_to_remember(sv, fd)) { - goto remember_to_close; - } + /* open() gave us precisely the fd we wanted. + * This means that this fd was not busy + * (not opened to anywhere). + * Remember to close it on restore: + */ + add_squirrel_closed(sv, fd); continue; } + close_fd = newfd; } -#if BASH_REDIR_OUTPUT - redirect_more: -#endif - if (need_to_remember(sv, fd)) { - /* Copy old descriptor */ - /* Careful to not accidentally "save" - * to the same fd as right side fd in N>&M */ - int minfd = right_fd < 10 ? 10 : right_fd + 1; -#if defined(F_DUPFD_CLOEXEC) - int copy = fcntl(fd, F_DUPFD_CLOEXEC, minfd); -#else - int copy = fcntl(fd, F_DUPFD, minfd); -#endif - if (copy == -1) { - int e = errno; - if (e != EBADF) { - /* Strange error (e.g. "too many files" EMFILE?) */ - if (newfd >= 0) - close(newfd); - errno = e; - ash_msg_and_raise_perror("%d", fd); - /* NOTREACHED */ - } - /* EBADF: it is not open - good, remember to close it */ - remember_to_close: - copy = CLOSED; - } else { /* fd is open, save its copy */ -#if !defined(F_DUPFD_CLOEXEC) - fcntl(copy, F_SETFD, FD_CLOEXEC); -#endif - /* "exec fd>&-" should not close fds - * which point to script file(s). - * Force them to be restored afterwards */ - if (is_hidden_fd(sv, fd)) - copy |= COPYFD_RESTORE; - } - /* if we move stderr, let "set -x" code know */ - if (fd == preverrout_fd) - preverrout_fd = copy; - sv->two_fd[sv_pos].orig = fd; - sv->two_fd[sv_pos].copy = copy; - sv_pos++; - } - if (newfd < 0) { - /* NTOFD/NFROMFD: copy redir->ndup.dupfd to fd */ - if (redir->ndup.dupfd < 0) { /* "fd>&-" */ - /* Don't want to trigger debugging */ - if (fd != -1) - close(fd); - } else { - dup2_or_raise(redir->ndup.dupfd, fd); + + if (fd == newfd) + continue; + + /* if "N>FILE": move newfd to fd */ + /* if "N>&M": dup newfd to fd */ + /* if "N>&-": close fd (newfd is -1) */ + + IF_BASH_REDIR_OUTPUT(redirect_more:) + + closed = save_fd_on_redirect(fd, /*avoid:*/ newfd, sv); + if (newfd == -1) { + /* "N>&-" means "close me" */ + if (!closed) { + /* ^^^ optimization: saving may already + * have closed it. If not... */ + close(fd); } - } else if (fd != newfd) { /* move newfd to fd */ + } else { +///TODO: if _newfd_ is a script fd or saved fd, then simulate EBADF! +//if (newfd == ttyfd) { +// errno = EBADF; +// ash_msg_and_raise_perror("A %d", newfd); +//} +//if (newfd == g_parsefile->pf_fd) { +// errno = EBADF; +// ash_msg_and_raise_perror("B %d", newfd); +//} dup2_or_raise(newfd, fd); + if (close_fd >= 0) /* "N>FILE" or ">&FILE" or heredoc? */ + close(close_fd); #if BASH_REDIR_OUTPUT - if (!(redir->nfile.type == NTO2 && fd == 2)) + if (redir->nfile.type == NTO2 && fd == 1) { + /* ">&FILE". we already redirected to 1, now copy 1 to 2 */ + fd = 2; + newfd = 1; + close_fd = -1; + goto redirect_more; + } #endif - close(newfd); } -#if BASH_REDIR_OUTPUT - if (redir->nfile.type == NTO2 && fd == 1) { - /* We already redirected it to fd 1, now copy it to 2 */ - newfd = 1; - fd = 2; - goto redirect_more; - } -#endif } while ((redir = redir->nfile.next) != NULL); INT_ON; @@ -5542,7 +5597,7 @@ redirect(union node *redir, int flags) // dash has a bug: since REDIR_SAVEFD2=3 and REDIR_PUSH=1, this test // triggers for pure REDIR_PUSH too. Thus, this is done almost always, // not only for calls with flags containing REDIR_SAVEFD2. - // We do this unconditionally (see code above). + // We do this unconditionally (see save_fd_on_redirect()). //if ((flags & REDIR_SAVEFD2) && copied_fd2 >= 0) // preverrout_fd = copied_fd2; } @@ -5557,7 +5612,7 @@ redirectsafe(union node *redir, int flags) SAVE_INT(saveint); /* "echo 9>/dev/null; echo >&9; echo result: $?" - result should be 1, not 2! */ - err = setjmp(jmploc.loc); // huh?? was = setjmp(jmploc.loc) * 2; + err = setjmp(jmploc.loc); /* was = setjmp(jmploc.loc) * 2; */ if (!err) { exception_handler = &jmploc; redirect(redir, flags); @@ -5591,7 +5646,7 @@ pushredir(union node *redir) sv = ckzalloc(sizeof(*sv) + i * sizeof(sv->two_fd[0])); sv->pair_count = i; while (--i >= 0) - sv->two_fd[i].orig = sv->two_fd[i].copy = EMPTY; + sv->two_fd[i].orig_fd = sv->two_fd[i].moved_to = EMPTY; sv->next = redirlist; redirlist = sv; return sv->next; @@ -5601,7 +5656,7 @@ pushredir(union node *redir) * Undo the effects of the last redirection. */ static void -popredir(int drop, int restore) +popredir(int drop) { struct redirtab *rp; int i; @@ -5611,20 +5666,19 @@ popredir(int drop, int restore) INT_OFF; rp = redirlist; for (i = 0; i < rp->pair_count; i++) { - int fd = rp->two_fd[i].orig; - int copy = rp->two_fd[i].copy; + int fd = rp->two_fd[i].orig_fd; + int copy = rp->two_fd[i].moved_to; if (copy == CLOSED) { if (!drop) close(fd); continue; } if (copy != EMPTY) { - if (!drop || (restore && (copy & COPYFD_RESTORE))) { - copy &= ~COPYFD_RESTORE; + if (!drop) { /*close(fd);*/ dup2_or_raise(copy, fd); } - close(copy & ~COPYFD_RESTORE); + close(copy); } } redirlist = rp->next; @@ -5636,7 +5690,7 @@ static void unwindredir(struct redirtab *stop) { while (redirlist != stop) - popredir(/*drop:*/ 0, /*restore:*/ 0); + popredir(/*drop:*/ 0); } @@ -7715,7 +7769,7 @@ tryexec(IF_FEATURE_SH_STANDALONE(int applet_no,) const char *cmd, char **argv, c clearenv(); while (*envp) putenv(*envp++); - popredir(/*drop:*/ 1, /*restore:*/ 0); + popredir(/*drop:*/ 1); run_applet_no_and_exit(applet_no, cmd, argv); } /* re-exec ourselves with the new arguments */ @@ -8754,7 +8808,7 @@ evaltree(union node *n, int flags) status = evaltree(n->nredir.n, flags & EV_TESTED); } if (n->nredir.redirect) - popredir(/*drop:*/ 0, /*restore:*/ 0 /* not sure */); + popredir(/*drop:*/ 0); goto setstatus; case NCMD: evalfn = evalcommand; @@ -9902,7 +9956,7 @@ evalcommand(union node *cmd, int flags) out: if (cmd->ncmd.redirect) - popredir(/*drop:*/ cmd_is_exec, /*restore:*/ cmd_is_exec); + popredir(/*drop:*/ cmd_is_exec); unwindredir(redir_stop); unwindlocalvars(localvar_stop); if (lastarg) { @@ -13727,7 +13781,7 @@ int ash_main(int argc UNUSED_PARAM, char **argv) * Testcase: ash -c 'exec 1>&0' must not complain. */ // if (!sflag) g_parsefile->pf_fd = -1; // ^^ not necessary since now we special-case fd 0 - // in is_hidden_fd() to not be considered "hidden fd" + // in save_fd_on_redirect() evalstring(minusc, sflag ? 0 : EV_EXIT); } diff --git a/shell/ash_test/ash-redir/redir_script.tests b/shell/ash_test/ash-redir/redir_script.tests index ccc497d7b..740daa461 100755 --- a/shell/ash_test/ash-redir/redir_script.tests +++ b/shell/ash_test/ash-redir/redir_script.tests @@ -20,10 +20,15 @@ eval "find_fds $fds" # Shell should not lose that fd. Did it? find_fds -test x"$fds1" = x"$fds" && { echo "Ok: script fd is not closed"; exit 0; } +test x"$fds1" = x"$fds" \ +&& { echo "Ok: script fd is not closed"; exit 0; } + +# One legit way to handle it is to move script fd. For example, if we see that fd 10 moved to fd 11: +test x"$fds1" = x" 10>&- 3>&-" && \ +test x"$fds" = x" 11>&- 3>&-" \ +&& { echo "Ok: script fd is not closed"; exit 0; } echo "Bug: script fd is closed" echo "fds1:$fds1" echo "fds2:$fds" exit 1 - diff --git a/shell/ash_test/ash-redir/redir_to_bad_fd255.right b/shell/ash_test/ash-redir/redir_to_bad_fd255.right new file mode 100644 index 000000000..9c5e35b36 --- /dev/null +++ b/shell/ash_test/ash-redir/redir_to_bad_fd255.right @@ -0,0 +1,2 @@ +./redir_to_bad_fd255.tests: line 2: 255: Bad file descriptor +OK diff --git a/shell/ash_test/ash-redir/redir_to_bad_fd255.tests b/shell/ash_test/ash-redir/redir_to_bad_fd255.tests new file mode 100755 index 000000000..2266af6da --- /dev/null +++ b/shell/ash_test/ash-redir/redir_to_bad_fd255.tests @@ -0,0 +1,3 @@ +# ash uses fd 10 (usually) for reading the script +echo LOST >&255 +echo OK diff --git a/shell/ash_test/ash-redir/redir_to_bad_fd3.right b/shell/ash_test/ash-redir/redir_to_bad_fd3.right new file mode 100644 index 000000000..895a4a0a6 --- /dev/null +++ b/shell/ash_test/ash-redir/redir_to_bad_fd3.right @@ -0,0 +1,2 @@ +./redir_to_bad_fd3.tests: line 2: 3: Bad file descriptor +OK diff --git a/shell/ash_test/ash-redir/redir_to_bad_fd3.tests b/shell/ash_test/ash-redir/redir_to_bad_fd3.tests new file mode 100755 index 000000000..98c54cfc6 --- /dev/null +++ b/shell/ash_test/ash-redir/redir_to_bad_fd3.tests @@ -0,0 +1,3 @@ +# ash uses fd 10 (usually) for reading the script +echo LOST >&3 +echo OK diff --git a/shell/hush_test/hush-redir/redir_script.tests b/shell/hush_test/hush-redir/redir_script.tests index ccc497d7b..740daa461 100755 --- a/shell/hush_test/hush-redir/redir_script.tests +++ b/shell/hush_test/hush-redir/redir_script.tests @@ -20,10 +20,15 @@ eval "find_fds $fds" # Shell should not lose that fd. Did it? find_fds -test x"$fds1" = x"$fds" && { echo "Ok: script fd is not closed"; exit 0; } +test x"$fds1" = x"$fds" \ +&& { echo "Ok: script fd is not closed"; exit 0; } + +# One legit way to handle it is to move script fd. For example, if we see that fd 10 moved to fd 11: +test x"$fds1" = x" 10>&- 3>&-" && \ +test x"$fds" = x" 11>&- 3>&-" \ +&& { echo "Ok: script fd is not closed"; exit 0; } echo "Bug: script fd is closed" echo "fds1:$fds1" echo "fds2:$fds" exit 1 - diff --git a/shell/hush_test/hush-redir/redir_to_bad_fd255.right b/shell/hush_test/hush-redir/redir_to_bad_fd255.right new file mode 100644 index 000000000..936911ce5 --- /dev/null +++ b/shell/hush_test/hush-redir/redir_to_bad_fd255.right @@ -0,0 +1 @@ +hush: can't duplicate file descriptor: Bad file descriptor diff --git a/shell/hush_test/hush-redir/redir_to_bad_fd255.tests b/shell/hush_test/hush-redir/redir_to_bad_fd255.tests new file mode 100755 index 000000000..2266af6da --- /dev/null +++ b/shell/hush_test/hush-redir/redir_to_bad_fd255.tests @@ -0,0 +1,3 @@ +# ash uses fd 10 (usually) for reading the script +echo LOST >&255 +echo OK diff --git a/shell/hush_test/hush-redir/redir_to_bad_fd3.right b/shell/hush_test/hush-redir/redir_to_bad_fd3.right new file mode 100644 index 000000000..936911ce5 --- /dev/null +++ b/shell/hush_test/hush-redir/redir_to_bad_fd3.right @@ -0,0 +1 @@ +hush: can't duplicate file descriptor: Bad file descriptor diff --git a/shell/hush_test/hush-redir/redir_to_bad_fd3.tests b/shell/hush_test/hush-redir/redir_to_bad_fd3.tests new file mode 100755 index 000000000..98c54cfc6 --- /dev/null +++ b/shell/hush_test/hush-redir/redir_to_bad_fd3.tests @@ -0,0 +1,3 @@ +# ash uses fd 10 (usually) for reading the script +echo LOST >&3 +echo OK -- 2.25.1