Skip to content

Conversation

@mqudsi
Copy link
Contributor

@mqudsi mqudsi commented Nov 19, 2024

This prevents kernel-buffered stdout contents from being written to the terminal after the prompt itself, which can happen when the external process is terminated with a signal and hasn't executed the libc/runtime-provided exit handlers that flush stdout.

Screenshots of afl-fuzz being terminated by ^C before and after the change:

Before:

image

After:

image

I tried to minimize any performance impact by trying to reduce the number of times this is called (by placing it as high up as possible, but bubbling up reap status) and by not performing the call in non-interactive mode (where fish shouldn't be painting to the shell bypassing stdout). stderr isn't flushed because it's typically unbuffered.

Let me know if you think this should be included in the changelog afterwards.

TODOs:

  • Changes to fish usage are reflected in user documentation/manpages.
  • Tests have been added for regressions fixed
  • User-visible changes noted in CHANGELOG.rst

@zanchey
Copy link
Member

zanchey commented Nov 20, 2024

I'd mention it:

"Standard output is flushed after external commands exit, in an attempt to reduce interleaving their output with the prompt (:issue:10861)."

This prevents kernel-buffered stdout contents from being written to the terminal
after the prompt itself, which can happen when the external process is
terminated with a signal and hasn't executed the libc/runtime-provided exit
handlers that flush stdout.

While it is possible for a libc-less application to exit normally without
flushing the stdout fd, the only way for "normal" applications to bypass the
atexit handlers would be to exit via abort() (i.e. SIGABRT) or in response to
some other signal; so we currently only flush stdout in response to such an
exit to reduce syscall overhead.
Copy link
Member

@ridiculousfish ridiculousfish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't make any sense to me. In my understanding, std::io::stdout().flush() flushes Rust's internal buffer. Because this is in-process, there is no way for an external command, such as afl-fuzz, to append to that buffer, so there is nothing to flush, so I would not expect this to do anything at all.

If there is something to flush, then it ought to have been flushed before the external command ran!

Can you please write a command line program that demonstrates this in action? afl-fuzz doesn't seem available on Apple Silicon.

@mqudsi
Copy link
Contributor Author

mqudsi commented Nov 21, 2024

You're right; I think I ran into a change in timing that resulted in the "corrected" behavior. I was thinking in terms of fsync() rather than flush() but that doesn't really mean anything for a fifo buffer. I really don't know why I didn't catch this, thank you for bringing me to my senses.

Are race conditions here inevitable?

@ridiculousfish
Copy link
Member

I think some race conditions are inevitable, especially with curses-style apps such as afl-fuzz. For example, if the app moves the cursor to some point on the screen and then dies, then fish will inherit a weird cursor position and I can see how that might screw up the prompt - though I would expect the PROMPT_SP hack to handle that (odd that it doesn't).

If you want to pursue this, it would be useful to write a custom tool that reproduces this, so we can explore more.

@mqudsi
Copy link
Contributor Author

mqudsi commented Nov 21, 2024

It actually reproduces when afl is still starting up and only producing a colorized log of text to stdout with no real tui to speak of, but it must be using absolute positioning ansi escapes or similar because regular stdout is strictly linear.

I wonder if I can just record and replay the output of afl instead of trying to independently reproduce the race.

@krobelus
Copy link
Contributor

script from util-linux has a --log-timing option that should enable almost-perfect replay

@mqudsi mqudsi closed this Nov 23, 2024
@mqudsi
Copy link
Contributor Author

mqudsi commented Nov 23, 2024

Thanks, that's useful. Probably have to use socat to trick it into thinking its running in a terminal, though.

@mqudsi
Copy link
Contributor Author

mqudsi commented Dec 3, 2024

I wrote this last week but never hit submit:


I didn't need to use socat as script uses a pseudotty itself.

Here's a sample (replay with scriptreplay --timing=timing.log cargo-afl.log) that reproduces the issue: cargo-afl.log, timing.log

Looking at the output with sequin makes it easier to understand all the output, but it's not particularly elucidating. There doesn't seem to be any absolute positioning of text, in fact it just looks like fish took over but cargo afl kept writing, probably via a child process that inherited the same tty. Looks like a cargo-afl bug, it should definitely waitpid() any processes still writing to the terminal before exiting?

One obvious answer is to set +tostop at all times except when fish has explicitly launched a background process, but there are obvious drawbacks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants