Skip to content

Debugging Child Processes

vadimcn edited this page Feb 1, 2026 · 1 revision

LLDB can debug only one process at a time after fork or posix_spawn. It can follow either the parent or the child via target.process.follow-fork-mode, detaching the other.

This script approximates multiprocess debugging in CodeLLDB by automatically attaching to child PIDs when the parent calls clone3.
Limitations:

  • Linux only (glibc oriented), though a similar approach should be possible on other OSes.
  • The child runs briefly before the debugger attaches, so breakpoints in early startup code may be missed.

Usage: Add "preRunCommands": ["command script import <script path>"] to your VS Code launch configuration.

import lldb
import codelldb


def __lldb_init_module(debugger, internal_dict):
    target = debugger.GetTargetAtIndex(0)

    # Break on libc wrapper of the clone3 syscall (https://github.com/bminor/glibc/blob/master/sysdeps/unix/sysv/linux/arm/clone3.S)
    # This is where spawn*(), fork(), etc function calls end up just before plunging into the kernel.
    bp = target.BreakpointCreateByName('__clone3')
    bp.SetScriptCallbackFunction('debug_child_process.on_clone3')


def on_clone3(frame, bp_loc, internal_dict):
    thread = frame.thread
    process = thread.process
    debugger = process.target.debugger

    # thread_create() calls also end up here, however they do not create a new process, so we need to filter them out:
    # Parameters are passed as a pointer to `clone_args` struct, the pointer itself being in the rdi register.
    args_addr = frame.FindRegister('rdi').GetValueAsAddress()
    flags = process.ReadUnsignedFromMemory(args_addr, 8, lldb.SBError())  # Read clone_args.flags field.
    if flags & 0x00010000:  # CLONE_THREAD
        return False  # Tell LLDB to skip this breakpoint

    # Disable async execution.
    # Normally CodeLLDB uses the async mode, so that calls like StepOut() or Continue()
    # return immediately, while the debugger tracks execution via event notifications.
    # In this case, though, we want StepOut() to block until the debuggee stops again.
    try:
        debugger.SetAsync(False)
        # Execute the rest of the function, stop immediately after return.
        thread.StepOut()
    finally:
        # Restore async mode.
        debugger.SetAsync(True)

    # The returned child process id is in the rax register.
    child_pid = thread.GetFrameAtIndex(0).FindRegister('rax').GetValueAsUnsigned()

    # Ask VSCode to attach to the child process.
    codelldb.start_debugging('attach', {'pid': child_pid, 'stopOnEntry': True})
    print('Attaching to', child_pid)

    # Resume the debuggee.
    process.Continue()
    return False

Clone this wiki locally