Skip to content

Commit

Permalink
Merge pull request libdebug#96 from libdebug/atexit-handler
Browse files Browse the repository at this point in the history
Add atexit handler to kill any living process
  • Loading branch information
io-no authored Sep 12, 2024
2 parents 7b798b6 + a0a1788 commit fa43d10
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 5 deletions.
5 changes: 5 additions & 0 deletions docs/source/basic_features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ After creating the debugger object, you can start the execution of the program u
The `run()` command returns a `PipeManager` object, which you can use to interact with the program's standard input, output, and error. To read more about the PipeManager interface, please refer to the PipeManager documentation :class:`libdebug.utils.pipe_manager.PipeManager`. Please note that breakpoints are not kept between different runs of the program. If you want to set a breakpoint again, you should do so after the program has restarted.

Any process will be automatically killed when the debugging script exits. If you want to prevent this behavior, you can set the `kill_on_exit` parameter to False when creating the debugger object, or set the companion attribute `kill_on_exit` to False at runtime.

The command queue
-----------------
Control flow commands, register access and memory access are all done through the command queue. This is a FIFO queue of commands that are executed in order.
Expand Down Expand Up @@ -275,6 +277,9 @@ An alternative to running the program from the beginning and to resume libdebug
d.attach(pid)
Do note that libdebug automatically kills any running process when the debugging script exits, even if the debugger has detached from it.
If you want to prevent this behavior, you can set the `kill_on_exit` parameter to False when creating the debugger object, or set the companion attribute `kill_on_exit` to False at runtime.

Graceful Termination
====================

Expand Down
16 changes: 14 additions & 2 deletions libdebug/debugger/debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ def kill(self: Debugger) -> None:
self._internal_debugger.kill()

def terminate(self: Debugger) -> None:
"""Terminates the background thread.
"""Interrupts the process, kills it and then terminates the background thread.
The debugger object cannot be used after this method is called.
The debugger object will not be usable after this method is called.
This method should only be called to free up resources when the debugger object is no longer needed.
"""
self._internal_debugger.terminate()
Expand Down Expand Up @@ -328,6 +328,18 @@ def arch(self: Debugger, value: str) -> None:
"""Set the architecture of the process."""
self._internal_debugger.arch = map_arch(value)

@property
def kill_on_exit(self: Debugger) -> bool:
"""Get whether the process will be killed when the debugger exits."""
return self._internal_debugger.kill_on_exit

@kill_on_exit.setter
def kill_on_exit(self: Debugger, value: bool) -> None:
if not isinstance(value, bool):
raise TypeError("kill_on_exit must be a boolean")

self._internal_debugger.kill_on_exit = value

@property
def threads(self: Debugger) -> list[ThreadContext]:
"""Get the list of threads in the process."""
Expand Down
16 changes: 14 additions & 2 deletions libdebug/debugger/internal_debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ class InternalDebugger:
syscalls_to_not_pprint: list[int] | None
"""The syscalls to not pretty print."""

kill_on_exit: bool
"""A flag that indicates if the debugger should kill the debugged process when it exits."""

threads: list[ThreadContext]
"""A list of all the threads of the debugged process."""

Expand Down Expand Up @@ -187,6 +190,7 @@ def __init__(self: InternalDebugger) -> None:
self._is_running = False
self.resume_context = ResumeContext()
self.arch = map_arch(libcontext.platform)
self.kill_on_exit = True
self._process_memory_manager = ProcessMemoryManager()
self.fast_memory = False
self.__polling_thread_command_queue = Queue()
Expand Down Expand Up @@ -332,11 +336,19 @@ def kill(self: InternalDebugger) -> None:
self._join_and_check_status()

def terminate(self: InternalDebugger) -> None:
"""Terminates the background thread.
"""Interrupts the process, kills it and then terminates the background thread.
The debugger object cannot be used after this method is called.
The debugger object will not be usable after this method is called.
This method should only be called to free up resources when the debugger object is no longer needed.
"""
if self.instanced and self.running:
self.interrupt()

if self.instanced:
self.kill()

self.instanced = False

if self.__polling_thread is not None:
self.__polling_thread_command_queue.put((THREAD_TERMINATE, ()))
self.__polling_thread.join()
Expand Down
31 changes: 30 additions & 1 deletion libdebug/debugger/internal_debugger_holder.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
#
# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
# Copyright (c) 2024 Gabriele Digregorio. All rights reserved.
# Copyright (c) 2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for details.
#

from __future__ import annotations

import atexit
from dataclasses import dataclass, field
from threading import Lock
from typing import TYPE_CHECKING
from weakref import WeakKeyDictionary

from libdebug.liblog import liblog

if TYPE_CHECKING:
from libdebug.debugger.internal_debugger import InternalDebugger


@dataclass
class InternalDebuggerHolder:
Expand All @@ -19,3 +28,23 @@ class InternalDebuggerHolder:


internal_debugger_holder = InternalDebuggerHolder()


def _cleanup_internal_debugger() -> None:
"""Cleanup the internal debugger."""
for debugger in set(internal_debugger_holder.internal_debuggers.values()):
debugger: InternalDebugger

if debugger.instanced and debugger.kill_on_exit:
try:
debugger.interrupt()
except Exception as e:
liblog.debugger(f"Error while interrupting debuggee: {e}")

try:
debugger.terminate()
except Exception as e:
liblog.debugger(f"Error while terminating the debugger: {e}")


atexit.register(_cleanup_internal_debugger)
3 changes: 3 additions & 0 deletions libdebug/libdebug.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def debugger(
continue_to_binary_entrypoint: bool = True,
auto_interrupt_on_command: bool = False,
fast_memory: bool = False,
kill_on_exit: bool = True,
) -> Debugger:
"""This function is used to create a new `Debugger` object. It returns a `Debugger` object.
Expand All @@ -29,6 +30,7 @@ def debugger(
continue_to_binary_entrypoint (bool, optional): Whether to automatically continue to the binary entrypoint. Defaults to True.
auto_interrupt_on_command (bool, optional): Whether to automatically interrupt the process when a command is issued. Defaults to False.
fast_memory (bool, optional): Whether to use a faster memory reading method. Defaults to False.
kill_on_exit (bool, optional): Whether to kill the debugged process when the debugger exits. Defaults to True.
Returns:
Debugger: The `Debugger` object.
Expand All @@ -44,6 +46,7 @@ def debugger(
internal_debugger.auto_interrupt_on_command = auto_interrupt_on_command
internal_debugger.escape_antidebug = escape_antidebug
internal_debugger.fast_memory = fast_memory
internal_debugger.kill_on_exit = kill_on_exit

debugger = Debugger()
debugger.post_init_(internal_debugger)
Expand Down
Binary file added test/amd64/binaries/infinite_loop_test
Binary file not shown.
11 changes: 11 additions & 0 deletions test/amd64/run_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import unittest

from scripts.alias_test import AliasTest
from scripts.atexit_handler_test import AtexitHandlerTest
from scripts.attach_detach_test import AttachDetachTest
from scripts.auto_waiting_test import AutoWaitingNlinks, AutoWaitingTest
from scripts.backtrace_test import BacktraceTest
Expand Down Expand Up @@ -205,6 +206,16 @@ def fast_suite():
suite.addTest(AliasTest("test_finish_alias"))
suite.addTest(AliasTest("test_waiting_alias"))
suite.addTest(AliasTest("test_interrupt_alias"))
suite.addTest(AtexitHandlerTest("test_attach_detach_1"))
suite.addTest(AtexitHandlerTest("test_attach_detach_2"))
suite.addTest(AtexitHandlerTest("test_attach_detach_3"))
suite.addTest(AtexitHandlerTest("test_attach_detach_4"))
suite.addTest(AtexitHandlerTest("test_attach_1"))
suite.addTest(AtexitHandlerTest("test_attach_2"))
suite.addTest(AtexitHandlerTest("test_run_1"))
suite.addTest(AtexitHandlerTest("test_run_2"))
suite.addTest(AtexitHandlerTest("test_run_3"))
suite.addTest(AtexitHandlerTest("test_run_4"))
return suite


Expand Down
Loading

0 comments on commit fa43d10

Please sign in to comment.