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

Store copy of default buffer to shared user buffer #320

Merged
merged 10 commits into from
Jan 4, 2025

Conversation

cb1kenobi
Copy link
Collaborator

@cb1kenobi cb1kenobi commented Jan 3, 2025

Fixes a memory bug where underlying buffer data was no longer available causing the V8 garbage collected to crash Node.js.

node(98509,0x2087b0240) malloc: Heap corruption detected, free list is damaged at 0x600001968210

The culprit was due to at least three things:

  1. The default buffer data was being directly referenced by multiple ArrayBuffer instances
  2. The default buffer would be garbage collected as soon as getUserSharedBuffer() returned
  3. There was insufficient reference counting to prevent V8 from garbage collecting values still in use

The solution is to create a copy of the default buffer data and store it in the user_buffer_t. When the shared user buffer is requested, the smart pointer's reference count is incremented, and a new V8 ArrayBuffer value is returned.

Since the bug has been fixed, the changes to read.js from 3a18645 have been reverted.

@kriszyp
Copy link
Owner

kriszyp commented Jan 3, 2025

Thank you for the hard work on this!
However, I don't think we can actually share a JS value (the ArrayBuffer instance) across isolates. Any JS value can only be used in the isolate in which it was created. With the existing tests, this is problem is not apparent. So I created some expanded multi-threaded/worker tests that access getUserSharedBuffer from different worker/isolates (and fix the simple unit test): https://github.com/kriszyp/lmdb-js/tree/shared-buf-refs-fix
The use of the single JS value (ArrayBuffer) across workers does indeed frequently cause this test to crash (see samples below).
Ultimately, I believe there needs to be a separate ArrayBuffer for each isolate, that points to the same shared memory (basically for each isolate, need to call capi_create_external_arraybuffer(... shared_memory_address)).
Here are some of the various errors I observed with these tests (I don't think it is possible to make sense of them, they just demonstrate that multi-threaded access to shared JS values is unsafe and corrupts memory):

#
# Fatal error in , line 0
# Check failed: current_key != *desc->GetKey().
#
#
#
#FailureMessage Object: 0x7f63051fa510
----- Native stack trace -----

 1: 0xd45071  [node]
 2: 0x218d121 V8_Fatal(char const*, ...) [node]
 3: 0x13de769 v8::internal::DescriptorArray::CheckNameCollisionDuringInsertion(v8::internal::Descriptor*, unsigned int, int) [node]
 4: 0x13ca87a v8::internal::Map::ShareDescriptor(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Map>, v8::internal::Handle<v8::internal::DescriptorArray>, v8::internal::Descriptor*) [node]
 5: 0x13cac54 v8::internal::Map::CopyWithField(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Map>, v8::internal::Handle<v8::internal::Name>, v8::internal::Handle<v8::internal::FieldType>, v8::internal::PropertyAttributes, v8::internal::PropertyConstness, v8::internal::Representation, v8::internal::TransitionFlag) [node]
 6: 0x13cc95a v8::internal::Map::TransitionToDataProperty(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Map>, v8::internal::Handle<v8::internal::Name>, v8::internal::Handle<v8::internal::Object>, v8::internal::PropertyAttributes, v8::internal::PropertyConstness, v8::internal::StoreOrigin) [node]
 7: 0x13bbeca v8::internal::LookupIterator::PrepareTransitionToDataProperty(v8::internal::Handle<v8::internal::JSReceiver>, v8::internal::Handle<v8::internal::Object>, v8::internal::PropertyAttributes, v8::internal::StoreOrigin) [node]
 8: 0x13d7918 v8::internal::Object::TransitionAndWriteDataProperty(v8::internal::LookupIterator*, v8::internal::Handle<v8::internal::Object>, v8::internal::PropertyAttributes, v8::Maybe<v8::internal::ShouldThrow>, v8::internal::StoreOrigin) [node]
 9: 0x11de736 v8::internal::StoreIC::Store(v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Name>, v8::internal::Handle<v8::internal::Object>, v8::internal::StoreOrigin) [node]
10: 0x11df233 v8::internal::Runtime_StoreIC_Miss(int, unsigned long*, v8::internal::Isolate*) [node]



#
# Fatal error in , line 0
# unreachable code
#
#
#
#FailureMessage Object: 0x772e2dfff650



#
# Fatal error in , line 0
# unreachable code
#
#
#
#FailureMessage Object: 0x7ffe66166090
----- Native stack trace -----

 1: 0xd45071  [node]
 2: 0x218d121 V8_Fatal(char const*, ...) [node]
 3: 0x115fe02 auto v8::internal::BodyDescriptorApply<v8::internal::CallIterateBody, v8::internal::Map&, v8::internal::HeapObject&, int&, v8::internal::RecordMigratedSlotVisitor*&>(v8::internal::InstanceType, v8::internal::Map&, v8::internal::HeapObject&, int&, v8::internal::RecordMigratedSlotVisitor*&) [node]
 4: 0x1161871 void v8::internal::EvacuateVisitorBase::RawMigrateObject<(v8::internal::EvacuateVisitorBase::MigrationMode)0>(v8::internal::EvacuateVisitorBase*, v8::internal::HeapObject, v8::internal::HeapObject, int, v8::internal::AllocationSpace) [node]
 5: 0x1155019 v8::internal::EvacuateNewSpaceVisitor::Visit(v8::internal::HeapObject, int) [node]
 6: 0x1162ca6 v8::internal::Evacuator::RawEvacuatePage(v8::internal::MemoryChunk*, long*) [node]
 7: 0x1163413 v8::internal::Evacuator::EvacuatePage(v8::internal::MemoryChunk*) [node]
 8: 0x1163747 v8::internal::PageEvacuationJob::Run(v8::JobDelegate*) [node]
 9: 0x1d7fc65 v8::platform::DefaultJobState::Join() [node]
10: 0x1d7fe33 v8::platform::DefaultJobHandle::Join() [node]
11: 0x1166ec2 v8::internal::MarkCompactCollector::EvacuatePagesInParallel() [node]
12: 0x117ba14 v8::internal::MarkCompactCollector::Evacuate() [node]
13: 0x117c54e v8::internal::MarkCompactCollector::CollectGarbage() [node]
14: 0x112a436 v8::internal::Heap::MarkCompact() [node]
15: 0x112fa8d v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::internal::GarbageCollectionReason, char const*) [node]
16: 0x112ff7c v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
17: 0x1130c9a v8::internal::Heap::FinalizeIncrementalMarkingIfComplete(v8::internal::GarbageCollectionReason) [node]
18: 0x113262a v8::internal::IncrementalMarkingJob::Task::RunInternal() [node]
19: 0xd45006  [node]
20: 0xd485af node::PerIsolatePlatformData::FlushForegroundTasksInternal() [node]
21: 0x18c78a3  [node]
22: 0x18dc31b  [node]
23: 0x18c85c7 uv_run [node]
24: 0xbd3be6 node::SpinEventLoopInternal(node::Environment*) [node]
25: 0xd18824  [node]
26: 0xd192bd node::NodeMainInstance::Run() [node]
27: 0xc7d69f node::Start(int, char**) [node]
28: 0x76d68b22a3b8  [/lib/x86_64-linux-gnu/libc.so.6]
29: 0x76d68b22a47b __libc_start_main [/lib/x86_64-linux-gnu/libc.so.6]
30: 0xbd12ee _start [node]

(this one may actually make sense)
FATAL ERROR: v8::HandleScope::CreateHandle() Cannot create a handle without a HandleScope
 1: 0xb090e0 node::Abort() [/home/kzyp/.nvm/versions/node/v16.20.2/bin/node]
 2: 0xa1b60e node::FatalError(char const*, char const*) [/home/kzyp/.nvm/versions/node/v16.20.2/bin/node]
 3: 0xce178a v8::Utils::ReportApiFailure(char const*, char const*) [/home/kzyp/.nvm/versions/node/v16.20.2/bin/node]
 4: 0xe4c622 v8::internal::HandleScope::Extend(v8::internal::Isolate*) [/home/kzyp/.nvm/versions/node/v16.20.2/bin/node]
 5: 0xce2e41 v8::HandleScope::CreateHandle(v8::internal::Isolate*, unsigned long) [/home/kzyp/.nvm/versions/node/v16.20.2/bin/node]
 6: 0xabbbd5 napi_get_reference_value [/home/kzyp/.nvm/versions/node/v16.20.2/bin/node]
 7: 0x7865eb6a74ea  [/home/kzyp/dev/lmdb-js/build/Debug/lmdb.node]
 8: 0x7865eb6a7196  [/home/kzyp/dev/lmdb-js/build/Debug/lmdb.node]
 9: 0xab380d  [/home/kzyp/.nvm/versions/node/v16.20.2/bin/node]
10: 0xd3df8e  [/home/kzyp/.nvm/versions/node/v16.20.2/bin/node]
11: 0xd3f3af v8::internal::Builtin_HandleApiCall(int, unsigned long*, v8::internal::Isolate*) [/home/kzyp/.nvm/versions/node/v16.20.2/bin/node]
12: 0x15d9f39  [/home/kzyp/.nvm/versions/node/v16.20.2/bin/node]

src/lmdb-js.h Outdated
typedef struct user_buffer_t {
MDB_val buffer;
napi_env env;
Copy link
Owner

Choose a reason for hiding this comment

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

This isn't actually used (read) is it? (And I thought it was considered unsafe to store napi_env anyway)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's referenced in the destructor so that the ref can be released: https://github.com/kriszyp/lmdb-js/pull/320/files#diff-f38d85b2982fc3a98c8a1dfe5f1cab0eea8b2e9ad9657f69d3e6338f9a25079dR1079.

Hmm, yeah storing the napi_env is probably a bad idea. Might be moot if since I will probably store the value instead of the napi_ref.

@cb1kenobi cb1kenobi changed the title Store shared user buffer as ref Store shared user buffer as shared smart pointer Jan 3, 2025
@cb1kenobi cb1kenobi changed the title Store shared user buffer as shared smart pointer Store copy of default buffer to shared user buffer Jan 3, 2025
@kriszyp kriszyp merged commit b69091f into kriszyp:master Jan 4, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants