| // Copyright 2018 the V8 project authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/api/api-inl.h" |
| #include "src/execution/frames-inl.h" |
| #include "src/flags/flags.h" |
| #include "src/heap/read-only-spaces.h" |
| #include "src/heap/spaces.h" |
| #include "test/cctest/cctest.h" |
| #include "tools/debug_helper/debug-helper.h" |
| |
| namespace v8 { |
| namespace internal { |
| |
| namespace { |
| |
| namespace d = v8::debug_helper; |
| |
| uintptr_t memory_fail_start = 0; |
| uintptr_t memory_fail_end = 0; |
| |
| class MemoryFailureRegion { |
| public: |
| MemoryFailureRegion(uintptr_t start, uintptr_t end) { |
| memory_fail_start = start; |
| memory_fail_end = end; |
| } |
| ~MemoryFailureRegion() { |
| memory_fail_start = 0; |
| memory_fail_end = 0; |
| } |
| }; |
| |
| // Implement the memory-reading callback. This one just fetches memory from the |
| // current process, but a real implementation for a debugging extension would |
| // fetch memory from the debuggee process or crash dump. |
| d::MemoryAccessResult ReadMemory(uintptr_t address, void* destination, |
| size_t byte_count) { |
| if (address >= memory_fail_start && address <= memory_fail_end) { |
| // Simulate failure to read debuggee memory. |
| return d::MemoryAccessResult::kAddressValidButInaccessible; |
| } |
| memcpy(destination, reinterpret_cast<void*>(address), byte_count); |
| return d::MemoryAccessResult::kOk; |
| } |
| |
| void CheckPropBase(const d::PropertyBase& property, const char* expected_type, |
| const char* expected_name) { |
| CHECK(property.type == std::string("v8::internal::TaggedValue") || |
| property.type == std::string(expected_type)); |
| CHECK(property.decompressed_type == std::string(expected_type)); |
| CHECK(property.name == std::string(expected_name)); |
| } |
| |
| void CheckProp(const d::ObjectProperty& property, const char* expected_type, |
| const char* expected_name, |
| d::PropertyKind expected_kind = d::PropertyKind::kSingle, |
| size_t expected_num_values = 1) { |
| CheckPropBase(property, expected_type, expected_name); |
| CHECK_EQ(property.num_values, expected_num_values); |
| CHECK(property.kind == expected_kind); |
| } |
| |
| template <typename TValue> |
| void CheckProp(const d::ObjectProperty& property, const char* expected_type, |
| const char* expected_name, TValue expected_value) { |
| CheckProp(property, expected_type, expected_name); |
| CHECK(*reinterpret_cast<TValue*>(property.address) == expected_value); |
| } |
| |
| bool StartsWith(const std::string& full_string, const std::string& prefix) { |
| return full_string.substr(0, prefix.size()) == prefix; |
| } |
| |
| bool Contains(const std::string& full_string, const std::string& substr) { |
| return full_string.find(substr) != std::string::npos; |
| } |
| |
| void CheckStructProp(const d::StructProperty& property, |
| const char* expected_type, const char* expected_name, |
| size_t expected_offset, uint8_t expected_num_bits = 0, |
| uint8_t expected_shift_bits = 0) { |
| CheckPropBase(property, expected_type, expected_name); |
| CHECK_EQ(property.offset, expected_offset); |
| CHECK_EQ(property.num_bits, expected_num_bits); |
| CHECK_EQ(property.shift_bits, expected_shift_bits); |
| } |
| |
| const d::ObjectProperty& FindProp(const d::ObjectPropertiesResult& props, |
| std::string name) { |
| for (size_t i = 0; i < props.num_properties; ++i) { |
| if (name == props.properties[i]->name) { |
| return *props.properties[i]; |
| } |
| } |
| CHECK_WITH_MSG(false, ("property '" + name + "' not found").c_str()); |
| UNREACHABLE(); |
| } |
| |
| template <typename TValue> |
| TValue ReadProp(const d::ObjectPropertiesResult& props, std::string name) { |
| const d::ObjectProperty& prop = FindProp(props, name); |
| return *reinterpret_cast<TValue*>(prop.address); |
| } |
| |
| // A simple implementation of ExternalStringResource that lets us control the |
| // result of IsCacheable(). |
| class StringResource : public v8::String::ExternalStringResource { |
| public: |
| explicit StringResource(bool cacheable) : cacheable_(cacheable) {} |
| const uint16_t* data() const override { |
| return reinterpret_cast<const uint16_t*>(u"abcde"); |
| } |
| size_t length() const override { return 5; } |
| bool IsCacheable() const override { return cacheable_; } |
| |
| private: |
| bool cacheable_; |
| }; |
| |
| } // namespace |
| |
| TEST(GetObjectProperties) { |
| CcTest::InitializeVM(); |
| v8::Isolate* isolate = CcTest::isolate(); |
| i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate); |
| v8::HandleScope scope(isolate); |
| LocalContext context; |
| // Claim we don't know anything about the heap layout. |
| d::HeapAddresses heap_addresses{0, 0, 0, 0}; |
| |
| v8::Local<v8::Value> v = CompileRun("42"); |
| Handle<Object> o = v8::Utils::OpenHandle(*v); |
| d::ObjectPropertiesResultPtr props = |
| d::GetObjectProperties(o->ptr(), &ReadMemory, heap_addresses); |
| CHECK(props->type_check_result == d::TypeCheckResult::kSmi); |
| CHECK(props->brief == std::string("42 (0x2a)")); |
| CHECK(props->type == std::string("v8::internal::Smi")); |
| CHECK_EQ(props->num_properties, 0); |
| |
| v = CompileRun("[\"a\", \"bc\"]"); |
| o = v8::Utils::OpenHandle(*v); |
| props = d::GetObjectProperties(o->ptr(), &ReadMemory, heap_addresses); |
| CHECK(props->type_check_result == d::TypeCheckResult::kUsedMap); |
| CHECK(props->type == std::string("v8::internal::JSArray")); |
| CHECK_EQ(props->num_properties, 4); |
| CheckProp(*props->properties[0], "v8::internal::Map", "map"); |
| CheckProp(*props->properties[1], "v8::internal::Object", |
| "properties_or_hash"); |
| CheckProp(*props->properties[2], "v8::internal::FixedArrayBase", "elements"); |
| CheckProp(*props->properties[3], "v8::internal::Object", "length", |
| static_cast<i::Tagged_t>(IntToSmi(2))); |
| |
| // We need to supply some valid address for decompression before reading the |
| // elements from the JSArray. |
| heap_addresses.any_heap_pointer = o->ptr(); |
| |
| i::Tagged_t properties_or_hash = |
| *reinterpret_cast<i::Tagged_t*>(props->properties[1]->address); |
| i::Tagged_t elements = |
| *reinterpret_cast<i::Tagged_t*>(props->properties[2]->address); |
| |
| // The properties_or_hash_code field should be an empty fixed array. Since |
| // that is at a known offset, we should be able to detect it even without |
| // any ability to read memory. |
| { |
| MemoryFailureRegion failure(0, UINTPTR_MAX); |
| props = |
| d::GetObjectProperties(properties_or_hash, &ReadMemory, heap_addresses); |
| CHECK(props->type_check_result == |
| d::TypeCheckResult::kObjectPointerValidButInaccessible); |
| CHECK(props->type == std::string("v8::internal::HeapObject")); |
| CHECK_EQ(props->num_properties, 1); |
| CheckProp(*props->properties[0], "v8::internal::Map", "map"); |
| // "maybe" prefix indicates that GetObjectProperties recognized the offset |
| // within the page as matching a known object, but didn't know whether the |
| // object is on the right page. This response can only happen in builds |
| // without pointer compression, because otherwise heap addresses would be at |
| // deterministic locations within the heap reservation. |
| CHECK(COMPRESS_POINTERS_BOOL |
| ? StartsWith(props->brief, "EmptyFixedArray") |
| : Contains(props->brief, "maybe EmptyFixedArray")); |
| |
| // Provide a heap first page so the API can be more sure. |
| heap_addresses.read_only_space_first_page = |
| i_isolate->heap()->read_only_space()->FirstPageAddress(); |
| props = |
| d::GetObjectProperties(properties_or_hash, &ReadMemory, heap_addresses); |
| CHECK(props->type_check_result == |
| d::TypeCheckResult::kObjectPointerValidButInaccessible); |
| CHECK(props->type == std::string("v8::internal::HeapObject")); |
| CHECK_EQ(props->num_properties, 1); |
| CheckProp(*props->properties[0], "v8::internal::Map", "map"); |
| CHECK(StartsWith(props->brief, "EmptyFixedArray")); |
| } |
| |
| props = d::GetObjectProperties(elements, &ReadMemory, heap_addresses); |
| CHECK(props->type_check_result == d::TypeCheckResult::kUsedMap); |
| CHECK(props->type == std::string("v8::internal::FixedArray")); |
| CHECK_EQ(props->num_properties, 3); |
| CheckProp(*props->properties[0], "v8::internal::Map", "map"); |
| CheckProp(*props->properties[1], "v8::internal::Object", "length", |
| static_cast<i::Tagged_t>(IntToSmi(2))); |
| CheckProp(*props->properties[2], "v8::internal::Object", "objects", |
| d::PropertyKind::kArrayOfKnownSize, 2); |
| |
| // Get the second string value from the FixedArray. |
| i::Tagged_t second_string_address = |
| reinterpret_cast<i::Tagged_t*>(props->properties[2]->address)[1]; |
| props = d::GetObjectProperties(second_string_address, &ReadMemory, |
| heap_addresses); |
| CHECK(props->type_check_result == d::TypeCheckResult::kUsedMap); |
| CHECK(props->type == std::string("v8::internal::SeqOneByteString")); |
| CHECK_EQ(props->num_properties, 4); |
| CheckProp(*props->properties[0], "v8::internal::Map", "map"); |
| CheckProp(*props->properties[1], "uint32_t", "hash_field"); |
| CheckProp(*props->properties[2], "int32_t", "length", 2); |
| CheckProp(*props->properties[3], "char", "chars", |
| d::PropertyKind::kArrayOfKnownSize, 2); |
| CHECK_EQ( |
| strncmp("bc", |
| reinterpret_cast<const char*>(props->properties[3]->address), 2), |
| 0); |
| |
| // Read the second string again, using a type hint instead of the map. All of |
| // its properties should match what we read last time. |
| d::ObjectPropertiesResultPtr props2; |
| { |
| heap_addresses.read_only_space_first_page = 0; |
| uintptr_t map_address = |
| d::GetObjectProperties( |
| *reinterpret_cast<i::Tagged_t*>(props->properties[0]->address), |
| &ReadMemory, heap_addresses) |
| ->properties[0] |
| ->address; |
| MemoryFailureRegion failure(map_address, map_address + i::Map::kSize); |
| props2 = d::GetObjectProperties(second_string_address, &ReadMemory, |
| heap_addresses, "v8::internal::String"); |
| if (COMPRESS_POINTERS_BOOL) { |
| // The first page of each heap space can be automatically detected when |
| // pointer compression is active, so we expect to use known maps instead |
| // of the type hint. |
| CHECK_EQ(props2->type_check_result, d::TypeCheckResult::kKnownMapPointer); |
| CHECK(props2->type == std::string("v8::internal::SeqOneByteString")); |
| CHECK_EQ(props2->num_properties, 4); |
| CheckProp(*props2->properties[3], "char", "chars", |
| d::PropertyKind::kArrayOfKnownSize, 2); |
| CHECK_EQ(props2->num_guessed_types, 0); |
| } else { |
| CHECK_EQ(props2->type_check_result, d::TypeCheckResult::kUsedTypeHint); |
| CHECK(props2->type == std::string("v8::internal::String")); |
| CHECK_EQ(props2->num_properties, 3); |
| |
| // The type hint we provided was the abstract class String, but |
| // GetObjectProperties should have recognized that the Map pointer looked |
| // like the right value for a SeqOneByteString. |
| CHECK_EQ(props2->num_guessed_types, 1); |
| CHECK(std::string(props2->guessed_types[0]) == |
| std::string("v8::internal::SeqOneByteString")); |
| } |
| CheckProp(*props2->properties[0], "v8::internal::Map", "map", |
| *reinterpret_cast<i::Tagged_t*>(props->properties[0]->address)); |
| CheckProp(*props2->properties[1], "uint32_t", "hash_field", |
| *reinterpret_cast<int32_t*>(props->properties[1]->address)); |
| CheckProp(*props2->properties[2], "int32_t", "length", 2); |
| } |
| |
| // Try a weak reference. |
| props2 = d::GetObjectProperties(second_string_address | kWeakHeapObjectMask, |
| &ReadMemory, heap_addresses); |
| std::string weak_ref_prefix = "weak ref to "; |
| CHECK(weak_ref_prefix + props->brief == props2->brief); |
| CHECK(props2->type_check_result == d::TypeCheckResult::kUsedMap); |
| CHECK(props2->type == std::string("v8::internal::SeqOneByteString")); |
| CHECK_EQ(props2->num_properties, 4); |
| CheckProp(*props2->properties[0], "v8::internal::Map", "map", |
| *reinterpret_cast<i::Tagged_t*>(props->properties[0]->address)); |
| CheckProp(*props2->properties[1], "uint32_t", "hash_field", |
| *reinterpret_cast<i::Tagged_t*>(props->properties[1]->address)); |
| CheckProp(*props2->properties[2], "int32_t", "length", 2); |
| |
| // Build a complicated string (multi-level cons with slices inside) to test |
| // string printing. |
| v = CompileRun(R"( |
| const alphabet = "abcdefghijklmnopqrstuvwxyz"; |
| alphabet.substr(3,20) + alphabet.toUpperCase().substr(5,15) + "7")"); |
| o = v8::Utils::OpenHandle(*v); |
| props = d::GetObjectProperties(o->ptr(), &ReadMemory, heap_addresses); |
| CHECK(Contains(props->brief, "\"defghijklmnopqrstuvwFGHIJKLMNOPQRST7\"")); |
| |
| // Cause a failure when reading the "second" pointer within the top-level |
| // ConsString. |
| { |
| CheckProp(*props->properties[4], "v8::internal::String", "second"); |
| uintptr_t second_address = props->properties[4]->address; |
| MemoryFailureRegion failure(second_address, second_address + 4); |
| props = d::GetObjectProperties(o->ptr(), &ReadMemory, heap_addresses); |
| CHECK(Contains(props->brief, "\"defghijklmnopqrstuvwFGHIJKLMNOPQRST...\"")); |
| } |
| |
| // Build a very long string. |
| v = CompileRun("'a'.repeat(1000)"); |
| o = v8::Utils::OpenHandle(*v); |
| props = d::GetObjectProperties(o->ptr(), &ReadMemory, heap_addresses); |
| CHECK(Contains(props->brief, "\"" + std::string(80, 'a') + "...\"")); |
| |
| // GetObjectProperties can read cacheable external strings. |
| auto external_string = |
| v8::String::NewExternalTwoByte(isolate, new StringResource(true)); |
| o = v8::Utils::OpenHandle(*external_string.ToLocalChecked()); |
| props = d::GetObjectProperties(o->ptr(), &ReadMemory, heap_addresses); |
| CHECK(Contains(props->brief, "\"abcde\"")); |
| // GetObjectProperties cannot read uncacheable external strings. |
| external_string = |
| v8::String::NewExternalTwoByte(isolate, new StringResource(false)); |
| o = v8::Utils::OpenHandle(*external_string.ToLocalChecked()); |
| props = d::GetObjectProperties(o->ptr(), &ReadMemory, heap_addresses); |
| CHECK_EQ(std::string(props->brief).find("\""), std::string::npos); |
| |
| // Build a basic JS object and get its properties. |
| v = CompileRun("({a: 1, b: 2})"); |
| o = v8::Utils::OpenHandle(*v); |
| props = d::GetObjectProperties(o->ptr(), &ReadMemory, heap_addresses); |
| |
| // Objects constructed from literals get their properties placed inline, so |
| // the GetObjectProperties response should include an array. |
| const d::ObjectProperty& prop = FindProp(*props, "in-object properties"); |
| CheckProp(prop, "v8::internal::Object", "in-object properties", |
| d::PropertyKind::kArrayOfKnownSize, 2); |
| // The second item in that array is the SMI value 2 from the object literal. |
| props2 = |
| d::GetObjectProperties(reinterpret_cast<i::Tagged_t*>(prop.address)[1], |
| &ReadMemory, heap_addresses); |
| CHECK(props2->brief == std::string("2 (0x2)")); |
| |
| // Verify the result for a heap object field which is itself a struct: the |
| // "descriptors" field on a DescriptorArray. |
| // Start by getting the object's map and the map's descriptor array. |
| props = d::GetObjectProperties(ReadProp<i::Tagged_t>(*props, "map"), |
| &ReadMemory, heap_addresses); |
| props = d::GetObjectProperties( |
| ReadProp<i::Tagged_t>(*props, "instance_descriptors"), &ReadMemory, |
| heap_addresses); |
| // It should have at least two descriptors (possibly plus slack). |
| CheckProp(*props->properties[1], "uint16_t", "number_of_all_descriptors"); |
| uint16_t number_of_all_descriptors = |
| *reinterpret_cast<uint16_t*>(props->properties[1]->address); |
| CHECK_GE(number_of_all_descriptors, 2); |
| // The "descriptors" property should describe the struct layout for each |
| // element in the array. |
| const d::ObjectProperty& descriptors = *props->properties[6]; |
| // No C++ type is reported directly because there may not be an actual C++ |
| // struct with this layout, hence the empty string in this check. |
| CheckProp(descriptors, /*type=*/"", "descriptors", |
| d::PropertyKind::kArrayOfKnownSize, number_of_all_descriptors); |
| CHECK_EQ(descriptors.size, 3 * i::kTaggedSize); |
| CHECK_EQ(descriptors.num_struct_fields, 3); |
| CheckStructProp(*descriptors.struct_fields[0], |
| "v8::internal::PrimitiveHeapObject", "key", |
| 0 * i::kTaggedSize); |
| CheckStructProp(*descriptors.struct_fields[1], "v8::internal::Object", |
| "details", 1 * i::kTaggedSize); |
| CheckStructProp(*descriptors.struct_fields[2], "v8::internal::Object", |
| "value", 2 * i::kTaggedSize); |
| |
| // Build a basic JS function and get its properties. This will allow us to |
| // exercise bitfield functionality. |
| v = CompileRun("(function () {})"); |
| o = v8::Utils::OpenHandle(*v); |
| props = d::GetObjectProperties(o->ptr(), &ReadMemory, heap_addresses); |
| props = d::GetObjectProperties( |
| ReadProp<i::Tagged_t>(*props, "shared_function_info"), &ReadMemory, |
| heap_addresses); |
| const d::ObjectProperty& flags = FindProp(*props, "flags"); |
| CHECK_GE(flags.num_struct_fields, 3); |
| CheckStructProp(*flags.struct_fields[0], "FunctionKind", "function_kind", 0, |
| 5, 0); |
| CheckStructProp(*flags.struct_fields[1], "bool", "is_native", 0, 1, 5); |
| CheckStructProp(*flags.struct_fields[2], "bool", "is_strict", 0, 1, 6); |
| |
| // Get data about a different bitfield struct which is contained within a smi. |
| Handle<i::JSFunction> function = Handle<i::JSFunction>::cast(o); |
| Handle<i::SharedFunctionInfo> shared(function->shared(), i_isolate); |
| Handle<i::DebugInfo> debug_info = |
| i_isolate->debug()->GetOrCreateDebugInfo(shared); |
| props = |
| d::GetObjectProperties(debug_info->ptr(), &ReadMemory, heap_addresses); |
| const d::ObjectProperty& debug_flags = FindProp(*props, "flags"); |
| CHECK_GE(debug_flags.num_struct_fields, 5); |
| CheckStructProp(*debug_flags.struct_fields[0], "bool", "has_break_info", 0, 1, |
| i::kSmiTagSize + i::kSmiShiftSize); |
| CheckStructProp(*debug_flags.struct_fields[4], "bool", "can_break_at_entry", |
| 0, 1, i::kSmiTagSize + i::kSmiShiftSize + 4); |
| } |
| |
| TEST(ListObjectClasses) { |
| CcTest::InitializeVM(); |
| |
| // The ListObjectClasses result will change as classes are added, removed, or |
| // renamed. Just check that a few expected classes are included in the list, |
| // and that there are no duplicates. |
| const d::ClassList* class_list = d::ListObjectClasses(); |
| std::unordered_set<std::string> class_set; |
| for (size_t i = 0; i < class_list->num_class_names; ++i) { |
| CHECK_WITH_MSG(class_set.insert(class_list->class_names[i]).second, |
| "there should be no duplicate entries"); |
| } |
| CHECK_NE(class_set.find("v8::internal::HeapObject"), class_set.end()); |
| CHECK_NE(class_set.find("v8::internal::String"), class_set.end()); |
| CHECK_NE(class_set.find("v8::internal::JSRegExp"), class_set.end()); |
| } |
| |
| static void FrameIterationCheck( |
| v8::Local<v8::String> name, |
| const v8::PropertyCallbackInfo<v8::Value>& info) { |
| i::StackFrameIterator iter(reinterpret_cast<i::Isolate*>(info.GetIsolate())); |
| for (int i = 0; !iter.done(); i++) { |
| i::StackFrame* frame = iter.frame(); |
| CHECK(i != 0 || (frame->type() == i::StackFrame::EXIT)); |
| d::StackFrameResultPtr props = d::GetStackFrame(frame->fp(), &ReadMemory); |
| if (frame->is_java_script()) { |
| JavaScriptFrame* js_frame = JavaScriptFrame::cast(frame); |
| CHECK_EQ(props->num_properties, 1); |
| CheckProp(*props->properties[0], "v8::internal::JSFunction", |
| "currently_executing_jsfunction", js_frame->function().ptr()); |
| } else { |
| CHECK_EQ(props->num_properties, 0); |
| } |
| iter.Advance(); |
| } |
| } |
| |
| THREADED_TEST(GetFrameStack) { |
| LocalContext env; |
| v8::Isolate* isolate = env->GetIsolate(); |
| v8::HandleScope scope(isolate); |
| v8::Local<v8::ObjectTemplate> obj = v8::ObjectTemplate::New(isolate); |
| obj->SetAccessor(v8_str("xxx"), FrameIterationCheck); |
| CHECK(env->Global() |
| ->Set(env.local(), v8_str("obj"), |
| obj->NewInstance(env.local()).ToLocalChecked()) |
| .FromJust()); |
| v8::Script::Compile(env.local(), v8_str("function foo() {" |
| " return obj.xxx;" |
| "}" |
| "foo();")) |
| .ToLocalChecked() |
| ->Run(env.local()) |
| .ToLocalChecked(); |
| } |
| |
| } // namespace internal |
| } // namespace v8 |