Skip to content

Conversation

@dearblue
Copy link
Contributor

Calling mrb_gc_unregistor() from mrb_data_type::dfree caused a use-after-free deep inside mrb_close().
The impetus to investigate was #6342 (review).

Currently, when mrb_close() is called, all objects are destroyed first.
The process is done heap page by heap page, and when all objects belonging to a heap page are destroyed, the heap page is released.
If the next heap page contains RData objects, the mrb_gc_unregistor() function may be called from the mrb_data_type::dfree function.
At this time, the mrb_gc_unregistor() function gets an array object from a Ruby global variable.
If the array object belongs to a freed heap page, use-after-free is established by referencing this array object.

About the fixes.

First of all, there is the fact that the mrb_gv_get() function returns nil if mrb->globals is NULL.
Therefore, before destroying all objects, free mrb->globals and set mrb->globals to NULL at the same time.
Now the mrb_gv_get() function will return nil to the calling mrb_gc_unregistor() function and mrb_gc_unregistor() will do nothing more.

ref. #4618

Calling `mrb_gc_unregistor()` from `mrb_data_type::dfree` caused a use-after-free deep inside `mrb_close()`.
The impetus to investigate was <mruby#6342 (review)>.

Currently, when `mrb_close()` is called, all objects are destroyed first.
The process is done heap page by heap page, and when all objects belonging to a heap page are destroyed, the heap page is released.
If the next heap page contains `RData` objects, the `mrb_gc_unregistor()` function may be called from the `mrb_data_type::dfree` function.
At this time, the `mrb_gc_unregistor()` function gets an array object from a Ruby global variable.
If the array object belongs to a freed heap page, use-after-free is established by referencing this array object.

About the fixes.

First of all, there is the fact that the `mrb_gv_get()` function returns `nil` if `mrb->globals` is `NULL`.
Therefore, before destroying all objects, free `mrb->globals` and set `mrb->globals` to `NULL` at the same time.
Now the `mrb_gv_get()` function will return `nil` to the calling `mrb_gc_unregistor()` function and `mrb_gc_unregistor()` will do nothing more.

ref. mruby#4618
@dearblue dearblue requested a review from matz as a code owner October 22, 2024 14:08
@github-actions github-actions bot added the core label Oct 22, 2024
@dearblue
Copy link
Contributor Author

Reproduction code: uaf-gc-unregister.c

#if 0
#!/bin/sh

set -e

$(build/host/bin/mruby-config --cc --cflags) "$0" $(build/host/bin/mruby-config --ldflags --libs)

valgrind -- ./a.out
#lldb19 -- ./a.out

exit 0
#endif

#include <mruby.h>
#include <mruby/array.h>
#include <mruby/data.h>

#ifndef MRB_HEAP_PAGE_SIZE
# define MRB_HEAP_PAGE_SIZE 1024
#endif

struct mydata
{
  mrb_value ary;
};

static void
mydata_free(mrb_state *mrb, void *opaque)
{
  if (opaque) {
    struct mydata *p = (struct mydata *)opaque;
    mrb_gc_unregister(mrb, p->ary);
    mrb_free(mrb, p);
  }
}

static const mrb_data_type mydata_type = { "mydata", mydata_free };

int
main(int argc, char *argv[])
{
  mrb_state *mrb = mrb_open();

  struct RData *mydata_obj = mrb_data_object_alloc(mrb, mrb->object_class, NULL, NULL);
  mydata_obj->data = mrb_calloc(mrb, 1, sizeof(struct mydata));
  mydata_obj->type = &mydata_type;

  struct mydata *p = (struct mydata *)mydata_obj->data;
  p->ary = mrb_ary_new(mrb);
  int ai = mrb_gc_arena_save(mrb);

  // RData オブジェクトと "_gc_root_" グローバル変数オブジェクトを別のヒープページに確保する
  for (int i = 0; i < MRB_HEAP_PAGE_SIZE * 2; i++) {
    mrb_ary_push(mrb, p->ary, mrb_ary_new(mrb));
    mrb_gc_arena_restore(mrb, ai);
  }

  mrb_gc_register(mrb, p->ary);

  mrb_close(mrb);

  return 0;
}

Run through valgrind based on current master

% sh ./uaf-gc-unregister.c
==24341== Memcheck, a memory error detector
==24341== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==24341== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==24341== Command: ./a.out
==24341==
==24341== Invalid read of size 1
==24341==    at 0x26F3F7: mrb_gc_unregister (gc.c:452)
==24341==    by 0x25F25C: mydata_free (uaf-gc-unregister.c:32)
==24341==    by 0x26FD96: obj_free (gc.c:850)
==24341==    by 0x26F1B8: free_heap (gc.c:359)
==24341==    by 0x26F1B8: mrb_gc_destroy (gc.c:368)
==24341==    by 0x27D892: mrb_close (state.c:188)
==24341==    by 0x25F234: main (uaf-gc-unregister.c:60)
==24341==  Address 0x54df090 is 10,800 bytes inside a block of size 49,184 free'd
==24341==    at 0x484F2EC: free (vg_replace_malloc.c:993)
==24341==    by 0x27DD48: mrb_default_allocf (allocf.c:22)
==24341==    by 0x26F16E: mrb_free (gc.c:259)
==24341==    by 0x26F16E: free_heap (gc.c:361)
==24341==    by 0x26F16E: mrb_gc_destroy (gc.c:368)
==24341==    by 0x27D892: mrb_close (state.c:188)
==24341==    by 0x25F234: main (uaf-gc-unregister.c:60)
==24341==  Block was alloc'd at
==24341==    at 0x4852321: realloc (vg_replace_malloc.c:1806)
==24341==    by 0x26EFDD: mrb_realloc_simple (gc.c:197)
==24341==    by 0x26EFDD: mrb_realloc (gc.c:211)
==24341==    by 0x26EFDD: mrb_malloc (gc.c:227)
==24341==    by 0x26EFDD: mrb_calloc (gc.c:245)
==24341==    by 0x26EFDD: add_heap (gc.c:299)
==24341==    by 0x26EDA5: mrb_obj_alloc (gc.c:506)
==24341==    by 0x25F331: ary_new_capa (array.c:48)
==24341==    by 0x25F331: mrb_ary_new_capa (array.c:65)
==24341==    by 0x25F331: mrb_ary_new (array.c:72)
==24341==    by 0x25F207: main (uaf-gc-unregister.c:54)
==24341==
==24341==
==24341== HEAP SUMMARY:
==24341==     in use at exit: 0 bytes in 0 blocks
==24341==   total heap usage: 729 allocs, 729 frees, 261,916 bytes allocated
==24341==
==24341== All heap blocks were freed -- no leaks are possible
==24341==
==24341== For lists of detected and suppressed errors, rerun with: -s
==24341== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

@matz matz merged commit 3d3aa92 into mruby:master Oct 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants