Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v10.x] buffer: fix crash for invalid index types #23795

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 36 additions & 26 deletions src/node_buffer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,18 @@

#define THROW_AND_RETURN_IF_OOB(r) \
do { \
if (!(r)) return node::THROW_ERR_INDEX_OUT_OF_RANGE(env); \
} while (0)
if ((r).IsNothing()) return; \
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: If my understanding is correct, this return; would make the Copy return with undefined. Would it be better to include a test which checks that?

Copy link
Member Author

Choose a reason for hiding this comment

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

It would not return any value – this is the case where coercing to integer throws an exception (i.e. the last part of the test)

if (!(r).FromJust()) \
return node::THROW_ERR_INDEX_OUT_OF_RANGE(env); \
} while (0) \

#define SLICE_START_END(start_arg, end_arg, end_max) \
#define SLICE_START_END(env, start_arg, end_arg, end_max) \
size_t start; \
size_t end; \
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(start_arg, 0, &start)); \
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(end_arg, end_max, &end)); \
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(env, start_arg, 0, &start)); \
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(env, end_arg, end_max, &end)); \
if (end < start) end = start; \
THROW_AND_RETURN_IF_OOB(end <= end_max); \
THROW_AND_RETURN_IF_OOB(Just(end <= end_max)); \
size_t length = end - start;

namespace node {
Expand All @@ -76,9 +78,11 @@ using v8::EscapableHandleScope;
using v8::FunctionCallbackInfo;
using v8::Integer;
using v8::Isolate;
using v8::Just;
using v8::Local;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Nothing;
using v8::Object;
using v8::String;
using v8::Uint32;
Expand Down Expand Up @@ -161,29 +165,32 @@ void CallbackInfo::WeakCallback(Isolate* isolate) {
}


// Parse index for external array data.
inline MUST_USE_RESULT bool ParseArrayIndex(Local<Value> arg,
size_t def,
size_t* ret) {
// Parse index for external array data. An empty Maybe indicates
// a pending exception. `false` indicates that the index is out-of-bounds.
inline MUST_USE_RESULT Maybe<bool> ParseArrayIndex(Environment* env,
Local<Value> arg,
size_t def,
size_t* ret) {
if (arg->IsUndefined()) {
*ret = def;
return true;
return Just(true);
}

CHECK(arg->IsNumber());
int64_t tmp_i = arg.As<Integer>()->Value();
int64_t tmp_i;
if (!arg->IntegerValue(env->context()).To(&tmp_i))
return Nothing<bool>();

if (tmp_i < 0)
return false;
return Just(false);

// Check that the result fits in a size_t.
const uint64_t kSizeMax = static_cast<uint64_t>(static_cast<size_t>(-1));
// coverity[pointless_expression]
if (static_cast<uint64_t>(tmp_i) > kSizeMax)
return false;
return Just(false);

*ret = static_cast<size_t>(tmp_i);
return true;
return Just(true);
}

} // anonymous namespace
Expand Down Expand Up @@ -452,7 +459,7 @@ void StringSlice(const FunctionCallbackInfo<Value>& args) {
if (ts_obj_length == 0)
return args.GetReturnValue().SetEmptyString();

SLICE_START_END(args[0], args[1], ts_obj_length)
SLICE_START_END(env, args[0], args[1], ts_obj_length)

Local<Value> error;
MaybeLocal<Value> ret =
Expand Down Expand Up @@ -485,9 +492,10 @@ void Copy(const FunctionCallbackInfo<Value> &args) {
size_t source_start;
size_t source_end;

THROW_AND_RETURN_IF_OOB(ParseArrayIndex(args[2], 0, &target_start));
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(args[3], 0, &source_start));
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(args[4], ts_obj_length, &source_end));
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(env, args[2], 0, &target_start));
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(env, args[3], 0, &source_start));
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(env, args[4], ts_obj_length,
&source_end));

// Copy 0 bytes; we're done
if (target_start >= target_length || source_start >= source_end)
Expand Down Expand Up @@ -617,13 +625,13 @@ void StringWrite(const FunctionCallbackInfo<Value>& args) {
size_t offset;
size_t max_length;

THROW_AND_RETURN_IF_OOB(ParseArrayIndex(args[1], 0, &offset));
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(env, args[1], 0, &offset));
if (offset > ts_obj_length) {
return node::THROW_ERR_BUFFER_OUT_OF_BOUNDS(
env, "\"offset\" is outside of buffer bounds");
}

THROW_AND_RETURN_IF_OOB(ParseArrayIndex(args[2], ts_obj_length - offset,
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(env, args[2], ts_obj_length - offset,
&max_length));

max_length = MIN(ts_obj_length - offset, max_length);
Expand Down Expand Up @@ -678,10 +686,12 @@ void CompareOffset(const FunctionCallbackInfo<Value> &args) {
size_t source_end;
size_t target_end;

THROW_AND_RETURN_IF_OOB(ParseArrayIndex(args[2], 0, &target_start));
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(args[3], 0, &source_start));
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(args[4], target_length, &target_end));
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(args[5], ts_obj_length, &source_end));
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(env, args[2], 0, &target_start));
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(env, args[3], 0, &source_start));
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(env, args[4], target_length,
&target_end));
THROW_AND_RETURN_IF_OOB(ParseArrayIndex(env, args[5], ts_obj_length,
&source_end));

if (source_start > ts_obj_length)
return node::THROW_ERR_INDEX_OUT_OF_RANGE(env);
Expand Down
26 changes: 26 additions & 0 deletions test/parallel/test-buffer-copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ let cntr = 0;
}
}

{
// Current behavior is to coerce values to integers.
b.fill(++cntr);
c.fill(++cntr);
const copied = b.copy(c, '0', '0', '512');
assert.strictEqual(copied, 512);
for (let i = 0; i < c.length; i++) {
assert.strictEqual(c[i], b[i]);
}
}

{
// copy c into b, without specifying sourceEnd
b.fill(++cntr);
Expand Down Expand Up @@ -144,3 +155,18 @@ assert.strictEqual(b.copy(c, 512, 0, 10), 0);
assert.strictEqual(c[i], e[i]);
}
}

// https://github.com/nodejs/node/issues/23668: Do not crash for invalid input.
c.fill('c');
b.copy(c, 'not a valid offset');
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: An assertion after an invalid copy which ensures that nothing actually copied would be good. If the behaviour changes later on, this assertion would be able to catch that.

Copy link
Member Author

@addaleax addaleax Oct 24, 2018

Choose a reason for hiding this comment

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

@thefourtheye I’ve documented the behaviour – it is not not copying anything, it is coercing, values to integer, i.e. 'not a valid offset' becomes 0 (as in, ('foo' | 0) === 0)

// Make sure this acted like a regular copy with `0` offset.
assert.deepStrictEqual(c, b.slice(0, c.length));

{
c.fill('C');
assert.throws(() => {
b.copy(c, { [Symbol.toPrimitive]() { throw new Error('foo'); } });
}, /foo/);
// No copying took place:
assert.deepStrictEqual(c.toString(), 'C'.repeat(c.length));
}