Skip to content

Commit

Permalink
src: fix memory leak in ExternString
Browse files Browse the repository at this point in the history
v8 will silently return an empty handle
which doesn't delete our data if string length is
above String::kMaxLength

Fixes: #1374
PR-URL: #2402
Reviewed-By: trevnorris - Trevor Norris <trev.norris@gmail.com>
Reviewed-By: indutny - Fedor Indutny <fedor.indutny@gmail.com>
Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
  • Loading branch information
skomski authored and trevnorris committed Sep 2, 2015
1 parent 68a658d commit 617ee32
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 14 deletions.
12 changes: 8 additions & 4 deletions lib/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,10 +350,14 @@ function slowToString(encoding, start, end) {


Buffer.prototype.toString = function() {
const length = this.length | 0;
if (arguments.length === 0)
return this.utf8Slice(0, length);
return slowToString.apply(this, arguments);
if (arguments.length === 0) {
var result = this.utf8Slice(0, this.length);
} else {
var result = slowToString.apply(this, arguments);
}
if (result === undefined)
throw new Error('toString failed');
return result;
};


Expand Down
4 changes: 4 additions & 0 deletions src/node_buffer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,10 @@ void Initialize(Handle<Object> target,
target->Set(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "kMaxLength"),
Integer::NewFromUnsigned(env->isolate(), kMaxLength)).FromJust();

target->Set(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "kStringMaxLength"),
Integer::New(env->isolate(), String::kMaxLength)).FromJust();
}


Expand Down
39 changes: 29 additions & 10 deletions src/string_bytes.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
using v8::MaybeLocal;


template <typename ResourceType, typename TypeName>
class ExternString: public ResourceType {
public:
~ExternString() override {
delete[] data_;
free(const_cast<TypeName*>(data_));
isolate()->AdjustAmountOfExternalAllocatedMemory(-byte_length());
}

Expand All @@ -52,7 +53,11 @@ class ExternString: public ResourceType {
if (length == 0)
return scope.Escape(String::Empty(isolate));

TypeName* new_data = new TypeName[length];
TypeName* new_data =
static_cast<TypeName*>(malloc(length * sizeof(*new_data)));
if (new_data == nullptr) {
return Local<String>();
}
memcpy(new_data, data, length * sizeof(*new_data));

return scope.Escape(ExternString<ResourceType, TypeName>::New(isolate,
Expand All @@ -72,10 +77,15 @@ class ExternString: public ResourceType {
ExternString* h_str = new ExternString<ResourceType, TypeName>(isolate,
data,
length);
Local<String> str = String::NewExternal(isolate, h_str);
MaybeLocal<String> str = String::NewExternal(isolate, h_str);
isolate->AdjustAmountOfExternalAllocatedMemory(h_str->byte_length());

return scope.Escape(str);
if (str.IsEmpty()) {
delete h_str;
return Local<String>();
}

return scope.Escape(str.ToLocalChecked());
}

inline Isolate* isolate() const { return isolate_; }
Expand Down Expand Up @@ -765,11 +775,14 @@ Local<Value> StringBytes::Encode(Isolate* isolate,

case ASCII:
if (contains_non_ascii(buf, buflen)) {
char* out = new char[buflen];
char* out = static_cast<char*>(malloc(buflen));
if (out == nullptr) {
return Local<String>();
}
force_ascii(buf, out, buflen);
if (buflen < EXTERN_APEX) {
val = OneByteString(isolate, out, buflen);
delete[] out;
free(out);
} else {
val = ExternOneByteString::New(isolate, out, buflen);
}
Expand Down Expand Up @@ -797,14 +810,17 @@ Local<Value> StringBytes::Encode(Isolate* isolate,

case BASE64: {
size_t dlen = base64_encoded_size(buflen);
char* dst = new char[dlen];
char* dst = static_cast<char*>(malloc(dlen));
if (dst == nullptr) {
return Local<String>();
}

size_t written = base64_encode(buf, buflen, dst, dlen);
CHECK_EQ(written, dlen);

if (dlen < EXTERN_APEX) {
val = OneByteString(isolate, dst, dlen);
delete[] dst;
free(dst);
} else {
val = ExternOneByteString::New(isolate, dst, dlen);
}
Expand All @@ -813,13 +829,16 @@ Local<Value> StringBytes::Encode(Isolate* isolate,

case HEX: {
size_t dlen = buflen * 2;
char* dst = new char[dlen];
char* dst = static_cast<char*>(malloc(dlen));
if (dst == nullptr) {
return Local<String>();
}
size_t written = hex_encode(buf, buflen, dst, dlen);
CHECK_EQ(written, dlen);

if (dlen < EXTERN_APEX) {
val = OneByteString(isolate, dst, dlen);
delete[] dst;
free(dst);
} else {
val = ExternOneByteString::New(isolate, dst, dlen);
}
Expand Down
66 changes: 66 additions & 0 deletions test/parallel/test-stringbytes-external.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,69 @@ var PRE_3OF4_APEX = Math.ceil((EXTERN_APEX / 4) * 3) - RADIOS;
assert.equal(a, b);
assert.equal(b, c);
})();

// v8 fails silently if string length > v8::String::kMaxLength
(function() {
// v8::String::kMaxLength defined in v8.h
const kStringMaxLength = process.binding('buffer').kStringMaxLength;

assert.throws(function() {
new Buffer(kStringMaxLength + 1).toString();
}, /toString failed|Buffer allocation failed/);

try {
new Buffer(kStringMaxLength * 4);
} catch(e) {
assert.equal(e.message, 'Buffer allocation failed - process out of memory');
console.log(
'1..0 # Skipped: intensive toString tests due to memory confinements');
return;
}

assert.throws(function() {
new Buffer(kStringMaxLength + 1).toString('ascii');
}, /toString failed/);

assert.throws(function() {
new Buffer(kStringMaxLength + 1).toString('utf8');
}, /toString failed/);

assert.throws(function() {
new Buffer(kStringMaxLength * 2 + 2).toString('utf16le');
}, /toString failed/);

assert.throws(function() {
new Buffer(kStringMaxLength + 1).toString('binary');
}, /toString failed/);

assert.throws(function() {
new Buffer(kStringMaxLength + 1).toString('base64');
}, /toString failed/);

assert.throws(function() {
new Buffer(kStringMaxLength + 1).toString('hex');
}, /toString failed/);

var maxString = new Buffer(kStringMaxLength).toString();
assert.equal(maxString.length, kStringMaxLength);
// Free the memory early instead of at the end of the next assignment
maxString = undefined;

maxString = new Buffer(kStringMaxLength).toString('binary');
assert.equal(maxString.length, kStringMaxLength);
maxString = undefined;

maxString =
new Buffer(kStringMaxLength + 1).toString('binary', 1);
assert.equal(maxString.length, kStringMaxLength);
maxString = undefined;

maxString =
new Buffer(kStringMaxLength + 1).toString('binary', 0, kStringMaxLength);
assert.equal(maxString.length, kStringMaxLength);
maxString = undefined;

maxString = new Buffer(kStringMaxLength + 2).toString('utf16le');
assert.equal(maxString.length, (kStringMaxLength + 2) / 2);
maxString = undefined;
})();

6 comments on commit 617ee32

@thefourtheye
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a chance that the tests starting from line 129 to the end may never be executed in small memory environments, right? I don't think we should ignore tests like this. Please correct me if I am missing something.

@trevnorris
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thefourtheye You are correct, but this was decided as acceptable. We're creating many large Buffers, then turning them into large strings. The largest supported string is 256MB in size. Take a buffer that size and turn it into a string and you're already in swap on a raspberry pi1. Even attempting to run GC between each test had failures.

@thefourtheye
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I think understand now. Thanks for explaining :-)

@Fishrock123
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

authored on Aug 16, 1970

@skomski Uhhhh, what is up with this?

screen shot 2015-09-06 at 6 51 25 am

@Fishrock123
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @nodejs/tsc

@rvagg
Copy link
Member

@rvagg rvagg commented on 617ee32 Sep 6, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Fishrock123 see #2713 (comment) for proposal you can +1

Please sign in to comment.