-
-
Notifications
You must be signed in to change notification settings - Fork 191
fix: Invalid string pointers passed by SetResult due to C++ memory lifecycle
#1032
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
Conversation
|
This seems sane to me. Most issues of this nature are somewhat mitigated because the c# side will take a copy of the string at the return address immediately after invoking the method, so for some semi-transient things in the game engine this is generally enough, but not for immediately freed strings like this one. For that reason however, the string pool may actually be overkill, because we know that the consumer of the result will take a copy on the managed side, we can probably just keep a single copy of the latest result, and free it on each subsequent GetResult() |
I understand your suggestion about holding a single string copy However, regarding "free it on each subsequent GetResult()", I believe this may not be necessary When the string template SetResult is called again, @roflmuffin |
Yes my apologies I didn't mean manually free, but freed by reassigning the last result. I think it would be good if you could implement that and retest if you could. |
https://stackoverflow.com/questions/13873544/deallocation-of-memory-allocated-in-stdstring-in-c I believe that when using STL-allocated strings, we do not need to concern ourselves with memory deallocation. From my perspective, C++ automatically manages their memory through established RAII principles. I have written test code to demonstrate this behavior static thread_local std::string lastResult;
const char* oldPtr = lastResult.empty() ? nullptr : lastResult.c_str();
size_t oldCapacity = lastResult.capacity();
std::string oldContent = lastResult; // Copy for logging
printf("[BEFORE ASSIGNMENT] String: '%s' | Ptr: %p | Capacity: %zu\n", oldContent.c_str(), oldPtr, oldCapacity);
// Assignment operator automatically frees previous string content
// and allocates new memory for the incoming string
lastResult = value; // RAII: old memory freed, new memory allocated
const char* newPtr = lastResult.c_str();
size_t newCapacity = lastResult.capacity();
printf("[AFTER ASSIGNMENT] String: '%s' | Ptr: %p | Capacity: %zu\n", lastResult.c_str(), newPtr, newCapacity);The test results demonstrate that the new string directly overwrites the old string data. This behavior indicates sophisticated memory optimization within the STL implementation |
|
Actually, I did try what you suggested: releasing the previous the STL handles all of these operations transparently lol, Is there anything else you'd like me to confirm before merging? @roflmuffin |
|
LGTM, thanks |
Through Issue #1014, I discovered this critical problem
Initially, I suspected that the root cause of the above issue was related to string encoding problems
However, as our debugging deepened, I pinpointed that this problem actually occurs in
CounterStrikeSharp/src/scripting/script_engine.h
Lines 193 to 206 in 1ca8ff2
As an example:
CounterStrikeSharp/src/scripting/natives/natives_usermessages.cpp
Lines 210 to 239 in 1ca8ff2
std::string returnValueinside the functionscriptContext.SetResult(returnValue.c_str())to pass the string to the managed layerPbReadStringfunction ends,returnValueis automatically destroyedreturnValue.c_str()becomes an invalid pointerSo, I implemented a thread-local string pool in the
SetResulttemplate to ensure string lifetime extends beyond function scope, preventing dangling pointer issues in native-managed interop(...) // Create string copies to ensure persistent memory lifetime static thread_local std::vector<std::string> stringPool; static thread_local size_t poolIndex = 0; if (stringPool.size() < 16) { stringPool.emplace_back(); } (...)Regarding the observation mentioned in #1014: "When a player sends a short message containing Chinese characters (e.g., up to 5 characters, which is approx. 15 bytes in UTF-8), the message is read correctly by um.ReadString("param2")"
GUESS:
std::stringobject's internal buffer on the stack. Upon object destruction, the stack frame is marked as available for reuse but the actual memory content remains unchanged until overwritten by subsequent function calls. This creates a probabilistic window where the string data may still be accessible, depending on call stack reuse patterns and compiler optimizationsmalloc/new. Thestd::stringobject on the stack contains only a pointer to this heap-allocated buffer. Upon destruction, the string's destructor immediately callsfree/deleteon the heap memory, making it available to the memory allocator and potentially overwriting the content immediately. This eliminates any timing window for accidental data recoveryTesting confirms the expected behavior

#1014