https://github.com/qiyao updated 
https://github.com/llvm/llvm-project/pull/205753

>From 26be35369124d816832293606545f80d735c49d4 Mon Sep 17 00:00:00 2001
From: Yao Qi <[email protected]>
Date: Wed, 24 Jun 2026 21:23:21 +0100
Subject: [PATCH 1/2] [lldb] Reject NULL key in PythonDictionary::GetItem

A `breakpoint set -P <class>` with an invalid-UTF-8 class name crashes
lldb with `EXC_BAD_ACCESS`.  The class name is passed through
unmodified, so the byte sequence `\xd0p` (an incomplete two-byte UTF-8
sequence) reaches the Python layer:

```
 ./bin/lldb -b -o $'breakpoint set -P \xd0p -f main.cpp' ./bin/lldb
...
PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/
Stack dump:
 #4 PythonDictionary::GetItem(PythonObject const&) const
 #6 PythonDictionary::GetItemForKey(PythonObject const&) const
 #7 PythonObject::ResolveNameWithDictionary(StringRef, PythonDictionary const&)
 #9 ScriptedPythonInterface::CreatePluginObject<...>(...)
```

`ResolveNameWithDictionary` builds a `PythonString` from the class name
and looks it up in the module dictionary.  When the name is not valid
UTF-8, `PyUnicode_FromStringAndSize` returns `NULL`, so the
`PythonString` is left with `m_py_obj == NULL`.  `GetItemForKey` forwards
that object to `GetItem`, which passed the `NULL` `PyObject *` straight
to `PyDict_GetItemWithError`.

Debugging the crashing lldb under lldb shows the `NULL` key arriving in
`GetItem`:

```
(lldb) break set -n lldb_private::python::PythonDictionary::GetItem
(lldb) run --no-use-colors -b -s /tmp/bp_cmds.txt
* stop reason = breakpoint 1.1
(lldb) up
(lldb) frame variable key.m_py_obj
(PyObject *) key.m_py_obj = nullptr
```

Continuing lands on the dereference of that `NULL` inside CPython:

```
(lldb) continue
* stop reason = EXC_BAD_ACCESS (code=1, address=0x8)
    frame #0: PyDict_GetItemWithError + 40
->  ldr    x8, [x1, #0x8]
```

`x1` holds the key argument, which is `NULL`, so reading the type field
at offset `0x8` faults at address `0x8`.

`GetItem` already guards against an invalid dictionary (`!IsValid()`);
extend that guard to reject an invalid key as well and return
`nullDeref()`.  `GetItemForKey` consumes the error and returns an empty
`PythonObject`, so existing callers such as `ResolveNameWithDictionary`
keep working and the breakpoint command now fails cleanly instead of
crashing.

This was found by `lldb-commandinterpreter-fuzzer`.

Adds `PythonDataObjectsTest.TestPythonDictionaryGetItemWithInvalidKey`,
which feeds an invalid-UTF-8 key to `GetItem`.  Without the fix the test
dereferences the `NULL` key and crashes (`SIGSEGV`).
---
 .../Python/PythonDataObjects.cpp              |  2 +-
 .../Python/PythonDataObjectsTests.cpp         | 24 +++++++++++++++++++
 2 files changed, 25 insertions(+), 1 deletion(-)

diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp 
b/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp
index 800f991680c85..fba98abf9e83b 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp
@@ -697,7 +697,7 @@ PythonObject PythonDictionary::GetItemForKey(const 
PythonObject &key) const {
 
 Expected<PythonObject>
 PythonDictionary::GetItem(const PythonObject &key) const {
-  if (!IsValid())
+  if (!IsValid() || !key.IsValid())
     return nullDeref();
   PyObject *o = PyDict_GetItemWithError(m_py_obj, key.get());
   if (PyErr_Occurred())
diff --git a/lldb/unittests/ScriptInterpreter/Python/PythonDataObjectsTests.cpp 
b/lldb/unittests/ScriptInterpreter/Python/PythonDataObjectsTests.cpp
index 0d4b04b7a1284..24ed721049e67 100644
--- a/lldb/unittests/ScriptInterpreter/Python/PythonDataObjectsTests.cpp
+++ b/lldb/unittests/ScriptInterpreter/Python/PythonDataObjectsTests.cpp
@@ -490,6 +490,30 @@ TEST_F(PythonDataObjectsTest, 
TestPythonDictionaryValueEquality) {
   EXPECT_EQ(value_1, chk_str.GetString());
 }
 
+TEST_F(PythonDataObjectsTest, TestPythonDictionaryGetItemWithInvalidKey) {
+  // A PythonString constructed from invalid UTF-8 silently fails (the
+  // PyUnicode_FromStringAndSize call returns NULL) and leaves m_py_obj == 
NULL.
+  // PythonDictionary::GetItem(const PythonObject&) must not pass that NULL key
+  // through to PyDict_GetItemWithError, which would then dereference it and
+  // crash.
+  llvm::StringRef invalid_utf8("\xd0p", 2);
+  PythonString invalid_key(invalid_utf8);
+  EXPECT_FALSE(invalid_key.IsValid());
+
+  PythonDictionary dict(PyInitialValue::Empty);
+  dict.SetItemForKey(PythonString("real_key"), PythonInteger(42));
+
+  // GetItem must return an error (not crash) when given an invalid key.
+  llvm::Expected<PythonObject> item = dict.GetItem(invalid_key);
+  EXPECT_FALSE(static_cast<bool>(item));
+  llvm::consumeError(item.takeError());
+
+  // GetItemForKey wraps GetItem, consumes any error, and returns an empty
+  // PythonObject.  It must not crash either.
+  PythonObject result = dict.GetItemForKey(invalid_key);
+  EXPECT_FALSE(result.IsAllocated());
+}
+
 TEST_F(PythonDataObjectsTest, TestPythonDictionaryManipulation) {
   // Test that manipulation of a dictionary behaves correctly when wrapped
   // by a PythonDictionary.

>From 25ecf86e9c8b3f76727cac021de92ab0ff426b2d Mon Sep 17 00:00:00 2001
From: Yao Qi <[email protected]>
Date: Mon, 29 Jun 2026 11:21:43 +0100
Subject: [PATCH 2/2] [lldb] Add API test for invalid-UTF-8 scripted process
 class name

Follow-up to the `PythonDictionary::GetItem` NULL-key fix, adding the
requested API integration test in `lldb/test/API` for the invalid-UTF-8
class name crash.

Why the test is written this way
--------------------------------

The crash is reached only when a class name that is not valid UTF-8
arrives at `PythonObject::ResolveNameWithDictionary`, which builds a
`PythonString` from it.  `PyUnicode_FromStringAndSize` then returns
`NULL`,mthe `PythonString` is left invalid, and (before the fix)
`GetItem` dereferenced that `NULL` key.

To exercise this from an API test the malformed bytes must be delivered
to lldb verbatim.  Every existing entry point that takes a scripted
class name, including the breakpoint resolver path
(`SBTarget::BreakpointCreateFromScript` / "breakpoint set -P") and
`SBLaunchInfo::SetScriptedProcessClassName(const char *)`, accepts the
name as a NUL-terminated C string.  The Python bindings encode the
incoming Python str to UTF-8 at the SWIG boundary, so an invalid
sequence fails to encode and never reaches lldb.  A normal breakpoint
(or any other) API test therefore cannot reproduce the crash: the bad
bytes are rejected by the bindings before they get anywhere near
GetItem.  The only way they previously reached the C++ layer was through
the command line (e.g. "breakpoint set -P '\xd0p'"), which is why a
direct API test was not previously possible without spawning lldb as a
subprocess.

To make the path testable through the SB API, add an overload
```
  SBLaunchInfo::SetScriptedProcessClassName(const char *src, size_t len)
```
that accepts a Python bytes object and preserves the exact bytes
(including invalid UTF-8 and embedded NULs).  `ScriptedMetadata` already
stores the class name as a byte-preserving std::string, so no other
layer needs to change.

SWIG note: the existing (const char *src, size_t src_len) in-typemap
already accepts bytes, but SWIG selects an overload using the typecheck
typemap, not the in-typemap.  Without a matching typecheck, the new
overload rejects a bytes argument.  Add a typecheck for
(const char *src, size_t src_len) that accepts str/bytes/bytearray so
the overload dispatches correctly.  The only overloaded user of that
signature is the new method; non-overloaded users (e.g.
SBProcess::PutSTDIN) are unaffected because they generate no dispatcher.

The test creates a scripted process whose class name is the invalid
sequence b"\xd0p" and asserts the launch fails cleanly.  Against the
unfixed code the launch crashes in `PythonDictionary::GetItem;` with the
fix it returns a clean error.
---
 lldb/bindings/python/python-typemaps.swig     | 17 +++++++++
 lldb/include/lldb/API/SBLaunchInfo.h          |  9 +++++
 lldb/source/API/SBLaunchInfo.cpp              | 11 ++++++
 .../TestScriptedProcessInvalidClassName.py    | 37 +++++++++++++++++++
 4 files changed, 74 insertions(+)
 create mode 100644 
lldb/test/API/functionalities/scripted_process/TestScriptedProcessInvalidClassName.py

diff --git a/lldb/bindings/python/python-typemaps.swig 
b/lldb/bindings/python/python-typemaps.swig
index 072e688c4bde1..19ab7a1822895 100644
--- a/lldb/bindings/python/python-typemaps.swig
+++ b/lldb/bindings/python/python-typemaps.swig
@@ -272,6 +272,23 @@ AND call SWIG_fail at the same time, because it will 
result in a double free.
     SWIG_fail;
   }
 }
+
+// Overload dispatch typecheck for (const char *src, size_t src_len).
+//
+// The %typemap(in) above already accepts str/bytes/bytearray, but SWIG's
+// generated overload dispatcher selects a candidate using the typecheck
+// typemap, not the in typemap.  Without this, an overloaded method taking
+// (const char *src, size_t src_len) (e.g. SBLaunchInfo::SetScriptedProcess
+// ClassName) would reject a Python bytes argument because the default char*
+// typecheck only matches str.  Accept the same input kinds as the in typemap.
+%typemap(typecheck, precedence=SWIG_TYPECHECK_STRING) (const char *src,
+                                                       size_t src_len) {
+  $1 = (PyUnicode_Check($input) || PyBytes_Check($input) ||
+        PyByteArray_Check($input))
+           ? 1
+           : 0;
+}
+
 // For SBProcess::WriteMemory, SBTarget::GetInstructions and 
SBDebugger::DispatchInput.
 %typemap(in) (const void *buf, size_t size),
              (const void *data, size_t data_len),
diff --git a/lldb/include/lldb/API/SBLaunchInfo.h 
b/lldb/include/lldb/API/SBLaunchInfo.h
index 06e72efc30f9f..355cb119a6951 100644
--- a/lldb/include/lldb/API/SBLaunchInfo.h
+++ b/lldb/include/lldb/API/SBLaunchInfo.h
@@ -202,6 +202,15 @@ class LLDB_API SBLaunchInfo {
 
   void SetScriptedProcessClassName(const char *class_name);
 
+  /// Set the scripted process class name from a buffer of \a src_len bytes.
+  ///
+  /// Unlike the NUL-terminated overload, this preserves the exact bytes of
+  /// \a src, including embedded NULs and byte sequences that are not valid
+  /// UTF-8.  It exists so that tests can exercise the class-name resolution
+  /// path with arbitrary bytes; the NUL-terminated overload cannot carry such
+  /// input across the scripting-language bindings.
+  void SetScriptedProcessClassName(const char *src, size_t src_len);
+
   lldb::SBStructuredData GetScriptedProcessDictionary() const;
 
   void SetScriptedProcessDictionary(lldb::SBStructuredData dict);
diff --git a/lldb/source/API/SBLaunchInfo.cpp b/lldb/source/API/SBLaunchInfo.cpp
index 572590f4cae76..46858786054fd 100644
--- a/lldb/source/API/SBLaunchInfo.cpp
+++ b/lldb/source/API/SBLaunchInfo.cpp
@@ -355,6 +355,17 @@ void SBLaunchInfo::SetScriptedProcessClassName(const char 
*class_name) {
   m_opaque_sp->SetScriptedMetadata(metadata_sp);
 }
 
+void SBLaunchInfo::SetScriptedProcessClassName(const char *src,
+                                               size_t src_len) {
+  LLDB_INSTRUMENT_VA(this, src, src_len);
+  ScriptedMetadataSP metadata_sp = m_opaque_sp->GetScriptedMetadata();
+  StructuredData::DictionarySP dict_sp =
+      metadata_sp ? metadata_sp->GetArgsSP() : nullptr;
+  metadata_sp = std::make_shared<ScriptedMetadata>(
+      llvm::StringRef(src, src_len), dict_sp);
+  m_opaque_sp->SetScriptedMetadata(metadata_sp);
+}
+
 lldb::SBStructuredData SBLaunchInfo::GetScriptedProcessDictionary() const {
   LLDB_INSTRUMENT_VA(this);
 
diff --git 
a/lldb/test/API/functionalities/scripted_process/TestScriptedProcessInvalidClassName.py
 
b/lldb/test/API/functionalities/scripted_process/TestScriptedProcessInvalidClassName.py
new file mode 100644
index 0000000000000..90cb198400643
--- /dev/null
+++ 
b/lldb/test/API/functionalities/scripted_process/TestScriptedProcessInvalidClassName.py
@@ -0,0 +1,37 @@
+"""
+Test that creating a scripted process with an invalid-UTF-8 class name fails
+cleanly instead of crashing lldb.
+"""
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbtest_config
+
+
+class ScriptedProcessInvalidClassNameTestCase(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def test_invalid_utf8_class_name(self):
+        """A scripted process class name that is not valid UTF-8 must produce a
+        clean error, not crash lldb.
+
+        ``b"\xd0p"`` is an incomplete two-byte UTF-8 sequence.  When used as a
+        scripted process class name it reaches PyUnicode_FromStringAndSize,
+        which returns NULL; PythonDictionary::GetItem then dereferenced that
+        NULL key and crashed with EXC_BAD_ACCESS.
+        """
+        target = self.dbg.CreateTarget(lldbtest_config.lldbExec)
+        self.assertTrue(target.IsValid(), VALID_TARGET)
+
+        launch_info = lldb.SBLaunchInfo(None)
+        launch_info.SetProcessPluginName("ScriptedProcess")
+        launch_info.SetScriptedProcessClassName(b"\xd0p")
+
+        error = lldb.SBError()
+        process = target.Launch(launch_info, error)
+
+        # The launch must fail cleanly rather than crash.  Reaching this
+        # assertion at all means lldb survived the invalid class name.
+        self.assertTrue(error.Fail(), "launch should fail, not crash")
+        self.assertFalse(process.IsValid())

_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to