Skip to content

Commit 9adc8dd

Browse files
authored
When running OS Plugins from dSYM's, make sure start state is correct (#146441)
This is an odd corner case of the use of scripts loaded from dSYM's - a macOS only feature, which can load OS Plugins that re-present the thread state of the program we attach to. If we find out about and load the dSYM scripts when we discover a target in the course of attaching to it, we can end up running the OS plugin before we've started up the private state thread. However, the os_plugin in that case will be running before we broadcast the stop event to the public event listener. So it should formally use the private state and not the public state for the Python code environment. This patch says that if we have not yet started up the private state thread, then any thread that is servicing events is doing so on behalf of the private state machinery, and should see the private state, not the public state. Most of the patch is getting a test that will actually reproduce the error. Only the test `test_python_os_plugin_remote` actually reproduced the error. In `test_python_os_plugin` we actually do start up the private state thread before handling the event. `test_python_os_plugin` is there for completeness sake.
1 parent 13c8970 commit 9adc8dd

File tree

10 files changed

+282
-10
lines changed

10 files changed

+282
-10
lines changed

lldb/include/lldb/Host/HostThread.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ class HostThread {
4343

4444
bool EqualsThread(lldb::thread_t thread) const;
4545

46+
bool HasThread() const;
47+
4648
private:
4749
std::shared_ptr<HostNativeThreadBase> m_native_thread;
4850
};

lldb/include/lldb/Target/Process.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2547,6 +2547,8 @@ void PruneThreadPlans();
25472547

25482548
bool CurrentThreadIsPrivateStateThread();
25492549

2550+
bool CurrentThreadPosesAsPrivateStateThread();
2551+
25502552
virtual Status SendEventData(const char *data) {
25512553
return Status::FromErrorString(
25522554
"Sending an event is not supported for this process.");

lldb/source/Host/common/HostThread.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,9 @@ lldb::thread_result_t HostThread::GetResult() const {
4444
bool HostThread::EqualsThread(lldb::thread_t thread) const {
4545
return m_native_thread->EqualsThread(thread);
4646
}
47+
48+
bool HostThread::HasThread() const {
49+
if (!m_native_thread)
50+
return false;
51+
return m_native_thread->GetSystemHandle() != LLDB_INVALID_HOST_THREAD;
52+
}

lldb/source/Target/Process.cpp

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,7 +1271,7 @@ uint32_t Process::AssignIndexIDToThread(uint64_t thread_id) {
12711271
}
12721272

12731273
StateType Process::GetState() {
1274-
if (CurrentThreadIsPrivateStateThread())
1274+
if (CurrentThreadPosesAsPrivateStateThread())
12751275
return m_private_state.GetValue();
12761276
else
12771277
return m_public_state.GetValue();
@@ -3144,16 +3144,19 @@ void Process::CompleteAttach() {
31443144
}
31453145
}
31463146

3147-
if (!m_os_up) {
3147+
// If we don't have an operating system plugin loaded yet, see if
3148+
// LoadOperatingSystemPlugin can find one (and stuff it in m_os_up).
3149+
if (!m_os_up)
31483150
LoadOperatingSystemPlugin(false);
3149-
if (m_os_up) {
3150-
// Somebody might have gotten threads before now, but we need to force the
3151-
// update after we've loaded the OperatingSystem plugin or it won't get a
3152-
// chance to process the threads.
3153-
m_thread_list.Clear();
3154-
UpdateThreadListIfNeeded();
3155-
}
3151+
3152+
if (m_os_up) {
3153+
// Somebody might have gotten threads before we loaded the OS Plugin above,
3154+
// so we need to force the update now or the newly loaded plugin won't get
3155+
// a chance to process the threads.
3156+
m_thread_list.Clear();
3157+
UpdateThreadListIfNeeded();
31563158
}
3159+
31573160
// Figure out which one is the executable, and set that in our target:
31583161
ModuleSP new_executable_module_sp;
31593162
for (ModuleSP module_sp : GetTarget().GetImages().Modules()) {
@@ -5856,6 +5859,13 @@ bool Process::CurrentThreadIsPrivateStateThread()
58565859
return m_private_state_thread.EqualsThread(Host::GetCurrentThread());
58575860
}
58585861

5862+
bool Process::CurrentThreadPosesAsPrivateStateThread() {
5863+
// If we haven't started up the private state thread yet, then whatever thread
5864+
// is fetching this event should be temporarily the private state thread.
5865+
if (!m_private_state_thread.HasThread())
5866+
return true;
5867+
return m_private_state_thread.EqualsThread(Host::GetCurrentThread());
5868+
}
58595869

58605870
void Process::Flush() {
58615871
m_thread_list.Flush();

lldb/source/Target/StackFrameList.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,7 @@ void StackFrameList::SelectMostRelevantFrame() {
723723
// Don't call into the frame recognizers on the private state thread as
724724
// they can cause code to run in the target, and that can cause deadlocks
725725
// when fetching stop events for the expression.
726-
if (m_thread.GetProcess()->CurrentThreadIsPrivateStateThread())
726+
if (m_thread.GetProcess()->CurrentThreadPosesAsPrivateStateThread())
727727
return;
728728

729729
Log *log = GetLog(LLDBLog::Thread);

lldb/test/API/functionalities/plugins/python_os_plugin/operating_system.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ def create_thread(self, tid, context):
2424
return None
2525

2626
def get_thread_info(self):
27+
if self.process.state != lldb.eStateStopped:
28+
print("Error: get_thread_info called with state not stopped")
29+
return []
30+
2731
if not self.threads:
2832
self.threads = [
2933
{
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
C_SOURCES := main.c
2+
ENABLE_THREADS := YES
3+
4+
include Makefile.rules
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""
2+
Test that an OS plugin in a dSYM sees the right process state
3+
when run from a dSYM on attach
4+
"""
5+
6+
from lldbsuite.test.decorators import *
7+
from lldbsuite.test.lldbtest import *
8+
import lldbsuite.test.lldbutil as lldbutil
9+
from lldbgdbserverutils import get_debugserver_exe
10+
11+
import os
12+
import lldb
13+
import time
14+
import socket
15+
import shutil
16+
17+
18+
class TestOSPluginIndSYM(TestBase):
19+
NO_DEBUG_INFO_TESTCASE = True
20+
21+
# The port used by debugserver.
22+
PORT = 54638
23+
24+
# The number of attempts.
25+
ATTEMPTS = 10
26+
27+
# Time given to the binary to launch and to debugserver to attach to it for
28+
# every attempt. We'll wait a maximum of 10 times 2 seconds while the
29+
# inferior will wait 10 times 10 seconds.
30+
TIMEOUT = 2
31+
32+
def no_debugserver(self):
33+
if get_debugserver_exe() is None:
34+
return "no debugserver"
35+
return None
36+
37+
def port_not_available(self):
38+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
39+
if s.connect_ex(("127.0.0.1", self.PORT)) == 0:
40+
return "{} not available".format(self.PORT)
41+
return None
42+
43+
@skipUnlessDarwin
44+
def test_python_os_plugin(self):
45+
self.do_test_python_os_plugin(False)
46+
47+
@skipTestIfFn(no_debugserver)
48+
@skipTestIfFn(port_not_available)
49+
def test_python_os_plugin_remote(self):
50+
self.do_test_python_os_plugin(True)
51+
52+
def do_test_python_os_plugin(self, remote):
53+
"""Test that the environment for os plugins in dSYM's is correct"""
54+
executable = self.build_dsym("my_binary")
55+
56+
# Make sure we're set up to load the symbol file's python
57+
self.runCmd("settings set target.load-script-from-symbol-file true")
58+
59+
target = self.dbg.CreateTarget(None)
60+
61+
error = lldb.SBError()
62+
63+
# Now run the process, and then attach. When the attach
64+
# succeeds, make sure that we were in the right state when
65+
# the OS plugins were run.
66+
if not remote:
67+
popen = self.spawnSubprocess(executable, [])
68+
69+
process = target.AttachToProcessWithID(lldb.SBListener(), popen.pid, error)
70+
self.assertSuccess(error, "Attach succeeded")
71+
else:
72+
self.setup_remote_platform(executable)
73+
process = target.process
74+
self.assertTrue(process.IsValid(), "Got a valid process from debugserver")
75+
76+
# We should have figured out the target from the result of the attach:
77+
self.assertTrue(target.IsValid, "Got a valid target")
78+
79+
# Make sure that we got the right plugin:
80+
self.expect(
81+
"settings show target.process.python-os-plugin-path",
82+
substrs=["operating_system.py"],
83+
)
84+
85+
for thread in process.threads:
86+
stack_depth = thread.num_frames
87+
reg_threads = thread.frames[0].reg
88+
89+
# OKAY, that realized the threads, now see if the creation
90+
# state was correct. The way we use the OS plugin, it doesn't need
91+
# to create a thread, and doesn't have to call get_register_info,
92+
# so we don't expect those to get called.
93+
self.expect(
94+
"test_report_command",
95+
substrs=[
96+
"in_init=1",
97+
"in_get_thread_info=1",
98+
"in_create_thread=2",
99+
"in_get_register_info=2",
100+
"in_get_register_data=1",
101+
],
102+
)
103+
104+
def build_dsym(self, name):
105+
self.build(debug_info="dsym", dictionary={"EXE": name})
106+
executable = self.getBuildArtifact(name)
107+
dsym_path = self.getBuildArtifact(name + ".dSYM")
108+
python_dir_path = dsym_path
109+
python_dir_path = os.path.join(dsym_path, "Contents", "Resources", "Python")
110+
if not os.path.exists(python_dir_path):
111+
os.mkdir(python_dir_path)
112+
python_file_name = name + ".py"
113+
114+
os_plugin_dir = os.path.join(python_dir_path, "OS_Plugin")
115+
if not os.path.exists(os_plugin_dir):
116+
os.mkdir(os_plugin_dir)
117+
118+
plugin_dest_path = os.path.join(os_plugin_dir, "operating_system.py")
119+
plugin_origin_path = os.path.join(self.getSourceDir(), "operating_system.py")
120+
shutil.copy(plugin_origin_path, plugin_dest_path)
121+
122+
module_dest_path = os.path.join(python_dir_path, python_file_name)
123+
with open(module_dest_path, "w") as f:
124+
f.write("def __lldb_init_module(debugger, unused):\n")
125+
f.write(
126+
f" debugger.HandleCommand(\"settings set target.process.python-os-plugin-path '{plugin_dest_path}'\")\n"
127+
)
128+
f.close()
129+
130+
return executable
131+
132+
def setup_remote_platform(self, exe):
133+
# Get debugserver to start up our process for us, and then we
134+
# can use `process connect` to attach to it.
135+
debugserver = get_debugserver_exe()
136+
debugserver_args = ["localhost:{}".format(self.PORT), exe]
137+
self.spawnSubprocess(debugserver, debugserver_args)
138+
139+
# Select the platform.
140+
self.runCmd("platform select remote-gdb-server")
141+
142+
# Connect to debugserver
143+
interpreter = self.dbg.GetCommandInterpreter()
144+
connected = False
145+
for i in range(self.ATTEMPTS):
146+
result = lldb.SBCommandReturnObject()
147+
interpreter.HandleCommand(f"gdb-remote localhost:{self.PORT}", result)
148+
connected = result.Succeeded()
149+
if connected:
150+
break
151+
time.sleep(self.TIMEOUT)
152+
153+
self.assertTrue(connected, "could not connect to debugserver")
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#include <unistd.h>
2+
3+
int main() {
4+
while (1) {
5+
sleep(1);
6+
}
7+
return 0;
8+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env python
2+
3+
import lldb
4+
import struct
5+
6+
# Value is:
7+
# 0 called - state is not stopped
8+
# 1 called - state is stopped
9+
# 2 not called
10+
11+
stop_state = {
12+
"in_init": 2,
13+
"in_get_thread_info": 2,
14+
"in_create_thread": 2,
15+
"in_get_register_info": 2,
16+
"in_get_register_data": 2,
17+
}
18+
19+
20+
def ReportCommand(debugger, command, exe_ctx, result, unused):
21+
global stop_state
22+
for state in stop_state:
23+
result.AppendMessage(f"{state}={stop_state[state]}\n")
24+
result.SetStatus(lldb.eReturnStatusSuccessFinishResult)
25+
26+
27+
class OperatingSystemPlugIn:
28+
"""This class checks that all the"""
29+
30+
def __init__(self, process):
31+
"""Initialization needs a valid.SBProcess object.
32+
global stop_state
33+
34+
This plug-in will get created after a live process is valid and has stopped for the
35+
first time."""
36+
self.process = process
37+
stop_state["in_init"] = self.state_is_stopped()
38+
interp = process.target.debugger.GetCommandInterpreter()
39+
result = lldb.SBCommandReturnObject()
40+
cmd_str = (
41+
f"command script add test_report_command -o -f {__name__}.ReportCommand"
42+
)
43+
interp.HandleCommand(cmd_str, result)
44+
45+
def state_is_stopped(self):
46+
if self.process.state == lldb.eStateStopped:
47+
return 1
48+
else:
49+
return 0
50+
51+
def does_plugin_report_all_threads(self):
52+
return True
53+
54+
def create_thread(self, tid, context):
55+
global stop_state
56+
stop_state["in_create_thread"] = self.state_is_stopped()
57+
58+
return None
59+
60+
def get_thread_info(self):
61+
global stop_state
62+
stop_state["in_get_thread_info"] = self.state_is_stopped()
63+
idx = self.process.threads[0].idx
64+
return [
65+
{
66+
"tid": 0x111111111,
67+
"name": "one",
68+
"queue": "queue1",
69+
"state": "stopped",
70+
"stop_reason": "breakpoint",
71+
"core": idx,
72+
}
73+
]
74+
75+
def get_register_info(self):
76+
global stop_state
77+
stop_state["in_get_register_info"] = self.state_is_stopped()
78+
return None
79+
80+
def get_register_data(self, tid):
81+
global stop_state
82+
stop_state["in_get_register_data"] = self.state_is_stopped()
83+
return None

0 commit comments

Comments
 (0)