When killing processes in Linux, we often need to control their exit behavior for scripting purposes. The standard termination signals (SIGTERM, SIGKILL) typically result in non-zero exit codes (128 + signal number), which can cause issues when we want dependent scripts to consider the termination as successful.
Linux processes return an exit status between 0-255 when they terminate. By convention:
0 = success
1-127 = program-defined error codes
128+ = terminated by signal (128 + signal number)
This means SIGTERM (15) returns 143 (128+15), and SIGKILL (9) returns 137 (128+9).
For processes you control, the cleanest solution is to trap signals and exit gracefully:
#!/bin/bash
trap "exit 0" SIGTERM SIGINT
# Main process work here
while true; do sleep 1; done
When you can't modify the source code, you can intercept the exit call using LD_PRELOAD:
// fake_exit.c
#include
#include
void _exit(int status) {
exit(0);
}
Compile and use:
gcc -shared -fPIC -o fake_exit.so fake_exit.c
LD_PRELOAD=./fake_exit.so your_program
For already running processes, you can use GDB to modify the exit behavior:
gdb -p PID
(gdb) call (void)exit(0)
(gdb) detach
(gdb) quit
- The LD_PRELOAD method won't work for statically linked binaries
- Some programs have cleanup handlers that might still run
- Security implications of intercepting system calls
For scripting scenarios, consider:
#!/bin/bash
your_program || true
# or
your_program && true
When terminating processes in Linux, the default behavior is for killed processes to return special exit codes indicating they were terminated by signals. For example:
# Example of normal process termination
sleep 10 & wait $!; echo $? # Returns 0
killall sleep; echo $? # Returns 143 (SIGTERM)
The fundamental issue stems from how Unix signal handling works. When a process receives SIGTERM (15), it typically exits with status 128+15=143. For SIGKILL (9), it's 128+9=137.
Option 1: Signal Trapping
For processes you control, you can trap signals:
#!/bin/bash
trap "exit 0" SIGTERM SIGINT
while true; do sleep 1; done
Option 2: LD_PRELOAD Hack
Create a shared library to override _exit():
// override.c
void _exit(int status) {
__real_exit(0);
}
// Compile with:
// gcc -shared -fPIC -o liboverride.so override.c -ldl
// Run with:
// LD_PRELOAD=./liboverride.so your_command
Option 3: GDB Injection
For arbitrary running processes:
gdb -p PID -batch -ex 'call exit(0)'
The most reliable method combines ptrace with careful signal handling:
#! /usr/bin/python3
import os
import signal
import sys
pid = int(sys.argv[1])
os.kill(pid, signal.SIGSTOP)
with open(f"/proc/{pid}/mem", "rb+") as mem:
# Architecture-specific assembly to:
# 1. Set exit code in register
# 2. Call exit syscall
mem.seek(ADDRESS_OF_CODE_CAVE)
mem.write(b"\xb8\x00\x00\x00\x00\xcd\x80") # mov eax,0; int 0x80
os.kill(pid, signal.SIGCONT)
- These methods may cause resource leaks
- Security implications exist with LD_PRELOAD and ptrace
- The GDB method requires proper permissions
- Not all processes can be safely manipulated this way
Consider modifying the parent process instead:
# In wrapper script
your_command || true
Or using process groups:
setsid your_command &
kill -- -$! && wait $! || true