One more Heisenbug past the crocodile

One more Heisenbug past the crocodile

$> set -o pipefail

$> fortune | head -1 > /dev/null && echo "ПовСзло!" || echo "Π’Ρ‹ ΠΏΡ€ΠΎΠΈΠ³Ρ€Π°Π»ΠΈ"
ПовСзло!

$> fortune | head -1 > /dev/null && echo "ПовСзло!" || echo "Π’Ρ‹ ΠΏΡ€ΠΎΠΈΠ³Ρ€Π°Π»ΠΈ"
Π’Ρ‹ ΠΏΡ€ΠΎΠΈΠ³Ρ€Π°Π»ΠΈ

Here fortune conditional program without exit(rand()).

Can you explain what is wrong here?

Lyrical-historical digression

The first time I met this Heisenbug was a quarter of a century ago. Then for the gateway in FaxNET it was required to make several utilities through pipes "playing checkers" under FreeBSD. As expected, I considered myself an advanced and quite experienced programmer. Therefore, I intended to do everything as carefully and carefully as possible, paying special attention to error handling ...

Diligence in "careful error handling" then added to me the previous experience of dealing with bugs in sendmail and uucp / uupc. It makes no sense to dive into the details of that story, but I fought this Heisenbug for two weeks at 10-14 hours. Therefore, it was remembered, and yesterday this old acquaintance again looked for a visit.

TL;DR Reply

Utility head can close the channel fortune at once as soon as it reads the first line. If fortune outputs more than one line, then the appropriate call write() will return an error, or report fewer bytes than requested. In turn, written with careful error handling fortune may reflect this situation in its exit status. Then due to setting set -o pipefail will work || echo "Π’Ρ‹ ΠΏΡ€ΠΎΠΈΠ³Ρ€Π°Π»ΠΈ".

But, head may not have time close before fortune finish outputting data. Then it will work && echo "ПовСзло!".

In one of my today GNUMakefile there is such fragment:

echo '#define MDBX_BUILD_COMPILER "$(shell set -o pipefail; $(CC) --version | head -1 || echo 'Please use GCC or CLANG compatible compiler')"'

Translated into human

Here the usual GNU Make ΠΈ bash in the compiler's way using the option --version it is asked who he is, and if the option is not supported, then a stub is substituted "Please use GCC or CLANG compatible compiler".

Like boiler plate can be found anywhere. It appeared in this place a long time ago and worked perfectly everywhere (Linux, Solaris, OSX, FreeBSD, WSL etc.). But yesterday at altlinux on the platform Elbrus 2000 (E2K) I noticed:

#define MDBX_BUILD_COMPILER "lcc:1.23.20:Sep--4-2019:e2k-v3-linux Please use GCC or CLANG compatible compiler"

Frankly, I did not immediately see the old "acquaintance". Moreover, the project has already been repeatedly tested on Elbrus and under a lot of different distributions, including Alt. With different compilers, versions of GNU Make and bash. Therefore, I did not want to see my oversight here.

When trying to reproduce the problem and / or understand what the matter is, there are more oddities.
Command line spell:

echo "#define MDBX_BUILD_COMPILER '$(set -o pipefail; LC_ALL=C cc --version | head -1 || echo "Please use GCC or CLANG compatible compiler")'"

Through-time it gave out an extra text, then no ... Often one of the options stuck for a long time, but if you poke a little longer, you always get both!

Of course, strace our everything! And so, having typed a strace-tirade, but before I had time to press Enter, I recognized my old friend Mr. Heisenbug, and in the developers compiler himself 25 years ago, Nostalgia… And I decided to feel sad writing this note πŸ˜‰

By the way, like any self-respecting Heisenbugunder strace prefers not to reproduce.

So what's going on?

  • Utility head has the right (or rather even forced) to close the readable channel as soon as it reads the requested number of lines.
  • The data-generating program-writer (in this case cc) can output multiple lines and free do it with multiple calls write().
  • If the reader will have time to close the channel on his side before the end of the recording on the writer's side, then the writer will receive an error.
  • Program Writer right how to ignore the write error to the pipe, and reflect it in your exit code.
  • Due to the installation set -o pipefail the pipeline termination code will be non-zero (erroneous) if the result of at least one element is non-zero, and then it will work || echo "Please use GCC or CLANG compatible compiler".

There may be variations, depending on how the writer program works with signals. For example, the program may crash (with automatic generation of a non-zero/erroneous exit status), or write() will return the result of writing fewer bytes than requested and set errno = EPIPE.

Who is to blame?

In the described case little bit of everything. Error handling in cc (lcc:1.23.20:Sepβ€”4-2019:e2k-v3-linux) is not redundant. In many cases, it's better to be safe, although this reveals sudden flaws in the boilerplate designed for traditional behavior.

What to do?

Wrong:

fortune | head -1 && echo "ПовСзло, Π½ΠΎ Π²Ρ‹ рискуСтС!" || echo "WTF?"

Correctly:

  1. (fortune && echo "УспСшно" || echo "Ошибка") | head -1

    Here, early closing of the pipe will be handled by the command interpreter when servicing the nested pipeline ("within" the parentheses). Accordingly, if fortune will report in the status about the error of writing to a closed channel, then the output || echo "Ошибка" will not get anywhere, since the channel is already closed.

  2. fortune | cat - | head -1 && echo "УспСшно" || echo "Ошибка"

    Here is the utility cat acts as a damper, as it ignores the error EPIPE when withdrawing. This is enough for now fortune small (several lines) and fits in the channel buffer (from 512 bytes to β‰ˆ64K, in most OSes β‰₯4K). Otherwise, the problem may return.

How to handle correctly EPIPE and other recording errors?

There is no single correct solution, but there are simple recommendations:

  • EPIPE required be sure to process (and reflected in the exit status) when outputting data requiring integrity. For example, during the work of archivers or backup utilities.
  • EPIPE better to ignore when displaying informational and auxiliary messages. For example, when displaying information on options --help or --version.
  • If the code being developed is allowed to be used in a pipeline before | headthen EPIPE it is better to ignore, otherwise it is better to process and reflect in the exit status.

I would like to take this opportunity to thank the team MCST ΠΈ altlinux for a great job. Your dedication is amazing!
Keep it up Camarades, up meetings in autumn!

Thank you beret for fixing bugs and errors.
KDPV from George A.

Source: habr.com

Add a comment