Skip to content

Commit 1d433f7

Browse files
GenevieveGenevieve L'EsperanceMaxim Shirshindylanahsmith
authored
Use length to ensure null chars do not cause early termination of C string copies/reads (#272)
* Intro test case for null terminator string * unexpected result: expected ss, got sx00s * Fix the assertion, add Unicode symbols * Pass go string length into v8::String::New to ensure it does not cut off at null chars * Reuse the existing RtnString type * Formatting * Update value_test.go Co-authored-by: Dylan Thacker-Smith <[email protected]> * Table tests for NewValue * Use std::string constructor in CopyString(Utf8Value) to keep whole string * Update changelog Co-authored-by: Genevieve L'Esperance <[email protected]> Co-authored-by: Maxim Shirshin <[email protected]> Co-authored-by: Dylan Thacker-Smith <[email protected]>
1 parent cb8779b commit 1d433f7

File tree

5 files changed

+73
-18
lines changed

5 files changed

+73
-18
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
### Fixed
10+
- Use string length to ensure null character-containing strings in Go/JS are not terminated early.
11+
912
## [v0.7.0] - 2021-12-09
1013

1114
### Added

v8go.cc

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const char* CopyString(String::Utf8Value& value) {
5858
if (value.length() == 0) {
5959
return nullptr;
6060
}
61-
return CopyString(*value);
61+
return CopyString(std::string(*value, value.length()));
6262
}
6363

6464
static RtnError ExceptionError(TryCatch& try_catch,
@@ -808,12 +808,13 @@ ValuePtr NewValueIntegerFromUnsigned(IsolatePtr iso, uint32_t v) {
808808
return tracked_value(ctx, val);
809809
}
810810

811-
RtnValue NewValueString(IsolatePtr iso, const char* v) {
811+
RtnValue NewValueString(IsolatePtr iso, const char* v, int v_length) {
812812
ISOLATE_SCOPE_INTERNAL_CONTEXT(iso);
813813
TryCatch try_catch(iso);
814814
RtnValue rtn = {};
815815
Local<String> str;
816-
if (!String::NewFromUtf8(iso, v).ToLocal(&str)) {
816+
if (!String::NewFromUtf8(iso, v, NewStringType::kNormal, v_length)
817+
.ToLocal(&str)) {
817818
rtn.error = ExceptionError(try_catch, iso, ctx->ptr.Get(iso));
818819
return rtn;
819820
}
@@ -948,18 +949,24 @@ RtnString ValueToDetailString(ValuePtr ptr) {
948949
return rtn;
949950
}
950951
String::Utf8Value ds(iso, str);
951-
rtn.string = CopyString(ds);
952+
rtn.data = CopyString(ds);
953+
rtn.length = ds.length();
952954
return rtn;
953955
}
954956

955-
const char* ValueToString(ValuePtr ptr) {
957+
RtnString ValueToString(ValuePtr ptr) {
956958
LOCAL_VALUE(ptr);
959+
RtnString rtn = {0};
957960
// String::Utf8Value will result in an empty string if conversion to a string
958961
// fails
959962
// TODO: Consider propagating the JS error. A fallback value could be returned
960963
// in Value.String()
961-
String::Utf8Value utf8(iso, value);
962-
return CopyString(utf8);
964+
String::Utf8Value src(iso, value);
965+
char* data = static_cast<char*>(malloc(src.length()));
966+
memcpy(data, *src, src.length());
967+
rtn.data = data;
968+
rtn.length = src.length();
969+
return rtn;
963970
}
964971

965972
uint32_t ValueToUint32(ValuePtr ptr) {

v8go.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ typedef struct {
106106
} RtnValue;
107107

108108
typedef struct {
109-
const char* string;
109+
const char* data;
110+
int length;
110111
RtnError error;
111112
} RtnString;
112113

@@ -193,7 +194,7 @@ extern ValuePtr NewValueNull(IsolatePtr iso_ptr);
193194
extern ValuePtr NewValueUndefined(IsolatePtr iso_ptr);
194195
extern ValuePtr NewValueInteger(IsolatePtr iso_ptr, int32_t v);
195196
extern ValuePtr NewValueIntegerFromUnsigned(IsolatePtr iso_ptr, uint32_t v);
196-
extern RtnValue NewValueString(IsolatePtr iso_ptr, const char* v);
197+
extern RtnValue NewValueString(IsolatePtr iso_ptr, const char* v, int v_length);
197198
extern ValuePtr NewValueBoolean(IsolatePtr iso_ptr, int v);
198199
extern ValuePtr NewValueNumber(IsolatePtr iso_ptr, double v);
199200
extern ValuePtr NewValueBigInt(IsolatePtr iso_ptr, int64_t v);
@@ -202,7 +203,7 @@ extern RtnValue NewValueBigIntFromWords(IsolatePtr iso_ptr,
202203
int sign_bit,
203204
int word_count,
204205
const uint64_t* words);
205-
const char* ValueToString(ValuePtr ptr);
206+
extern RtnString ValueToString(ValuePtr ptr);
206207
const uint32_t* ValueToArrayIndex(ValuePtr ptr);
207208
int ValueToBoolean(ValuePtr ptr);
208209
int32_t ValueToInt32(ValuePtr ptr);

value.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ func Null(iso *Isolate) *Value {
5757
// string -> V8::String
5858
// int32 -> V8::Integer
5959
// uint32 -> V8::Integer
60-
// bool -> V8::Boolean
6160
// int64 -> V8::BigInt
6261
// uint64 -> V8::BigInt
6362
// bool -> V8::Boolean
@@ -73,7 +72,7 @@ func NewValue(iso *Isolate, val interface{}) (*Value, error) {
7372
case string:
7473
cstr := C.CString(v)
7574
defer C.free(unsafe.Pointer(cstr))
76-
rtn := C.NewValueString(iso.ptr, cstr)
75+
rtn := C.NewValueString(iso.ptr, cstr, C.int(len(v)))
7776
return valueResult(nil, rtn)
7877
case int32:
7978
rtnVal = &Value{
@@ -199,13 +198,12 @@ func (v *Value) Boolean() bool {
199198
// DetailString provide a string representation of this value usable for debugging.
200199
func (v *Value) DetailString() string {
201200
rtn := C.ValueToDetailString(v.ptr)
202-
if rtn.string == nil {
201+
if rtn.data == nil {
203202
err := newJSError(rtn.error)
204203
panic(err) // TODO: Return a fallback value
205204
}
206-
s := rtn.string
207-
defer C.free(unsafe.Pointer(s))
208-
return C.GoString(s)
205+
defer C.free(unsafe.Pointer(rtn.data))
206+
return C.GoStringN(rtn.data, rtn.length)
209207
}
210208

211209
// Int32 perform the equivalent of `Number(value)` in JS and convert the result to a
@@ -242,8 +240,8 @@ func (v *Value) Object() *Object {
242240
// print their definition.
243241
func (v *Value) String() string {
244242
s := C.ValueToString(v.ptr)
245-
defer C.free(unsafe.Pointer(s))
246-
return C.GoString(s)
243+
defer C.free(unsafe.Pointer(s.data))
244+
return C.GoStringN(s.data, C.int(s.length))
247245
}
248246

249247
// Uint32 perform the equivalent of `Number(value)` in JS and convert the result to an

value_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ func TestValueString(t *testing.T) {
8181
}{
8282
{"Number", `13 * 2`, "26"},
8383
{"String", `"string"`, "string"},
84+
{"String with null character and non-latin unicode", `"a\x00Ω"`, "a\x00Ω"},
8485
{"Object", `let obj = {}; obj`, "[object Object]"},
8586
{"Function", `let fn = function(){}; fn`, "function(){}"},
8687
}
@@ -97,6 +98,51 @@ func TestValueString(t *testing.T) {
9798
}
9899
}
99100

101+
func TestNewValue(t *testing.T) {
102+
t.Parallel()
103+
ctx := v8.NewContext(nil)
104+
iso := ctx.Isolate()
105+
defer iso.Dispose()
106+
defer ctx.Close()
107+
108+
tests := []struct {
109+
name string
110+
input interface{}
111+
predicate string
112+
}{
113+
{"string", "s\x00s\x00", `str => str === "s\x00s\x00"`},
114+
{"int32", int32(36), `int => int === 36`},
115+
{"bool", true, `b => b === true`},
116+
}
117+
118+
for _, tt := range tests {
119+
tt := tt
120+
t.Run(tt.name, func(t *testing.T) {
121+
val, err := ctx.RunScript(tt.predicate, "test.js")
122+
if err != nil {
123+
t.Fatal(err)
124+
}
125+
fn, err := val.AsFunction()
126+
if err != nil {
127+
t.Fatal(err)
128+
}
129+
130+
jsVal, err := v8.NewValue(iso, tt.input)
131+
if err != nil {
132+
t.Fatal(err)
133+
}
134+
135+
result, err := fn.Call(ctx.Global(), jsVal)
136+
if err != nil {
137+
t.Fatal(err)
138+
}
139+
if !result.Boolean() {
140+
t.Fatal("unexpected result: expected true, got false")
141+
}
142+
})
143+
}
144+
}
145+
100146
func TestValueDetailString(t *testing.T) {
101147
t.Parallel()
102148
ctx := v8.NewContext(nil)

0 commit comments

Comments
 (0)