• Kouhei Sutou's avatar
    Fix SEGV on re-raising NoMemoryError · 1d84b320
    Kouhei Sutou authored
    Think about the following Ruby script:
    
    segv.rb:
    
        begin
          lambda do
            lambda do
              "x" * 1000 # NoMemoryError
            end.call
          end.call
        rescue
          raise
        end
    
    If memory can't allocate after `"x" * 1000`, mruby crashes.
    
    Because L_RAISE: block in mrb_vm_exec() calls mrb_env_unshare() via
    cipop() and mrb_env_unshare() uses allocated memory without NULL check:
    
    L_RAISE: block:
    
        L_RAISE:
          // ...
          while (ci[0].ridx == ci[-1].ridx) {
            cipop(mrb);
            // ...
          }
    
    cipop():
    
        static void
        cipop(mrb_state *mrb)
        {
          struct mrb_context *c = mrb->c;
    
          if (c->ci->env) {
            mrb_env_unshare(mrb, c->ci->env);
          }
    
          c->ci--;
        }
    
    mrb_env_unshare():
    
        MRB_API void
        mrb_env_unshare(mrb_state *mrb, struct REnv *e)
        {
          size_t len = (size_t)MRB_ENV_STACK_LEN(e);
          // p is NULL in this case
          mrb_value *p = (mrb_value *)mrb_malloc(mrb, sizeof(mrb_value)*len);
    
          MRB_ENV_UNSHARE_STACK(e);
          if (len > 0) {
            stack_copy(p, e->stack, len); // p is NULL but used. It causes SEGV.
          }
          e->stack = p;
          mrb_write_barrier(mrb, (struct RBasic *)e);
        }
    
    To solve the SEGV, this change always raises NoMemoryError even when
    realloc() is failed after the first NoMemoryError in
    mrb_realloc(). mrb_unv_unshare() doesn't need to check NULL with this
    change.
    
    But it causes infinite loop in the following while:
    
        L_RAISE:
          // ...
          while (ci[0].ridx == ci[-1].ridx) {
            cipop(mrb);
            // ...
          }
    
    Because cipop() never pops ci.
    
    This change includes cipop() change. The change pops ci even when
    mrb_unv_unshare() is failed by NoMemoryError.
    
    This case can be reproduced by the following program:
    
        #include <stdlib.h>
        #include <mruby.h>
        #include <mruby/compile.h>
    
        static void *
        allocf(mrb_state *mrb, void *ptr, size_t size, void *ud)
        {
          static mrb_bool always_fail = FALSE;
    
          if (size == 1001) {
            always_fail = TRUE;
          }
          if (always_fail) {
            return NULL;
          }
    
          if (size == 0) {
            free(ptr);
            return NULL;
          } else {
            return realloc(ptr, size);
          }
        }
    
        int
        main(int argc, char **argv)
        {
          mrb_state *mrb;
          mrbc_context *c;
          FILE *file;
    
          mrb = mrb_open_allocf(allocf, NULL);
          c = mrbc_context_new(mrb);
          file = fopen(argv[1], "r");
          mrb_load_file_cxt(mrb, file, c);
          fclose(file);
          mrbc_context_free(mrb, c);
          mrb_close(mrb);
    
          return EXIT_SUCCESS;
        }
    
    Try the following command lines:
    
        % cc -I include -L build/host/lib -O0 -g3 -o no-memory no-memory.c -lmruby -lm
        % ./no-memory segv.rb
    1d84b320
vm.c 61.5 KB