Browse Source

src: fix memory leak in ExternString

v8 will silently return an empty handle
which doesn't delete our data if string length is
above String::kMaxLength

Fixes: https://github.com/nodejs/node/issues/1374
PR-URL: https://github.com/nodejs/node/pull/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>

Amended by @rvagg to change author date from
  "1970-08-16 16:09:02 +0200"
to
  "2015-08-16 16:09:02 +0200"
as per discussion @ https://github.com/nodejs/node/issues/2713
v4.x
Karl Skomski 10 years ago
committed by Rod Vagg
parent
commit
5201cb0ff1
  1. 12
      lib/buffer.js
  2. 4
      src/node_buffer.cc
  3. 39
      src/string_bytes.cc
  4. 66
      test/parallel/test-stringbytes-external.js

12
lib/buffer.js

@ -350,10 +350,14 @@ function slowToString(encoding, start, end) {
Buffer.prototype.toString = function() { Buffer.prototype.toString = function() {
const length = this.length | 0; if (arguments.length === 0) {
if (arguments.length === 0) var result = this.utf8Slice(0, this.length);
return this.utf8Slice(0, length); } else {
return slowToString.apply(this, arguments); var result = slowToString.apply(this, arguments);
}
if (result === undefined)
throw new Error('toString failed');
return result;
}; };

4
src/node_buffer.cc

@ -1024,6 +1024,10 @@ void Initialize(Handle<Object> target,
target->Set(env->context(), target->Set(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "kMaxLength"), FIXED_ONE_BYTE_STRING(env->isolate(), "kMaxLength"),
Integer::NewFromUnsigned(env->isolate(), kMaxLength)).FromJust(); Integer::NewFromUnsigned(env->isolate(), kMaxLength)).FromJust();
target->Set(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "kStringMaxLength"),
Integer::New(env->isolate(), String::kMaxLength)).FromJust();
} }

39
src/string_bytes.cc

@ -22,13 +22,14 @@ using v8::Local;
using v8::Object; using v8::Object;
using v8::String; using v8::String;
using v8::Value; using v8::Value;
using v8::MaybeLocal;
template <typename ResourceType, typename TypeName> template <typename ResourceType, typename TypeName>
class ExternString: public ResourceType { class ExternString: public ResourceType {
public: public:
~ExternString() override { ~ExternString() override {
delete[] data_; free(const_cast<TypeName*>(data_));
isolate()->AdjustAmountOfExternalAllocatedMemory(-byte_length()); isolate()->AdjustAmountOfExternalAllocatedMemory(-byte_length());
} }
@ -52,7 +53,11 @@ class ExternString: public ResourceType {
if (length == 0) if (length == 0)
return scope.Escape(String::Empty(isolate)); 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)); memcpy(new_data, data, length * sizeof(*new_data));
return scope.Escape(ExternString<ResourceType, TypeName>::New(isolate, return scope.Escape(ExternString<ResourceType, TypeName>::New(isolate,
@ -72,10 +77,15 @@ class ExternString: public ResourceType {
ExternString* h_str = new ExternString<ResourceType, TypeName>(isolate, ExternString* h_str = new ExternString<ResourceType, TypeName>(isolate,
data, data,
length); length);
Local<String> str = String::NewExternal(isolate, h_str); MaybeLocal<String> str = String::NewExternal(isolate, h_str);
isolate->AdjustAmountOfExternalAllocatedMemory(h_str->byte_length()); 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_; } inline Isolate* isolate() const { return isolate_; }
@ -765,11 +775,14 @@ Local<Value> StringBytes::Encode(Isolate* isolate,
case ASCII: case ASCII:
if (contains_non_ascii(buf, buflen)) { 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); force_ascii(buf, out, buflen);
if (buflen < EXTERN_APEX) { if (buflen < EXTERN_APEX) {
val = OneByteString(isolate, out, buflen); val = OneByteString(isolate, out, buflen);
delete[] out; free(out);
} else { } else {
val = ExternOneByteString::New(isolate, out, buflen); val = ExternOneByteString::New(isolate, out, buflen);
} }
@ -797,14 +810,17 @@ Local<Value> StringBytes::Encode(Isolate* isolate,
case BASE64: { case BASE64: {
size_t dlen = base64_encoded_size(buflen); 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); size_t written = base64_encode(buf, buflen, dst, dlen);
CHECK_EQ(written, dlen); CHECK_EQ(written, dlen);
if (dlen < EXTERN_APEX) { if (dlen < EXTERN_APEX) {
val = OneByteString(isolate, dst, dlen); val = OneByteString(isolate, dst, dlen);
delete[] dst; free(dst);
} else { } else {
val = ExternOneByteString::New(isolate, dst, dlen); val = ExternOneByteString::New(isolate, dst, dlen);
} }
@ -813,13 +829,16 @@ Local<Value> StringBytes::Encode(Isolate* isolate,
case HEX: { case HEX: {
size_t dlen = buflen * 2; 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); size_t written = hex_encode(buf, buflen, dst, dlen);
CHECK_EQ(written, dlen); CHECK_EQ(written, dlen);
if (dlen < EXTERN_APEX) { if (dlen < EXTERN_APEX) {
val = OneByteString(isolate, dst, dlen); val = OneByteString(isolate, dst, dlen);
delete[] dst; free(dst);
} else { } else {
val = ExternOneByteString::New(isolate, dst, dlen); val = ExternOneByteString::New(isolate, dst, dlen);
} }

66
test/parallel/test-stringbytes-external.js

@ -107,3 +107,69 @@ var PRE_3OF4_APEX = Math.ceil((EXTERN_APEX / 4) * 3) - RADIOS;
assert.equal(a, b); assert.equal(a, b);
assert.equal(b, c); 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;
})();

Loading…
Cancel
Save