Skip to content

Commit

Permalink
Fix memory leak in Oj.dump if raise an exception
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 Jul 31, 2021
1 parent cfc14d1 commit 8b6bc45
Showing 1 changed file with 43 additions and 12 deletions.
55 changes: 43 additions & 12 deletions ext/oj/oj.c
Expand Up @@ -1237,6 +1237,15 @@ 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);
static VALUE dump_ensure(VALUE a);

/* Document-method: dump
* call-seq: dump(obj, options={})
*
Expand All @@ -1246,9 +1255,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,23 +1271,45 @@ 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) {
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);
}

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(out.buf);
rstr = rb_str_new2(arg->out->buf);
rstr = oj_encode(rstr);
if (out.allocated) {
xfree(out.buf);
}

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: to_json
* call-seq: to_json(obj, options)
*
Expand Down

0 comments on commit 8b6bc45

Please sign in to comment.