macOS `sigaction()` handler with `SA_SIGINFO` does not include `si_pid`
Asked Answered
R

2

5

I'm trying to write a signal handler which needs to know the pid of the process that sends the signal. I'm having no luck with getting anything useful from the siginfo_t passed into my handler on macOS 10.14 with Xcode 10.

I've reduced my code to the below minimal sample to demonstrate the issue. In this sample I spawn a child process to send the signal I want to test which is defaulted to SIGTERM, but no other signal I've tried works any better.

Assuming you want to build and test this on a mac, you probably want to tell lldb to not stop when receiving a signal. You can use this lldb command: pro hand -p true -s false SIGTERM.

I'm also compiling with C++, but I believe I have excised all of that and the sample code should be pure C now.

Note that it doesn't matter if the signal originates from a child, terminal, or another process the result is always that si_pid is always 0 (along with everything other than the si_signo and si_addr). It doesnt matter how many times I send the signal, so it seems to not be simply a race condition.

How can I get the pid of the process sending the signal on macOS 10.14? I don't recall having this issue on 10.12 which is what I was using before.

This is just a sample to demostrate the problem, so please ignore anything that isn't actually causing a problem.

If the code seems like it should work as I expect, then I would be interested in seeing comments about systems that it works on too.

#include <unistd.h>
#include <signal.h>
#include <stdio.h>

volatile sig_atomic_t histogram[3] = {0,0,0};
volatile sig_atomic_t signaled = 0;
const int testsig = SIGTERM;

void sigaction_handler(int sig, siginfo_t* info, void* context)
{
    switch (info->si_pid) {
        case 0:
        case 1:
            histogram[info->si_pid]++;
            break;

        default:
            histogram[2]++;
            break;
    }
    signaled = 1;
}

int main(int argc, const char * argv[]) {

    pid_t mainpid = getpid();
    pid_t pid = fork();
    if (pid == 0) {
        while (kill(mainpid, 0) == 0) {
            sleep(1);
            kill(mainpid, testsig);
        }
        _exit(0);
    }

    struct sigaction sigAction;
    memset( &sigAction, 0, sizeof( sigAction ) );

    sigAction.sa_sigaction = sigaction_handler;
    sigemptyset (&sigAction.sa_mask);
    sigAction.sa_flags = SA_SIGINFO;
    sigaction(testsig, &sigAction, NULL);

    while (1) {
        if (signaled) {
            printf("pid 0: %d, pid 1: %d, others: %d\n", histogram[0], histogram[1], histogram[2]);
            signaled = 0;
        }
        sleep(1);
    }
}
Rascality answered 20/11, 2018 at 19:8 Comment(8)
Need to add #include <stdbool.h> for that to compile as C, btw.Lists
@Lists thanks, I've edited the code.Rascality
The signal handler also invokes undefined behavior according to POSIX because it assigns to variables that aren't volatile sig_atomic_t ones. (Reference). I can picture the loop in main always seeing signaled as false with aggressive optimization settings (But can't duplicate with gcc or clang on linux). Don't have a mac for actual useful testing, though.Lists
@Lists I appreciate that, my IRL code does not, however. I'll see about updating the sample with compliant code, but I dont see how that could be the problem. The debugger always says it is 0 too.Rascality
I'm pretty sure struct sigaction sigAction = {}; is invalid C code. In fact, cppreference.com states "In C, the braced list of initializers cannot be empty.", but I'm not going to parse 6.7.9 Initialization of the C standard and verify that's correct.Division
@AndrewHenle sure, I've updated the code accordingly. Still not the problem tho :(Rascality
Now it's completely uninitialized other than the fields you explicitly set. memset( &sigAction, 0, sizeof( sigAction ) ); along with the current sigemptyset() would be good.Division
@AndrewHenle on Mac those are the only fields, but I'll add that anyway.Rascality
R
2

It turns out that debugging via Xcode LLDB is the culprit. If I build and run the program normally it works fine. If I find out why I will update this answer.

I already have the "PASS" set for SIGTERM in lldb as noted in the question, so it seems like somehow there is a bug in the version of lldb shipped with Xcode 10.0 and it is "passing" the signal by creating a new struct and setting the signal number rather then the structure that would have normally been received. As I stated before this did used to work fine in whatever version of lldb shipped with macos 10.12

If somebody has a better explaination, please post an answer and I will accept and award bounty.

Rascality answered 4/12, 2018 at 17:22 Comment(0)
U
4

I'm currently using macOS Mojave 10.14.1.

How can I get the pid of the process sending the signal on macOS 10.14? I don't recall having this issue on 10.12 which is what I was using before.

The following code meets your wish simply. If you send SIGTERM, you can see pid of sender process.

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>

static void hdl (int sig, siginfo_t *siginfo, void *context)
{
    printf ("Sending PID: %ld, UID: %ld\n",
            (long)siginfo->si_pid, (long)siginfo->si_uid);
}

int main (int argc, char *argv[])
{
    struct sigaction act;

    fprintf(stderr, "%i pp %i\n",getpid(), getppid());

    memset (&act, '\0', sizeof(act));

    /* Use the sa_sigaction field because the handles has two additional parameters */
    act.sa_sigaction = &hdl;

    /* The SA_SIGINFO flag tells sigaction() to use the sa_sigaction field, not sa_handler. */
    act.sa_flags = SA_SIGINFO;

    if (sigaction(SIGTERM, &act, NULL) < 0) {
        perror ("sigaction");
        return 1;
    }

    while (1)
        sleep (10);

    return 0;
}

For your code,

Rule of thumb: Don't forget to carry burial procedures out even though you are sure that child process ends prior parent process. By invoking wait(...) you tell the operating system that I'm done my things for my child so now you can clean allocated fields etc.

I'd prefer initialize signal utilities prior forking what if the parent process doesn't have a chance to register signal action? Moreover, I don't understand why you handle 0 and 1 cases in switch. Intrinsically the cases aren't hit, so always omitted.

In addition, you didn't use break in your if condition within main(). It doesn't go in if after a while yet the following circumstance which is not anticipated and desirable is that the program stays forever in while() loop. I'd prefer to put signaled into condition of while() loop.

At last but not least, due to sleep() call in child process until signaled is turned out 0, SIGTERM is caught several times successfully. When signaled is 0, the loop stops.

#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <memory.h>
#include <sys/wait.h>

volatile sig_atomic_t histogram[3] = {0,0,0};
volatile sig_atomic_t signaled = 0;
const int testsig = SIGTERM;

void sigaction_handler(int sig, siginfo_t* info, void* context)
{
    switch (info->si_pid) {
    case 0:
    case 1:
        histogram[info->si_pid]++;
        break;

    default:
        fprintf(stderr, "sender pid -> %i\n", info->si_pid);
        histogram[2]++;
        break;
    }
    signaled = 1;
}

int main(int argc, const char * argv[]) {


    struct sigaction sigAction;
    memset( &sigAction, 0, sizeof( sigAction ) );

    sigAction.sa_sigaction = sigaction_handler;
    sigemptyset (&sigAction.sa_mask);
    sigAction.sa_flags = SA_SIGINFO;
    sigaction(testsig, &sigAction, NULL);

    pid_t mainpid = getpid();
    pid_t pid = fork();
    if (pid == 0) {
        fprintf(stderr, "my pid -> %i parent's pid-> %i\n", getpid(), getppid());
        if (kill(mainpid, 0) == 0) { // signals are not queued not need loop
            sleep(1);
            kill(mainpid, testsig);
        }
        _exit(0);
    } else {

        wait(NULL); // play with this line to see what the difference is
        while ( signaled ) {
            printf("pid 0: %d, pid 1: %d, others: %d\n", histogram[0], histogram[1], histogram[2]);
            signaled = 0;
            sleep(1);
        }
        // wait(NULL); // play with this line to see what the difference is

    }
}
Unwashed answered 3/12, 2018 at 15:31 Comment(3)
strange, your forkless sample doesnt work for me either on my machine. both pid and uid are still 0.Rascality
To be clear the SIGTERM I'm expecting should come from either launchd or a terminal (neither work for me). the wait and loop stuff you touch on is moot and merely part of what I'd hoped would be a clear example that the sender pid is always 0 on mojave; "fixing" them does nothing to change the situation.Rascality
ok, so with your second example the wait seems to clear things up regarding signals sent from the children. Unfortunately, my IRL code requires this from a terminal process and I still cannot get that to work. Any ideas there?Rascality
R
2

It turns out that debugging via Xcode LLDB is the culprit. If I build and run the program normally it works fine. If I find out why I will update this answer.

I already have the "PASS" set for SIGTERM in lldb as noted in the question, so it seems like somehow there is a bug in the version of lldb shipped with Xcode 10.0 and it is "passing" the signal by creating a new struct and setting the signal number rather then the structure that would have normally been received. As I stated before this did used to work fine in whatever version of lldb shipped with macos 10.12

If somebody has a better explaination, please post an answer and I will accept and award bounty.

Rascality answered 4/12, 2018 at 17:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.