Skip to content

Fix race between adapter connection publication and session attachment#2049

Open
aperez wants to merge 2 commits into
microsoft:mainfrom
aperez:fix-adapter-publication-race
Open

Fix race between adapter connection publication and session attachment#2049
aperez wants to merge 2 commits into
microsoft:mainfrom
aperez:fix-adapter-publication-race

Conversation

@aperez

@aperez aperez commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Connection.__init__ publishes a new connection to _connections under _lock, then releases the lock before classifying it. In that window another thread can attach the connection to a session (via an attach request or the launch flow), which sets conn.server and the session's pid. When the constructor resumes, sessions.get(self.pid) returns that just-attached session, and because it is not a process replacement the old code logged "is not expecting replacement" and closed the channel, dropping a session that was already live. This shows up with subprocess debugging and in-place process replacement.

Fix it by re-checking under _lock whether the connection was already attached (self.server is not None) before treating it as an unexpected replacement. Closing the channel under _lock is deadlock-free: JsonMessageChannel.close() only takes the channel's own lock and the disconnect handler runs on the parser thread.

Add a regression test that overlaps publication and attachment.

`Connection.__init__` publishes a new connection to `_connections` under
`_lock`, then releases the lock before classifying it. In that window
another thread can attach the connection to a session (via an `attach`
request or the launch flow), which sets `conn.server` and the session's
`pid`. When the constructor resumes, `sessions.get(self.pid)` returns
that just-attached session, and because it is not a process replacement
the old code logged "is not expecting replacement" and closed the
channel, dropping a session that was already live. This shows up with
subprocess debugging and in-place process replacement.

Fix it by re-checking under `_lock` whether the connection was already
attached (`self.server is not None`) before treating it as an unexpected
replacement. Closing the channel under `_lock` is deadlock-free:
`JsonMessageChannel.close()` only takes the channel's own lock and the
disconnect handler runs on the parser thread.

Add a regression test that overlaps publication and attachment.
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@aperez

aperez commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree company="Meta"

@aperez
aperez marked this pull request as ready for review July 23, 2026 20:21
@aperez
aperez requested a review from a team as a code owner July 23, 2026 20:21
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@rchiodo

rchiodo commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

🔒 Automated review in progress — @rchiodo is auto-reviewing this PR.

@rchiodo rchiodo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved via Review Center.

@heejaechang heejaechang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved via Review Center.

@rchiodo

rchiodo commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Comment thread tests/debugpy/adapter/test_servers.py Outdated
connection_thread.start()
try:
assert attachment_started.wait(5)
assert classification_waiting.wait(5)

@rchiodo rchiodo Jul 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This test feels a bit over-coupled to the implementation. The ObservedLock + classification_waiting scaffolding asserts that the fix re-acquires _lock specifically during classification — a correct-but-different fix (e.g. snapshotting self.server a different way) would fail here even though the behavior is fine. The real value is in the final behavioral checks (connections[0].server is attached_session.server and not channel.closed). Would it make sense to do that instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the feedback!
I rewrote the test to be behavioral: it now drives the real attach path (wait_for_connectionattach_to_session) and asserts only outcomes, connections[0].server is session.server and not channel.closed. I dropped the ObservedLock/classification_waiting scaffolding entirely. The test still fails against the pre-fix code (channel gets closed).

@rchiodo

rchiodo commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@heejaechang heejaechang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved via Review Center.

@rchiodo rchiodo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved via Review Center.

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.

3 participants