Skip to content

Commit

Permalink
Fix memory leak in Oj.dump if raise an exception (#676)
Browse files Browse the repository at this point in the history
If an exception occurs inside Oj.dump, the heap area has not been released.
The heap area allocated by ALLOC_N must be released.
This patch will fix this problem.

### Test code
```ruby
require 'oj'

data = {
  "foo" => "a" * 1024 * 1024 * 10,
  1 => "Invalid hash key with strict mode"
}

1000.times do |i|
  begin
    Oj.dump(data, mode: :strict)
  rescue
  end

  if i % 100 == 0
    rss = Integer(`ps -o rss= -p #{Process.pid}`) / 1024.0
    puts "#{i}, #{rss}"
  end
end
```

### Before
```
$ ruby json.rb
0, 49.4375
100, 1051.0
200, 1674.03125
300, 1773.625
400, 1822.171875
500, 1842.078125
600, 1841.015625
700, 1815.65625
800, 1781.53125
900, 1792.28125
```

1.8 GB of memory was used in my enviroment.

### After
```
$ ruby json.rb
0, 49.40625
100, 49.421875
200, 49.53125
300, 49.59375
400, 49.65625
500, 49.6875
600, 49.71875
700, 49.734375
800, 49.765625
900, 49.796875
```

50 MB of memory was used in my enviroment.
  • Loading branch information
Watson1978 committed Aug 2, 2021
1 parent cfc14d1 commit c6ad6e9
Showing 1 changed file with 45 additions and 16 deletions.
61 changes: 45 additions & 16 deletions ext/oj/oj.c
Expand Up @@ -1237,6 +1237,38 @@ static VALUE safe_load(VALUE self, VALUE doc) {
* - *io* [_IO__|_String_] IO Object to read from
*/

struct dump_arg {
struct _out *out;
struct _options *copts;
int argc;
VALUE *argv;
};

static VALUE dump_body(VALUE a)
{
volatile struct dump_arg *arg = (void *)a;
VALUE rstr;

oj_dump_obj_to_json_using_params(*arg->argv, arg->copts, arg->out, arg->argc - 1, arg->argv + 1);
if (0 == arg->out->buf) {
rb_raise(rb_eNoMemError, "Not enough memory.");
}
rstr = rb_str_new2(arg->out->buf);
rstr = oj_encode(rstr);

return rstr;
}

static VALUE dump_ensure(VALUE a)
{
volatile struct dump_arg *arg = (void *)a;

if (arg->out->allocated) {
xfree(arg->out->buf);
}
return Qnil;
}

/* Document-method: dump
* call-seq: dump(obj, options={})
*
Expand All @@ -1246,9 +1278,9 @@ static VALUE safe_load(VALUE self, VALUE doc) {
*/
static VALUE dump(int argc, VALUE *argv, VALUE self) {
char buf[4096];
struct dump_arg arg;
struct _out out;
struct _options copts = oj_default_options;
VALUE rstr;

if (1 > argc) {
rb_raise(rb_eArgError, "wrong number of arguments (0 for 1).");
Expand All @@ -1262,21 +1294,18 @@ static VALUE dump(int argc, VALUE *argv, VALUE self) {
if (CompatMode == copts.mode && copts.escape_mode != ASCIIEsc) {
copts.escape_mode = JSONEsc;
}
out.buf = buf;
out.end = buf + sizeof(buf) - 10;
out.allocated = false;
out.omit_nil = copts.dump_opts.omit_nil;
out.caller = CALLER_DUMP;
oj_dump_obj_to_json_using_params(*argv, &copts, &out, argc - 1, argv + 1);
if (0 == out.buf) {
rb_raise(rb_eNoMemError, "Not enough memory.");
}
rstr = rb_str_new2(out.buf);
rstr = oj_encode(rstr);
if (out.allocated) {
xfree(out.buf);
}
return rstr;
arg.out = &out;
arg.copts = &copts;
arg.argc = argc;
arg.argv = argv;

arg.out->buf = buf;
arg.out->end = buf + sizeof(buf) - 10;
arg.out->allocated = false;
arg.out->omit_nil = copts.dump_opts.omit_nil;
arg.out->caller = CALLER_DUMP;

return rb_ensure(dump_body, (VALUE)&arg, dump_ensure, (VALUE)&arg);
}

/* Document-method: to_json
Expand Down

0 comments on commit c6ad6e9

Please sign in to comment.