-
-
Notifications
You must be signed in to change notification settings - Fork 21
Add exec/execv/execve support #273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Pretty sure this isn't correct
There was a problem hiding this 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.EXECenum member and movedEventTypeto 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.
|
@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()): |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See exec
|
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: |
There was a problem hiding this comment.
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
…as first argument
Now it shows up as a property, not a method
Now no behavior change even with the new event API
|
Things to think about:
|
There was a problem hiding this 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.
There was a problem hiding this 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.
Onto writing tests next.
…y still cause CPU burning in some multiprocessing edge cases
df554e4 to
38cd949
Compare
38cd949 to
732919d
Compare
There was a problem hiding this 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 |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
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".
| hit_count (int): The number of times this event hook has been triggered. | ||
| """ | ||
|
|
||
| event: EventType = field(default=None) |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
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.
| event: EventType = field(default=None) | |
| event: EventType |
| return (unsigned long)ip << 8 | TRAP_BRKPT; | ||
| } | ||
| } catch (...) { | ||
| // Could not read memory, just ignore IT |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
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").
| // Could not read memory, just ignore IT | |
| // Could not read memory, just ignore it |
| } | ||
| // For other si_code values, we don't have extra info to return | ||
| else { | ||
| // Uknown si_code, return it as is |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
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".
| // Uknown si_code, return it as is | |
| // Unknown si_code, return it as is |
| _internal_debugger: InternalDebugger = field(default=None, init=True, repr=False) | ||
|
|
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
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.
| _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") |
| def callback(_: ThreadContext, __: EventHook) -> None: | ||
| pass | ||
|
|
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
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'.
| def callback(_: ThreadContext, __: EventHook) -> None: | |
| pass | |
| def default_callback(_: ThreadContext, __: EventHook) -> None: | |
| pass | |
| callback = default_callback |
| @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 |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
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.
No description provided.