C++ & OpenSSL: SIGPIPE when writing in closed pipe
Asked Answered
F

3

7

I'm coding a C++ SSL Server for TCP Connections on Linux. When the program uses SSL_write() to write into a closed pipe, a SIGPIPE-Exception gets thrown which causes the program to shut down. I know that this is normal behaviour. But the program should not always die when the peer not closes the connection correctly.

I have already googled a lot and tried pretty much everything I found, but it seems like nothing is working for me. signal(SIGPIPE,SIG_IGN) does not work - the exception still gets thrown (Same for signal(SIGPIPE, SomeKindOfHandler).

The gdb output:

Program received signal SIGPIPE, Broken pipe.
0x00007ffff6b23ccd in write () from /lib/x86_64-linux-gnu/libpthread.so.0
(gdb) where
#0  0x00007ffff6b23ccd in write () from /lib/x86_64-linux-gnu/libpthread.so.0
#1  0x00007ffff7883835 in ?? () from /lib/x86_64-linux-gnu/libcrypto.so.1.0.0
#2  0x00007ffff7881687 in BIO_write () from /lib/x86_64-linux-gnu/libcrypto.so.1.0.0
#3  0x00007ffff7b9d3e0 in ?? () from /lib/x86_64-linux-gnu/libssl.so.1.0.0
#4  0x00007ffff7b9db04 in ?? () from /lib/x86_64-linux-gnu/libssl.so.1.0.0
#5  0x000000000042266a in NetInterface::SendToSubscribers(bool) () at ../Bether/NetInterface.h:181
#6  0x0000000000425834 in main () at ../Bether/main.cpp:111

About the Code:

I'm using a thread which is waiting for new connections and accepting them. The thread then puts the connection information (BIO & SSL) into a static map inside the NetInterface class. Every 5 seconds NetInterface::sendTOSubscribers() is executed from main(). This function accesses the static map and sends data to every connection in there. This function is also where the SIGPIPE comes from. I have used signal(SIGPIPE,SIG_IGN) in main() (obviously before the 5-seconds loop) and in NetInterface::SendToSubscribers(), but it is not working anywhere.

Thanks for your help!

Frolic answered 16/8, 2015 at 23:1 Comment(5)
The issue is why the correction solution (ignoring SIGPIPE) isn't working for you. But you haven't given us any specific information about your code, so it's hard to figure. Are you sure the call to signal is actually taking place? Are you using threads?Tribadism
Does the call to signal occur before any threads are created? Are you 100% sure?Tribadism
Yes 100% sure, it's in the first line of main()Frolic
You could have an object constructed before main starts that creates a thread.Tribadism
@DavidSchwartz, you should not use signal. This is not used anymore. The solution should use sigaction or pthread_sigmask.Lustful
L
5

You have to call function sigaction to change this behavior either to ignore SIGPIPE or handle it in a specific way with your own signal handler. Please don't use function signal, it's obsolete.

http://man7.org/linux/man-pages/man2/sigaction.2.html

One way to do it (I haven't compiled this code but should be something like this):

void sigpipe_handler(int signal)
{
   ...
}

int main() 
{
    struct sigaction sh;
    struct sigaction osh;

    sh.sa_handler = &sigpipe_handler; //Can set to SIG_IGN
    // Restart interrupted system calls
    sh.sa_flags = SA_RESTART;

    // Block every signal during the handler
    sigemptyset(&sh.sa_mask);

    if (sigaction(SIGPIPE, &sh, &osh) < 0)
    {
        return -1;
    }

    ...
}

If the program is multithreaded, it is a little different as you have less control on which thread will receive the signal. That depends on the type of signal. For SIGPIPE, it will be sent to the pthread that generated the signal. Nevertheless, sigaction should work OK.

It is possible to set the mask in the main thread and all subsequently created pthreads will inherit the signal mask. Otherwise, the signal mask can be set in each thread.

sigset_t blockedSignal; 
sigemptyset(&blockedSignal);    
sigaddset(&blockedSignal, SIGPIPE); 
pthread_sigmask(SIG_BLOCK, &blockedSignal, NULL);

However, if you block the signal, it will be pending for the process and as soon as it is possible it will be delivered. For this case, use sigtimedwait at the end of the thread. sigaction set at the main thread or in the thread that generated SIGPIPE should work as well.

Lustful answered 17/8, 2015 at 0:13 Comment(4)
I have changed your code to use SIG_IGN and appended it to the very start of main() and to the very start of every thread function. But still, the SIGPIPE crashes the programFrolic
I have read about pthread_sigmask, do you think this could bring the solutin?Frolic
@Bobface, OK if you are using pthreads it is different. You have to block (or ignore or change handler) SIGPIPE in all threads because the signal can be sent to any thread. You don't have control on it. Let me edit my response in while.Lustful
@Bobface, I tested a multithreaded program using sigaction both with a signal handler and SIG_IGN and it worked correctly. Please ignore my immediately previous message. I edited my response.Lustful
F
3

I've found the solution, it works with pthread_sigmask.

sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGPIPE);
if (pthread_sigmask(SIG_BLOCK, &set, NULL) != 0)
    return -1;

Thanks to everyone for the help!

Frolic answered 17/8, 2015 at 16:9 Comment(1)
remember what I wrote at the end in my response. If you block the signal, it will still stay pending for the process and can be sent at any moment. You may need to call sigtimedwait (or sigwait) to handle it at the end: man7.org/linux/man-pages/man2/sigtimedwait.2.html . I would try a little more with sigaction but if it works with sigmask ...Lustful
V
1

My problem is that I'm working on a portable library to be usable at least on Linux, macOS/OpenBSD and Microsoft Windows platforms in a threaded environment and using OpenSSL. signal(SIGPIPE, SIG_IGN) is not an option because it modifies the users process environment. I could use a confusing case distinction with using SO_NOSIGPIPE on macOS/OpenBSD, MSG_NOSIGNAL on Linux platforms and do nothing on MS Windows. This is only usable with system call send() but OpenSSL uses write() instead. An issue SIGPIPE when OpenSSL is used with a socket is pending since years on OpenSSL developement.

So I had a look at the general solution from kroki. He wrote that he uses sigtimedwait() but that isn't available on macOS/OpenBSD. Fortunately it can be workaroud with sigpending() and sigwait(). Looking at krokis example to understand how it works I create a "scoped" class CSigpipe with only constructor and destructor. For details have a look at the references. Here is the class:

Header file sigpipe.hpp

#ifndef SAMPLE_SIGPIPE_HPP
#define SAMPLE_SIGPIPE_HPP

#ifndef _MSC_VER
#include <csignal>

namespace sigpipe {

class CSigpipe_scoped {
  public:
    CSigpipe_scoped();
    ~CSigpipe_scoped();

  private:
    bool m_sigpipe_pending;
    bool m_sigpipe_unblock;
};

} // namespace sigpipe

#define SCOPED_NO_SIGPIPE sigpipe::CSigpipe_scoped sigpipe;
#else
// This will the ported program also compile on win32 without error.
#define SCOPED_NO_SIGPIPE
#endif // #ifndef _MSC_VER

#endif // SAMPLE_SIGPIPE_HPP

Source file sigpipe.cpp

#ifndef _MSC_VER
#include "sigpipe.hpp"

namespace sigpipe {

CSigpipe_scoped::CSigpipe_scoped() {
    /* We want to ignore possible SIGPIPE that we can generate on write. SIGPIPE
     * is delivered *synchronously* and *only* to the thread doing the write. So
     * if it is reported as already pending (which means the thread blocks it),
     * then we do nothing: if we generate SIGPIPE, it will be merged with the
     * pending one (there's no queuing), and that suits us well. If it is not
     * pending, we block it in this thread (and we avoid changing signal action,
     * because it is per-process). */
    sigset_t pending;
    sigemptyset(&pending);
    sigpending(&pending);

    m_sigpipe_pending = sigismember(&pending, SIGPIPE);
    if (!m_sigpipe_pending) {
        sigset_t sigpipe_mask;
        sigemptyset(&sigpipe_mask);
        sigaddset(&sigpipe_mask, SIGPIPE);

        sigset_t blocked;
        sigemptyset(&blocked);
        pthread_sigmask(SIG_BLOCK, &sigpipe_mask, &blocked);

        /* Maybe is was blocked already?  */
        m_sigpipe_unblock = !sigismember(&blocked, SIGPIPE);
    }
}

CSigpipe_scoped::~CSigpipe_scoped() {
    /* If SIGPIPE was pending already we do nothing. Otherwise, if it become
     * pending (i.e., we generated it), then we sigwait() it (thus clearing
     * pending status). Then we unblock SIGPIPE, but only if it were us who
     * blocked it. */
    if (!m_sigpipe_pending) {
        sigset_t sigpipe_mask;
        sigemptyset(&sigpipe_mask);
        sigaddset(&sigpipe_mask, SIGPIPE);

        // I cannot use sigtimedwait() because it isn't available on
        // macOS/OpenBSD. I workaround it with sigpending() and sigwait() to
        // ensure that sigwait() never blocks.
        sigset_t pending;
        while (true) {
            /* Protect ourselves from a situation when SIGPIPE was sent by
             * the user to the whole process, and was delivered to other
             * thread before we had a chance to wait for it. */
            sigemptyset(&pending);
            sigpending(&pending);
            if (sigismember(&pending, SIGPIPE)) {
                int sig; // Only return buffer, not used.
                sigwait(&sigpipe_mask, &sig);
            } else
                break;
        }

        if (m_sigpipe_unblock)
            pthread_sigmask(SIG_UNBLOCK, &sigpipe_mask, nullptr);
    }
}

} // namespace sigpipe
#endif

A simple main example program.

#include <iostream>

#ifdef _MSC_VER
// This simple test doesn't make sense on Microsoft Windows because it doesn't
// support sigpipe. It doesn't know the signal name SIGPIPE and never raise a
// signal with broken pipe on socket write(). But it compiles the object
// "instantiation" without error to be portable.
#include "sigpipe.hpp"

int main() {
    SCOPED_NO_SIGPIPE
    std::cout << "Nothing done.\n";
}

#else

#include "sigpipe.hpp"
namespace {
volatile std::sig_atomic_t gSignalStatus;
}
void signal_handler(int signal) { gSignalStatus = signal; }

int main() {
    // Install a signal handler
    std::signal(SIGPIPE, signal_handler);

    // Suppress signal
    std::cout << "SignalValue: " << gSignalStatus << '\n';
    std::cout << "Suppress signal SIGPIPE\nRaise SIGPIPE = " << SIGPIPE << '\n';
    {
        SCOPED_NO_SIGPIPE
        if (std::raise(SIGPIPE) != 0)
            return EXIT_FAILURE; // Signal will be restored
    }                            // Signal will be restored

    if (gSignalStatus != 0)
        std::cout << "Signal delivered, signal_handler executed." << '\n';
    std::cout << "SignalValue: " << gSignalStatus << '\n';

    // Not suppressing signal
    std::cout << "\nRaise SIGPIPE = " << SIGPIPE << '\n';
    std::raise(SIGPIPE);
    if (gSignalStatus != 0)
        std::cout << "Signal delivered, signal_handler executed." << '\n';
    std::cout << "SignalValue: " << gSignalStatus << '\n';

    return EXIT_SUCCESS;
}
#endif

Compiling on Linux with:

~$ g++ -std=c++17 -Wall -Wpedantic -Wextra -Werror -Wuninitialized -Wsuggest-override -Wdeprecated sigpipe.cpp main.cpp -lpthread

Output of the simple example on Linux:

~$ a.out
SignalValue: 0
Suppress signal SIGPIPE
Raise SIGPIPE = 13
SignalValue: 0

Raise SIGPIPE = 13
Signal delivered, signal_handler executed.
SignalValue: 13

References:
SIGPIPE and how to ignore it
Ignore SIGPIPE without affecting other threads in a process
How to prevent SIGPIPEs (or handle them properly)
signals, threads, SIGPIPE, sigpending (fun!)

Vertex answered 20/12, 2023 at 1:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.