Problem
I am using the mach interface to suspend tasks and resume them. The code I provided does exactly that: sudo ./code -r <PID>
to resume and sudo ./code -s <PID>
to suspend. This C program compiles and runs, both functions return KERN_SUCCESS, but they have the following problems:
task_suspend
: it does not suspend the task, simple as that.
task_resume
: I had to use kill -SIGSTOP to test it on a stopped task as task_suspend
does not work. It does resume the task, but for some reason, I cannot send SIGSTOP again. If I want to send a STOP signal again I need to perform SIGCONT (even if the task is already running).
Setup
Documentation
task_suspend
increments the suspend_count. Well, it does not. I checked by printing the suspend_count obtainable by task_info()
, task_threads()
and thread_info()
(task suspend count and each thread's suspend count).task_suspend2()
, which I did not even know existed, and the task_suspend_internal()
.Code
#include <mach/mach.h>
#include <mach/mach_error.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define OPTION_STOP 1
#define OPTION_RESUME 2
void check_and_usage(int argc, char **argv)
{
if (argc == 2 && !strcmp(argv[1], "help")) {
fprintf(stdout, "\nmtask [-OPTION] [PIDs ..]\n\n[OPTIONS]\n -r : RESUME a task.\n -s : STOP a task.\n\n");
exit(0);
}
if (argc < 3) {
fprintf(stderr, "\nNot enough arguments provided!\nmtask [-OPTION] [PIDs ..]\n\n");
exit(1);
}
if (strcmp(argv[1], "-s") && strcmp(argv[1], "-r")) {
fprintf(stderr, "\nInvalid option!\n\n[OPTIONS]\n -r : RESUME a task.\n -s : STOP a task.\n\n");
exit(1);
}
}
void res_check(kern_return_t kern_res, char * msg)
{
if (kern_res != KERN_SUCCESS) {
fprintf(stderr, "%s : %s\n", msg, mach_error_string(kern_res));
exit(1);
}
}
int main(int argc, char **argv)
{
check_and_usage(argc, argv);
int type = 0;
if (!strcmp(argv[1], "-s")) type = OPTION_STOP;
else if (!strcmp(argv[1], "-r")) type = OPTION_RESUME;
kern_return_t kern_res;
for (int i = 2; i < argc; i++) {
mach_port_t task;
int task_pid = atoi(argv[i]);
kern_res = task_for_pid(mach_task_self(), task_pid, &task);
res_check(kern_res, "Error fetching task from pid");
switch (type) {
case OPTION_STOP:
kern_res = task_suspend(task);
break;
case OPTION_RESUME:
kern_res = task_resume(task);
break;
}
res_check(kern_res, "Error changing task state");
mach_port_deallocate(mach_task_self(), task);
}
}
Additional things I tried
I tried for a program to do task_suspend(mach_task_self())
(suspend itself) and then resume it from another terminal. The program that supposedly suspends itself does stop, but I am not sure exactly why as task_suspend()
should not work... Moreover, running task_resume
for that program returns: Error changing task state : (os/kern) failure
.
I know there are alternatives to stop a task, but I am trying to learn and understand the mach interface. This behavior is unexpected and / or undocumented (or quite hidden!). Any information or link is appreciated!
Both calls are working, but the suspension is pointless if the program is going to terminate, as upon its ending the suspended task will be automatically resumed. To make a similar program to what is described in the question alternatives must be found.
Looking in detail at the latest implementation of task_suspend()
and task_resume()
(xnu-7195.81.3) I understood what is going on. I will keep the question and answer in case someone runs into the same problems. Having said that, let's see why these problems are occurring:
task_suspend()
Looking at the task.c file from the Apple Developer repository and its implementation we can see that, after suspending the task, the process claims a send right on the task resume port. Notably, this is stored in the calling process IPC state (more info on IPC here). Finally, the last step before returning is described in a comment within the C file
Any IPC operations that deallocate the send right will auto-release the suspension.
Hence, the program which I provided, which only performs task_suspend()
and finishes, resumes the suspended task upon its completion, as it is freeing the IPC space (which triggers resuming the task). In other words, the call is working, but the suspended task is resumed once the calling process ends.
task_resume()
The situation I described with the signals is quite peculiar. It is possible to deduce what part of the code got executed (knowing it returns KERN_SUCCESS and runs again the task) within the function implementation, but I am not sure why this happens. Somehow, the release function gets called thanks to this:
if (--task->user_stop_count == 0) {
release = TRUE;
}
But how is the user_stop_count equal to 1 when no one suspended it? That is a question for next time.
What we can take from this is that Apple should probably make more documentation and more accessible (personal opinion).