Bug 32423 - Use After Free when breakpoint is deleted from within breakpoint stop() python callback
Summary: Use After Free when breakpoint is deleted from within breakpoint stop() pytho...
Status: UNCONFIRMED
Alias: None
Product: gdb
Classification: Unclassified
Component: python (show other bugs)
Version: HEAD
: P2 normal
Target Milestone: ---
Assignee: Not yet assigned to anyone
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2024-12-06 12:34 UTC by k4lizen
Modified: 2024-12-09 12:54 UTC (History)
1 user (show)

See Also:
Host:
Target:
Build:
Last reconfirmed:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description k4lizen 2024-12-06 12:34:55 UTC
OS:       6.12.1-artix1-1
Gdb:      16.0.50.20241206-git
Python:   3.12.7

related:
1. https://sourceware.org/bugzilla/show_bug.cgi?id=12802
2. https://sourceware.org/gdb/current/onlinedocs/gdb.html/Breakpoints-In-Python.html#Breakpoints-In-Python

> You should not alter the execution state of the inferior (i.e., step, next, etc.),
> alter the current frame context (i.e., change the current active frame), or alter,
> add or delete any breakpoint. As a general rule, you should not alter any data 
> within GDB or the inferior at this time. 

That being said, a Use-After-Free should probably be avoided.

# Reproduction:

repro.py
```python
import gdb

class StuffBreak(gdb.Breakpoint):
    def __init__(self):
        super().__init__("stuff")
        print("stuff break set")

    def stop(self):
        EvilBreak()

class EvilBreak(gdb.FinishBreakpoint):
    def __init__(self):
        super().__init__()
        print("finish break set")

    def stop(self):
        print("break hit")
        self.delete()
        return False

StuffBreak()
print("Loaded.")
```

repro.c
```c
void stuff(){
    // i dont do anything :7
}

int main() {
    for( int i = 0; i < 1000000000; ++i){
        stuff();
    }
}
```

~> gcc repro.c -g -o repro
~> gdb -nx -ex "source repro.py" -ex "run" ./repro

[..snip]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
Temporary breakpoint 2 at 0x55555555513b: file minimal.c, line 6.
finish break set
break hit


Fatal signal: Segmentation fault
----- Backtrace -----
0x61e37e5df56d gdb_internal_backtrace_1
	~/binutils-gdb/gdb/bt-utils.c:121
0x61e37e5df64c _Z22gdb_internal_backtracev
	~/binutils-gdb/gdb/bt-utils.c:182
0x61e37e81a3ae handle_fatal_signal
	~/binutils-gdb/gdb/event-top.c:1018
0x61e37e81a53d handle_sigsegv
	~/binutils-gdb/gdb/event-top.c:1089
0x7bb728c4eb7f ???
0x61e37e5b6931 _Z18disable_breakpointP10breakpoint
	~/binutils-gdb/gdb/breakpoint.c:13674
0x61e37ea6d690 _Z25bpfinishpy_post_stop_hookP23gdbpy_breakpoint_object
	python/py-finishbreakpoint.c:153
0x61e37ea5a128 _Z31gdbpy_breakpoint_cond_says_stopPK23extension_language_defnP10breakpoint
	python/py-breakpoint.c:1196
0x61e37e824b5b _Z34breakpoint_ext_lang_cond_says_stopP10breakpoint
	~/binutils-gdb/gdb/extension.c:621
0x61e37e59d631 bpstat_check_breakpoint_conditions
	~/binutils-gdb/gdb/breakpoint.c:5684
0x61e37e59e19b _Z18bpstat_stop_statusPK13address_spacemP11thread_infoRK17target_waitstatusP6bpstat
	~/binutils-gdb/gdb/breakpoint.c:5938
0x61e37e90be91 handle_signal_stop
	~/binutils-gdb/gdb/infrun.c:7103
0x61e37e90a3dc handle_inferior_event
	~/binutils-gdb/gdb/infrun.c:6556
0x61e37e904fb0 _Z20fetch_inferior_eventv
	~/binutils-gdb/gdb/infrun.c:4698
0x61e37e8da6e6 _Z22inferior_event_handler19inferior_event_type
	~/binutils-gdb/gdb/inf-loop.c:42
0x61e37e962e9a handle_target_event
	~/binutils-gdb/gdb/linux-nat.c:4440
0x61e37ef1d9ba handle_file_event
	~/binutils-gdb/gdbsupport/event-loop.cc:551
0x61e37ef1e037 gdb_wait_for_event
	~/binutils-gdb/gdbsupport/event-loop.cc:672
0x61e37ef1ccfe _Z16gdb_do_one_eventi
	~/binutils-gdb/gdbsupport/event-loop.cc:216
0x61e37ec4e2f0 _Z22wait_sync_command_donev
	~/binutils-gdb/gdb/top.c:422
0x61e37ec4e3a4 _Z28maybe_wait_sync_command_donei
	~/binutils-gdb/gdb/top.c:439
0x61e37ec4ea75 _Z15execute_commandPKci
	~/binutils-gdb/gdb/top.c:572
0x61e37e998a73 catch_command_errors
	~/binutils-gdb/gdb/main.c:508
0x61e37e998ca7 execute_cmdargs
	~/binutils-gdb/gdb/main.c:607
0x61e37e99a324 captured_main_1
	~/binutils-gdb/gdb/main.c:1305
0x61e37e99a581 captured_main
	~/binutils-gdb/gdb/main.c:1330
0x61e37e99a620 _Z8gdb_mainP18captured_main_args
	~/binutils-gdb/gdb/main.c:1359
0x61e37e49d91a main
	~/binutils-gdb/gdb/gdb.c:38
---------------------
A fatal error internal to GDB has been detected, further
debugging is not possible.  GDB will now terminate.

This is a bug, please report it.  For instructions, see:
<https://www.gnu.org/software/gdb/bugs/>.

fish: Job 1, 'gdb -nx -ex "source minimal.py"…' terminated by signal SIGSEGV (Address boundary error)
~>

# Cause:

Here is the backtrace of the breakpoint being deleted:
```
#0  delete_breakpoint (bpt=0x555557b91e30) at breakpoint.c:12591
#1  0x0000555555ba2aba in bppy_delete_breakpoint (self=0x7fffdfa5de50, args=0x0) at python/py-breakpoint.c:442
#2  0x00007ffff77b5724 in ?? () from target:/usr/lib/libpython3.12.so.1.0
#3  0x00007ffff779f984 in PyObject_Vectorcall () from target:/usr/lib/libpython3.12.so.1.0
#4  0x00007ffff778607f in _PyEval_EvalFrameDefault () from target:/usr/lib/libpython3.12.so.1.0
#5  0x00007ffff77dbfeb in ?? () from target:/usr/lib/libpython3.12.so.1.0
#6  0x00007ffff77dbaf9 in ?? () from target:/usr/lib/libpython3.12.so.1.0
#7  0x00007ffff7777aa4 in ?? () from target:/usr/lib/libpython3.12.so.1.0
#8  0x00007ffff78a7a4e in _PyObject_CallMethod_SizeT () from target:/usr/lib/libpython3.12.so.1.0
#9  0x0000555555ba72f6 in gdb_PyObject_CallMethod<>(PyObject *, const char *, const char *) (o=0x7fffdd6089d0, method=0x5555560dc3a4 <stop_func> "stop", format=0x0) at python/python-internal.h:149
#10 0x0000555555ba51d1 in gdbpy_breakpoint_cond_says_stop (extlang=0x555556442b80 <extension_language_python>, b=0x555557598590) at python/py-breakpoint.c:1177
#11 0x00005555559851b9 in breakpoint_ext_lang_cond_says_stop (b=0x555557598590) at extension.c:628
#12 0x0000555555733cfa in bpstat_check_breakpoint_conditions (bs=0x555557f161d0, thread=0x5555571f9240) at breakpoint.c:5610
#13 0x0000555555734864 in bpstat_stop_status (aspace=0x555556594670, bp_addr=93824992415523, thread=0x5555571f9240, ws=..., stop_chain=0x555557597810) at breakpoint.c:5864
#14 0x0000555555a63c88 in handle_signal_stop (ecs=0x7fffffffdc40) at infrun.c:7102
#15 0x0000555555a621d3 in handle_inferior_event (ecs=0x7fffffffdc40) at infrun.c:6555
#16 0x0000555555a5cda7 in fetch_inferior_event () at infrun.c:4697
#17 0x0000555555a32175 in inferior_event_handler (event_type=INF_REG_EVENT) at inf-loop.c:41
#18 0x0000555555ab58db in handle_target_event (error=0, client_data=0x0) at linux-nat.c:4440
#19 0x0000555555fd7fd9 in handle_file_event (file_ptr=0x55555759dd90, ready_mask=1) at event-loop.cc:551
#20 0x0000555555fd8656 in gdb_wait_for_event (block=0) at event-loop.cc:672
#21 0x0000555555fd731d in gdb_do_one_event (mstimeout=-1) at event-loop.cc:216
#22 0x0000555555ae9bb9 in start_event_loop () at main.c:400
#23 0x0000555555ae9d85 in captured_command_loop () at main.c:464
#24 0x0000555555aeb941 in captured_main (data=0x7fffffffe020) at main.c:1337
#25 0x0000555555aeb9db in gdb_main (args=0x7fffffffe020) at main.c:1356
#26 0x00005555556428bb in main (argc=2, argv=0x7fffffffe158) at gdb.c:38
#27 0x00007ffff6e0ad6e in __libc_start_call_main (main=main@entry=0x555555642809 <main(int, char**)>, argc=argc@entry=2, argv=argv@entry=0x7fffffffe158) at ../sysdeps/nptl/libc_start_call_main.h:58
#28 0x00007ffff6e0ae2a in __libc_start_main_impl (main=0x555555642809 <main(int, char**)>, argc=2, argv=0x7fffffffe158, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe148)
    at ../csu/libc-start.c:360
#29 0x0000555555642735 in _start ()
```

From what I can tell the problem is that inside handle_signal_stop:
```c 
 /* See if there is a breakpoint/watchpoint/catchpoint/etc. that
     handles this event.  */
  ecs->event_thread->control.stop_bpstat
    = bpstat_stop_status (ecs->event_thread->inf->aspace.get (),
			  ecs->event_thread->stop_pc (),
			  ecs->event_thread, ecs->ws, stop_chain);

  /* Following in case break condition called a
      function.  */
  stop_print_frame = true;

  /* This is where we handle "moribund" watchpoints.  Unlike
     software breakpoints traps, hardware watchpoint traps are
     always distinguishable from random traps.  If no high-level
     watchpoint is associated with the reported stop data address
     anymore, then the bpstat does not explain the signal ---
     simply make sure to ignore it if `stopped_by_watchpoint' is
     set.  */

  if (ecs->event_thread->stop_signal () == GDB_SIGNAL_TRAP
      && !bpstat_explains_signal (ecs->event_thread->control.stop_bpstat,
				  GDB_SIGNAL_TRAP)
      && stopped_by_watchpoint)
    {
      infrun_debug_printf ("no user watchpoint explains watchpoint SIGTRAP, "
			   "ignoring");
    }
```

The `bpstat_stop_status` call returns a bpstat with a breakpoint, however it
calls the python callback (through `bpstat_check_breakpoint_conditions`)
allowing the breakpoint to be deleted, with the function still having a local
pointer to it.

The breakpoint is then used in the `bpstat_explains_signal` call also
shown above, and segfaults here:

```c
   4615           if (sig == GDB_SIGNAL_TRAP)
   4616             return true;
   4617         }
   4618       else
   4619         {
 ► 4620           if (bsp->breakpoint_at->explains_signal (sig))
   4621             return true;
   4622         }
   4623     }
   4624 
   4625   return false;
```

# Security
The bug can probably be used to gain code execution though reallocating a 0x170 heap
object with an arbitrary function pointer at the correct offset. That being said it requires
executing arbitrary python code in the first place.
Comment 1 k4lizen 2024-12-06 13:14:06 UTC
This also works:
repro.c
```c
void stuff(){
    // i dont do anything :7
}

int main() {
    stuff();
}
```
Comment 2 Tom de Vries 2024-12-09 04:41:57 UTC
This seems to works:
...
diff --git a/gdb/python/py-breakpoint.c b/gdb/python/py-breakpoint.c
index 75f50e1f423..11c205d9dfb 100644
--- a/gdb/python/py-breakpoint.c
+++ b/gdb/python/py-breakpoint.c
@@ -1168,12 +1168,18 @@ gdbpy_breakpoint_cond_says_stop (const struct extension_langua
ge_defn *extlang,
 
   gdbpy_enter enter_py (b->gdbarch);
 
+  /* Create a reference to the python object, keeping it alive in case the
+     breakpoint gets deleted in the stop method.  */
+  gdbpy_ref<> py_bp_ref = gdbpy_ref<>::new_reference (py_bp);
+
   if (bp_obj->is_finish_bp)
     bpfinishpy_pre_stop_hook (bp_obj);
 
   if (PyObject_HasAttrString (py_bp, stop_func))
     {
       gdbpy_ref<> result = gdbpy_call_method (py_bp, stop_func);
+      if (bp_obj->bp == nullptr)
+	error (_("Breakpoint deleted in %s.stop"), Py_TYPE (bp_obj)->tp_name);
 
       stop = 1;
       if (result != NULL)
...
getting us:
...
$ gdb -q -ex "source repro.py" -ex "run" ./repro
Reading symbols from ./repro...
Breakpoint 1 at 0x40049b: file repro.c, line 3.
stuff break set
Loaded.
Starting program: /data/vries/gdb/repro 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Temporary breakpoint 2 at 0x4004b9: file repro.c, line 6.
finish break set
break hit
Breakpoint deleted in EvilBreak.stop
(gdb) 
...