Re: [PATCH] scripts: handle BrokenPipeError for python scripts

From: Nick Desaulniers
Date: Thu Jan 12 2023 - 14:10:36 EST


On Wed, Jan 11, 2023 at 6:30 PM Masahiro Yamada <masahiroy@xxxxxxxxxx> wrote:
>
> In the follow-up of commit fb3041d61f68 ("kbuild: fix SIGPIPE error
> message for AR=gcc-ar and AR=llvm-ar"), Kees Cook pointed out that
> tools should _not_ catch their own SIGPIPEs [1] [2].
>
> Based on his feedback, LLVM was fixed [3].
>
> However, Python's default behavior is to show noisy bracktrace when
> SIGPIPE is sent. So, scripts written in Python are basically in the
> same situation as the buggy llvm tools.
>
> Example:
>
> $ make -s allnoconfig
> $ make -s allmodconfig
> $ scripts/diffconfig .config.old .config | head -n1
> -ALIX n
> Traceback (most recent call last):
> File "/home/masahiro/linux/scripts/diffconfig", line 132, in <module>
> main()
> File "/home/masahiro/linux/scripts/diffconfig", line 130, in main
> print_config("+", config, None, b[config])
> File "/home/masahiro/linux/scripts/diffconfig", line 64, in print_config
> print("+%s %s" % (config, new_value))
> BrokenPipeError: [Errno 32] Broken pipe
>
> Python documentatin [4] notes how to make scripts die immediately and

typo: s/documentatin/documentation/

> silently:
>
> """
> Piping output of your program to tools like head(1) will cause a
> SIGPIPE signal to be sent to your process when the receiver of its
> standard output closes early. This results in an exception like
> BrokenPipeError: [Errno 32] Broken pipe. To handle this case,
> wrap your entry point to catch this exception as follows:
>
> import os
> import sys
>
> def main():
> try:
> # simulate large output (your code replaces this loop)
> for x in range(10000):
> print("y")
> # flush output here to force SIGPIPE to be triggered
> # while inside this try block.
> sys.stdout.flush()
> except BrokenPipeError:
> # Python flushes standard streams on exit; redirect remaining output
> # to devnull to avoid another BrokenPipeError at shutdown
> devnull = os.open(os.devnull, os.O_WRONLY)
> os.dup2(devnull, sys.stdout.fileno())
> sys.exit(1) # Python exits with error code 1 on EPIPE
>
> if __name__ == '__main__':
> main()
>
> Do not set SIGPIPE’s disposition to SIG_DFL in order to avoid
> BrokenPipeError. Doing that would cause your program to exit
> unexpectedly whenever any socket connection is interrupted while
> your program is still writing to it.
> """
>
> Currently, tools/perf/scripts/python/intel-pt-events.py seems the
> only script that fixes the issue that way.
>
> tools/perf/scripts/python/compaction-times.py uses another approach
> signal.signal(signal.SIGPIPE, signal.SIG_DFL) but the Python
> documentation clearly says "Don't do it".
>
> I cannot fix all Python scripts since there are so many.
> I fixed some in the scripts/ directory.

That's ok; "Rome wasn't built in a day." This is a good start!
Thank you for the patch!
Reviewed-by: Nick Desaulniers <ndesaulniers@xxxxxxxxxx>

>
> [1]: https://lore.kernel.org/all/202211161056.1B9611A@keescook/
> [2]: https://github.com/llvm/llvm-project/issues/59037
> [3]: https://github.com/llvm/llvm-project/commit/4787efa38066adb51e2c049499d25b3610c0877b
> [4]: https://docs.python.org/3/library/signal.html#note-on-sigpipe
>
> Cc: Kees Cook <keescook@xxxxxxxxxxxx>
> Signed-off-by: Masahiro Yamada <masahiroy@xxxxxxxxxx>
> ---
>
> scripts/checkkconfigsymbols.py | 13 ++++++++++++-
> scripts/clang-tools/run-clang-tools.py | 21 ++++++++++++++-------
> scripts/diffconfig | 16 ++++++++++++++--
> 3 files changed, 40 insertions(+), 10 deletions(-)
>
> diff --git a/scripts/checkkconfigsymbols.py b/scripts/checkkconfigsymbols.py
> index 217d21abc86e..36c920e71313 100755
> --- a/scripts/checkkconfigsymbols.py
> +++ b/scripts/checkkconfigsymbols.py
> @@ -115,7 +115,7 @@ def parse_options():
> return args
>
>
> -def main():
> +def print_undefined_symbols():
> """Main function of this module."""
> args = parse_options()
>
> @@ -467,5 +467,16 @@ def parse_kconfig_file(kfile):
> return defined, references
>
>
> +def main():
> + try:
> + print_undefined_symbols()
> + except BrokenPipeError:
> + # Python flushes standard streams on exit; redirect remaining output
> + # to devnull to avoid another BrokenPipeError at shutdown
> + devnull = os.open(os.devnull, os.O_WRONLY)
> + os.dup2(devnull, sys.stdout.fileno())
> + sys.exit(1) # Python exits with error code 1 on EPIPE
> +
> +
> if __name__ == "__main__":
> main()
> diff --git a/scripts/clang-tools/run-clang-tools.py b/scripts/clang-tools/run-clang-tools.py
> index 56f2ec8f0f40..3266708a8658 100755
> --- a/scripts/clang-tools/run-clang-tools.py
> +++ b/scripts/clang-tools/run-clang-tools.py
> @@ -61,14 +61,21 @@ def run_analysis(entry):
>
>
> def main():
> - args = parse_arguments()
> + try:
> + args = parse_arguments()
>
> - lock = multiprocessing.Lock()
> - pool = multiprocessing.Pool(initializer=init, initargs=(lock, args))
> - # Read JSON data into the datastore variable
> - with open(args.path, "r") as f:
> - datastore = json.load(f)
> - pool.map(run_analysis, datastore)
> + lock = multiprocessing.Lock()
> + pool = multiprocessing.Pool(initializer=init, initargs=(lock, args))
> + # Read JSON data into the datastore variable
> + with open(args.path, "r") as f:
> + datastore = json.load(f)
> + pool.map(run_analysis, datastore)
> + except BrokenPipeError:
> + # Python flushes standard streams on exit; redirect remaining output
> + # to devnull to avoid another BrokenPipeError at shutdown
> + devnull = os.open(os.devnull, os.O_WRONLY)
> + os.dup2(devnull, sys.stdout.fileno())
> + sys.exit(1) # Python exits with error code 1 on EPIPE
>
>
> if __name__ == "__main__":
> diff --git a/scripts/diffconfig b/scripts/diffconfig
> index d5da5fa05d1d..43f0f3d273ae 100755
> --- a/scripts/diffconfig
> +++ b/scripts/diffconfig
> @@ -65,7 +65,7 @@ def print_config(op, config, value, new_value):
> else:
> print(" %s %s -> %s" % (config, value, new_value))
>
> -def main():
> +def show_diff():
> global merge_style
>
> # parse command line args
> @@ -129,4 +129,16 @@ def main():
> for config in new:
> print_config("+", config, None, b[config])
>
> -main()
> +def main():
> + try:
> + show_diff()
> + except BrokenPipeError:
> + # Python flushes standard streams on exit; redirect remaining output
> + # to devnull to avoid another BrokenPipeError at shutdown
> + devnull = os.open(os.devnull, os.O_WRONLY)
> + os.dup2(devnull, sys.stdout.fileno())
> + sys.exit(1) # Python exits with error code 1 on EPIPE
> +
> +
> +if __name__ == '__main__':
> + main()
> --
> 2.34.1
>


--
Thanks,
~Nick Desaulniers