Skip to content

Conversation

@MrIndeciso
Copy link
Member

No description provided.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds initial support for exec/execve syscall interception and handling, building upon the existing multiprocessing/fork support. The changes include refactoring EventType from a class with string attributes to a proper Enum in a dedicated module, converting ResumeContext to a dataclass, and implementing exec event detection and cleanup handlers.

Key Changes:

  • Introduced a new EventType.EXEC enum member and moved EventType to a separate module (libdebug/data/event_type.py)
  • Implemented exec event handling that properly cleans up threads, breakpoints, syscall handlers, and signal catchers when a process calls exec
  • Added public API methods (hook_event, unhook_event, current_argv, current_path) to support event-driven debugging and inspection of process state after exec

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
test/scripts/multiprocessing_test.py Updated multiprocessing tests to explicitly validate FORK events using the new EventType enum and resume_context
test/scripts/exec_test.py Added new test file for exec functionality (currently contains only a placeholder test)
test/scripts/__init__.py Imported and exposed the new ExecTest class
test/run_suite.py Added ExecTest to the fast test suite
newsfragments/273.feature.md Added changelog entry documenting the new exec support feature
libdebug/state/resume_context.py Converted ResumeContext to a dataclass and moved EventType to a separate module; removed the nested EventType class definition
libdebug/ptrace/ptrace_status_handler.py Added _handle_exec method and exec event case handler; updated FORK handling to include VFORK and set resume to False
libdebug/debugger/internal_debugger.py Added event callback system (hook_event, unhook_event), event callback propagation to child processes, clear_internal_state method, and modified wait loop to execute event callbacks
libdebug/debugger/debugger.py Exposed event hooking methods and added current_argv, current_path, and resume_context properties for runtime inspection
libdebug/data/event_type.py New file defining EventType as an Enum with all event types including the new EXEC event

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@MrIndeciso MrIndeciso requested a review from io-no November 24, 2025 21:06
@MrIndeciso
Copy link
Member Author

@io-no I think we need the junior dev's opinion on this draft implementation, I think there's problems that we need to consider but I like the main idea of this.

continue

# We check if we have any event callbacks to execute
for event_type in list(self.resume_context.event_type.values()):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should call this at each wait, no? Why just when we have an event w/o a callback? Also, I find it would be more coherent to have it in the manage_change flow

self.forward_signal = False
# We interrupt the process to allow the user to handle the exec event
self.internal_debugger.resume_context.event_type[pid] = EventType.EXEC
self.internal_debugger.resume_context.resume = False
Copy link
Member

@io-no io-no Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having False here as a fixed value is controversial. I mean, what's the point of having a callback on the exec/fork, so?

It is not coherent with the normal libdebug behaviour, where the hooks decide if something should stop or not the process. I see the point in having a default value when no hooks are registered, but the hooks must override this value. Also, I am not totally sure I want False as the default value (again, libdebug never stops the process if no hooks w/o callbacks are installed)

self.internal_debugger.set_child_debugger(message)
self.forward_signal = False
self.internal_debugger.resume_context.event_type[pid] = EventType.FORK
self.internal_debugger.resume_context.resume = False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See exec

@io-no
Copy link
Member

io-no commented Nov 29, 2025

Okay for the main idea. It seems coherent with the direction we discussed for event hooking (and what the pentester suggested to us).

if "_process_name" in self.__dict__:
del self._process_name

def clear_internal_state(self: InternalDebugger) -> None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should clear also the new hooks

@io-no io-no mentioned this pull request Dec 5, 2025
@MrIndeciso
Copy link
Member Author

Things to think about:

  • entry/exit state of syscalls after fork/exec/clone

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 16 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@MrIndeciso MrIndeciso force-pushed the support-exec branch 3 times, most recently from df554e4 to 38cd949 Compare December 29, 2025 15:19
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

elif signum == signal.SIGSTOP and self.internal_debugger.resume_context.force_interrupt:
# The user has requested an interrupt, we need to stop the process despite the ohter signals
elif signum == signal.SIGSTOP and self.internal_debugger.resume_context._force_interrupt:
# The user has requested an interrupt, we need to stop the process despite the other signals
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a typo in the comment: "the user has requested an interrupt, we need to stop the process despite the ohter signals" should be "the user has requested an interrupt, we need to stop the process despite the other signals".

Copilot uses AI. Check for mistakes.
hit_count (int): The number of times this event hook has been triggered.
"""

event: EventType = field(default=None)
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value for the 'event' field is set to None, which is inconsistent with the type annotation EventType. This could lead to issues when the event attribute is accessed before being properly initialized. Consider either removing the default value or using a sentinel value that aligns with the EventType enum.

Suggested change
event: EventType = field(default=None)
event: EventType

Copilot uses AI. Check for mistakes.
return (unsigned long)ip << 8 | TRAP_BRKPT;
}
} catch (...) {
// Could not read memory, just ignore IT
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a typo in the comment: "Could not read memory, just ignore IT" should be "Could not read memory, just ignore it" (lowercase "it").

Suggested change
// Could not read memory, just ignore IT
// Could not read memory, just ignore it

Copilot uses AI. Check for mistakes.
}
// For other si_code values, we don't have extra info to return
else {
// Uknown si_code, return it as is
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a typo in the comment: "Uknown si_code" should be "Unknown si_code".

Suggested change
// Uknown si_code, return it as is
// Unknown si_code, return it as is

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +38
_internal_debugger: InternalDebugger = field(default=None, init=True, repr=False)

Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value for '_internal_debugger' is set to None with init=True, which means EventHook instances could be created with None as the internal debugger. This will cause AttributeError when any property is accessed (e.g., enabled, is_post_hook) since they all call self._internal_debugger._ensure_process_stopped(). Consider making this a required field without a default value, or add validation to ensure it's never None.

Suggested change
_internal_debugger: InternalDebugger = field(default=None, init=True, repr=False)
_internal_debugger: InternalDebugger = field(init=True, repr=False)
def __post_init__(self: "EventHook") -> None:
"""Validate internal debugger after dataclass initialization."""
if self._internal_debugger is None:
raise ValueError("_internal_debugger must not be None")

Copilot uses AI. Check for mistakes.
Comment on lines +970 to +972
def callback(_: ThreadContext, __: EventHook) -> None:
pass

Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inner function 'callback' shadows the parameter 'callback' from the outer scope. When callback is True, this creates a new function with the same name, which then gets passed to EventHook. This works but is confusing and considered bad practice. Consider using a different variable name for the inner function, such as 'noop_callback' or 'default_callback'.

Suggested change
def callback(_: ThreadContext, __: EventHook) -> None:
pass
def default_callback(_: ThreadContext, __: EventHook) -> None:
pass
callback = default_callback

Copilot uses AI. Check for mistakes.
Comment on lines +2014 to +2055
@property
def stop_on_fork(self: InternalDebugger) -> bool:
"""Get whether the debugger stops on fork events."""
return self._stop_on_fork

@stop_on_fork.setter
def stop_on_fork(self: InternalDebugger, value: bool) -> None:
"""Set whether the debugger stops on fork events.
Args:
value (bool): True to stop on fork events, False otherwise.
"""
self._stop_on_fork = value
if not value and self._stop_on_fork_hook:
self.unhook_event(self._stop_on_fork_hook)
self._stop_on_fork_hook = None
elif value and not self._stop_on_fork_hook:
self._stop_on_fork_hook = self.__set_stop_hook(EventType.FORK)

@property
def stop_on_exec(self: InternalDebugger) -> bool:
"""Get whether the debugger stops on exec events."""
return self._stop_on_exec

@stop_on_exec.setter
def stop_on_exec(self: InternalDebugger, value: bool) -> None:
"""Set whether the debugger stops on exec events.
Args:
value (bool): True to stop on exec events, False otherwise.
"""
self._stop_on_exec = value
if not value and self._stop_on_exec_hook:
self.unhook_event(self._stop_on_exec_hook)
self._stop_on_exec_hook = None
elif value and not self._stop_on_exec_hook:
self._stop_on_exec_hook = self.__set_stop_hook(EventType.EXEC)

@property
def stop_on_clone(self: InternalDebugger) -> bool:
"""Get whether the debugger stops on clone events."""
return self._stop_on_clone
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The properties stop_on_fork, stop_on_exec, and stop_on_clone have getters that return self._stop_on_fork, self._stop_on_exec, and self._stop_on_clone respectively, but these attributes are never initialized in init. This causes AttributeError when _setup_utility_hooks() tries to read them (lines 353-358) if they haven't been set via the property setters first. Initialize these attributes to False in the init method to prevent this error.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants