') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); [release/6.0] Delete dangling thread session states by davmason · Pull Request #76431 · dotnet/runtime · GitHub
Skip to content

[release/6.0] Delete dangling thread session states - #76431

Merged
carlossanlop merged 5 commits into
dotnet:release/6.0from
davmason:session_use_after_free_6.0
Oct 7, 2022
Merged

[release/6.0] Delete dangling thread session states#76431
carlossanlop merged 5 commits into
dotnet:release/6.0from
davmason:session_use_after_free_6.0

Conversation

@davmason

Copy link
Copy Markdown
Contributor

As described in #76430, we have a partner team running in to a previously unknown EventPipe issue. We can leak EventPipeThreadSessionState* under certain circumstances, leading to a fatal CLR error and crashing the process.

This fix is a targeted fix for 6.0 where we do a simple loop over the existing threads and manually delete any dangling session states when we delete the session object. The fix for 8.0 will be more of a refactoring to make sure EventPipeThreadSessionStates are owned in a logically consistent manner.

Customer Impact

Customers that have many threads and short lived sessions are vulnerable to this issue and there is no workaround. They will experience random crashes.

Risk

Low, it is a targeted fix that is easy to reason about

Testing

Manual testing by our team, and pending partner validation

@davmasondavmason added this to the 6.0.x milestone Sep 30, 2022
@davmason
davmason requested review from a team, lateralusX and noahfalkSeptember 30, 2022 09:34
@davmasondavmason self-assigned this Sep 30, 2022
Comment threadsrc/native/eventpipe/ep-session.c Outdated
ep_rt_thread_array_iterator_t threads_iterator = ep_rt_thread_array_iterator_begin (&threads);
while (!ep_rt_thread_array_iterator_end (&threads, &threads_iterator)) {
EventPipeThread *thread = ep_rt_thread_array_iterator_value (&threads_iterator);
if (thread) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

When can this be null?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I don't think it ever can be null, we only ever add non-null threads in ep_thread_get_threads. I think this just keeps getting copy pasted around, anywhere we iterate threads we check for null on each entry

@lateralusXlateralusXOct 3, 2022

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This check is really not needed (at least not anymore). ep_thread_get_threads is currently only called from ep_session_suspend_write_event (before this change added a new one), and the logic in ep_thread_get_threads already makes sure threads added to the lists are not null, so only way to get null pointers in the list is corruption or future coding errors. Maybe we should change the check to an EP_ASSERT or drop it.

@jeffschwMSFTjeffschwMSFT left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

approved. we will take for consideration. please get a code review and best if we can get verification from the partner that the issue is addressed.


ep_thread_requires_lock_held (thread);

EP_ASSERT (thread->session_state [ep_session_get_index (session)] != NULL);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Did you hit this assert? Looking at the callers of ep_thread_get_session_state, you should end up with more asserts in the case this returns NULL

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The code in ep_session_remove_dangling_session_states iterates over all threads and under normal circumstances it will return NULL, it will only return non-NULL if a session state is leaked. So we will hit this assert in the code I added

@lateralusXlateralusXOct 4, 2022

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great! I didn't find your call since I didn't search your branch for callers of ep_thread_get_session_state and for the other callers it is critical that this assert holds. Maybe we could move the assert to the callers (only two places) where it should still be true, ep_thread_get_session_state shouldn't return a NULL session state.

Comment threadsrc/native/eventpipe/ep-session.c Outdated
// has been exceeded, we can leak the EventPipeThreadSessionState* and crash later trying to access
// the session from the thread session state. Whenever we terminate a session we check to make sure
// we haven't leaked any thread session states.
ep_thread_delete_session_state(thread, session);

@lateralusXlateralusXOct 3, 2022

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In case where the thread session state ends up in buffer_manager->thread_session_state_list and later deleted when we flush or delete the buffer manager buffers (ep_buffer_manager_deallocate_buffers), I guess the order in ep_session_free will prevent us from having any additional copies of thread session state when we call ep_session_remove_dangling_session_states?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

That's correct, I made sure to add the cleanup code as the last thing before we free the session object so everything except the leaked session states are cleaned up

@lateralusX

lateralusX commented Oct 3, 2022

Copy link
Copy Markdown
Member

Could an alternative fix be to just handle the case where we created a new thread session state but fails to add it to buffer manager, triggering the error case? All the logic seems to be located in ep_buffer_manager_write_event where we also detect the case where we drop events that will lead to the issue with leaked session state. We can detect when we create a new session state for current thread as well as failing to add it into buffer managers thread_session_state_list and make sure we get rid of the allocated thread state directly in ep_buffer_manager_write_event if/when we hit that case.

@davmason

Copy link
Copy Markdown
ContributorAuthor

Could an alternative fix be to just handle the case where we created a new thread session state but fails to add it to buffer manager, triggering the error case? All the logic seems to be located in ep_buffer_manager_write_event where we also detect the case where we drop events that will lead to the issue with leaked session state. We can detect when we create a new session state for current thread as well as failing to add it into buffer managers thread_session_state_list and make sure we get rid of the allocated thread state directly in ep_buffer_manager_write_event if/when we hit that case.

I thought about that approach but this way seemed easier to reason about for servicing, so it has less risk of introducing bugs.

@JulieLeeMSFT

Copy link
Copy Markdown
Member

Please check the test failures.

@lateralusX

lateralusX commented Oct 4, 2022

Copy link
Copy Markdown
Member

Could an alternative fix be to just handle the case where we created a new thread session state but fails to add it to buffer manager, triggering the error case? All the logic seems to be located in ep_buffer_manager_write_event where we also detect the case where we drop events that will lead to the issue with leaked session state. We can detect when we create a new session state for current thread as well as failing to add it into buffer managers thread_session_state_list and make sure we get rid of the allocated thread state directly in ep_buffer_manager_write_event if/when we hit that case.

I thought about that approach but this way seemed easier to reason about for servicing, so it has less risk of introducing bugs.

OK, handling it on the thread that creates the state before ever getting into threads session list in case we hit failure case without any future needs to do lookup and potentially cleanup on all threads session state lists sounds rather straight forward and removes any potential multithreading issues related to the fix as well, but if you validated the current approach, I agree that we should go with the fix you feel most comfortable with and then we should probably fix it in main by handling the error case and only add the session state to the thread list when we successfully transferred ownership of session state.

Comment threadsrc/native/eventpipe/ep-session.c Outdated
Comment threadsrc/native/eventpipe/ep-session.c Outdated
davmasonand others added 3 commits October 4, 2022 01:13
Co-authored-by: Johan Lorensson <lateralusx.github@gmail.com>
Co-authored-by: Johan Lorensson <lateralusx.github@gmail.com>

@lateralusXlateralusX left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@tommcdon

Copy link
Copy Markdown
Member

We have partner teams reporting the same issue on 7.0, and so this change should also be considered for 7.0 as well.

@carlossanlop

Copy link
Copy Markdown
Contributor

@davmason can you please add the servicing-consider label and send an email to Tactics requesting approval?

@jeffschwMSFTjeffschwMSFT added the Servicing-consider Issue for next servicing release review label Oct 5, 2022
@ZacWein

Copy link
Copy Markdown

I am from the partner team @davmason is referencing.

We went ahead and applied patched dotnet 6 dlls to half of our machines which had been encountering the issue and left the other half the same.

The patched machine are no longer crashing, but the un-patched machines have also stopped crashing which was unexpected.

Note the nature of these crashes is transient, so its possible that we just aren't seeing crashes at the moment, but may see unpatched machines start crashing tomorrow...

@davmason

Copy link
Copy Markdown
ContributorAuthor

@jeffschwMSFT - I was able to create a local repro that demonstrates the issue, and my changes fix the issue. Combined with the fact that Zach was able to run the privates I provided him with no issues (so we verified it doesn't break him at least) I think we should proceed with the fix.

@jeffschwMSFTjeffschwMSFT modified the milestones: 6.0.x, 6.0.11Oct 6, 2022
@jeffschwMSFTjeffschwMSFT added Servicing-approved Approved for servicing release and removed Servicing-consider Issue for next servicing release review labels Oct 6, 2022
@carlossanlop

Copy link
Copy Markdown
Contributor

CI green, approved, signed-off. No OOB package authoring changes needed.
Checked with David via chat, this is ready to merge (all feedback was addressed). :shipit:

@carlossanlop
carlossanlop merged commit cea0fb5 into dotnet:release/6.0Oct 7, 2022
@ghostghost locked as resolved and limited conversation to collaborators Nov 6, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants

@davmason@lateralusX@JulieLeeMSFT@tommcdon@carlossanlop@ZacWein@jkotas@jeffschwMSFT