How to Force a Terminated Process to Return Exit Code 0 in Linux


1 views

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