AI Pentesting, Part 12: LLDB and AI: Runtime Evidence Without Uncontrolled Execution
A debugger can observe a process, change it, and execute code inside it. AI-assisted debugging is therefore not automatically read-only. Use a known binary, explicit session identity, bounded memory reads, and human control over launch, continuation, expression evaluation, and attachment.
Lab boundary: written authorisation, synthetic identities and data, isolated targets, bounded requests, and human approval for active actions. Examples are instructional fixtures, not claims of testing a live organisation. Tool inventory reviewed 2026-10-28; compatibility and availability can change.
1. LLDB's native interfaces and community MCPs
LLDB provides a command interface, scripting APIs, register/memory inspection, disassembly, breakpoints, watchpoints, and post-mortem analysis. These are native debugger capabilities, not a built-in promise of safe AI automation. stass/lldb-mcp is a community bridge reviewed for this article. Its README describes session management, launch and attach, core dumps, execution control, inspection, arbitrary LLDB commands, and expression evaluation.
The bridge is not an official LLVM security boundary. Its documented generic-command and expression tools can defeat a narrowly intended inspection session. Review exact source, maintenance, dependencies, LLDB/Python compatibility, and client configuration before use. Do not copy a minimum Python version from an older README without checking the current MCP SDK's requirements.
| Operation | Risk class | Initial policy |
|---|---|---|
| Backtrace / frame variables | Reads potentially sensitive process state | Allowed for the selected lab process with redaction |
| Bounded memory read | May expose secrets or large data | Exact approved address and maximum size |
| Run / continue / step | Executes target instructions | Human controlled in an isolated lab |
| Expression evaluation | May call functions or alter state | Not exposed to the AI |
| Attach / arbitrary command | Broad process-control boundary | Denied outside an explicit approved workflow |
2. Prepare a deterministic session
Use the Part 11 lab_parser executable and its known source. Record its hash, compiler settings, OS, architecture, and LLDB build. Run without production credentials, mounted sensitive directories, or unrestricted network access. The executable itself is benign; an arbitrary untrusted binary needs stronger sandboxing and malware-handling procedures.
Debug permissions differ by OS. Use the platform's documented development workflow and signed binaries where required. Do not disable system integrity protections or request elevated access simply because attachment fails. Analyse a process you launched for the lab, not a production browser or security service.
lldb ./lab_parser
(lldb) breakpoint set --name parse_record
(lldb) run
(lldb) thread backtrace
(lldb) frame variable
(lldb) disassemble --name parse_recordThese are interactive LLDB commands entered one at a time. run executes the program; it is not passive inspection. At the breakpoint, check whether you stopped before local variables were initialised. A compiler's debug location can point into a function prologue, so not every displayed local is meaningful yet.
3. Inspect the length calculation safely
At a valid stopped location, inspect arguments and the two header bytes. Copy the actual header pointer from the debugger display into the memory command; do not use an address guessed by the model. The bracketed address below is a placeholder, not a literal command argument.
(lldb) frame variable header available capacity
(lldb) memory read --size 1 --format x --count 2 <actual-header-address>
(lldb) next
(lldb) frame variable length
(lldb) register read
(lldb) thread backtracenext also executes instructions and may cross source lines differently after optimisation. Expected fixture values are bytes 09 00, available length 2, capacity 8, and decoded length 9 once initialised. The function returns -2. Verify return behaviour through controlled stepping or the program's output; do not assume one architecture's return register on another ABI.
Ask AI to reconcile the observed bytes with the static decode from Part 11. If it claims the length is 2304, require a byte-order explanation. Use deterministic arithmetic or debugger formatting for number conversion. A fluent explanation should not override the actual bytes.
4. Restrict a debugger bridge by implementation
A client-side instruction saying 'read only' is insufficient if the server still exposes lldb_command or expression evaluation. Disable or remove broad tools at the server or put a reviewed broker in front of them. Allow explicit inspection operations against one known session, not arbitrary strings passed to the debugger.
{
"session": "lab-parser-session",
"allowed_operations": ["backtrace", "frame_variables", "disassembly"],
"memory_read_max_bytes": 64,
"allow_expression_evaluation": false,
"allow_arbitrary_commands": false,
"allow_attach": false,
"execution_control": "human_only"
}This is a proposed policy document, not a claim that the community bridge accepts these exact configuration keys. Enforce it in your adapter and test rejection. Include session ID, process ID, executable hash, stop reason, thread ID, and frame ID in every evidence record. A stale tool call must not inspect a newly reused process ID.
For cancellation, ensure the adapter stops delivering results from an old session after the client disconnects. Avoid logging entire memory buffers or environment blocks into model traces.
5. Core dumps, optimisation, and misleading observations
Core dumps and minidumps may contain passwords, tokens, customer records, and cryptographic material. Apply access control, retention, redaction, and approved model residency. A local model reduces external transmission but does not automatically secure its logs, caches, or tool process.
Example one: an optimised local is 'unavailable'. Report missing debug information, not a zero value. Example two: a backtrace omits an inlined function. Check inline metadata and disassembly before declaring the function never ran. Example three: memory read fails because the address is unmapped or the process exited. That is a failed observation, not proof the buffer was empty.
Watchpoints can help observe a known location, but hardware limits and debugger side effects matter. Conditional breakpoints may evaluate expressions, and stepping changes timing. A debugger trace of one run cannot prove a race condition is absent. Use targeted tests and repeated controlled observations for concurrency questions.
6. Produce a runtime evidence packet
Record the exact input, executable hash, launch arguments, debugger version, breakpoint addresses, stop reasons, selected thread/frame, bounded memory observations, and final output. Separate analyst commands from model suggestions. Include unavailable observations explicitly and avoid screenshots as the sole evidence when a text transcript can preserve details.
Static claim: header bytes form a little-endian length.
Runtime evidence: 09 00 observed at the actual header address.
State: available=2, capacity=8, length=9 after initialisation.
Outcome: controlled fixture returns -2.
Not demonstrated: memory corruption, exploitability, or caller safety.
Next check: compare optimised and unoptimised builds with the same input.End the process, terminate the bridge, remove temporary core dumps according to policy, and preserve the approved evidence bundle. Part 13 compares these observations with radare2 and Binary Ninja's alternative analysis representations.
7. Step-by-Step Lab: Turn a Static Parser Claim into a Runtime Trace
Prerequisites: The exact Part 11 lab binary, source/debug symbols, a compatible LLDB environment, and permission to launch your own lab process. Do not attach to production services or disable platform protections to make debugging convenient. Core dumps require separate sensitive-data handling.
- Record the executable hash and launch arguments. Open it with LLDB and set a named breakpoint on parse_record using the interactive commands earlier in this article. A launch is an active execution step and remains operator-controlled.
- Run the fixture and confirm the stopped process, thread, frame, executable, and stop reason. If the breakpoint is still in the prologue, step only far enough for the values under review to become meaningful.
- Inspect header, available, and capacity using frame-variable inspection. Read only the two actual header bytes from the pointer LLDB reports; never use an address guessed by the assistant.
- Advance deliberately to the decode and inspect length. Compare 09 00 with the expected little-endian value 9, then confirm the rejecting branch and final -2 output.
- Normalise a captured instruction address against its real module base before comparing traces across runs. Use the offline arithmetic fixture below to verify the representation, not as a substitute for the runtime module map.
- Repeat with the capacity-equal fixture and verify success. For an optimised build, record unavailable variables and changed source stepping without inventing their values.
- Terminate the process and debugger bridge. Preserve the bounded transcript, remove unnecessary dumps according to policy, and ask a second reviewer to reproduce the conclusion from the same binary and input.
const observations = [
{ base: 0x10000000, address: 0x10000120, size: 0x2000 },
{ base: 0x20000000, address: 0x20000120, size: 0x2000 }
];
const relative = observations.map(item => {
if (item.address < item.base || item.address >= item.base + item.size) throw Error("outside module");
return item.address - item.base;
});
if (relative.some(value => value !== 0x120)) throw Error("relative address mismatch");
console.log(JSON.stringify(relative.map(value => "0x" + value.toString(16))));Expected result: Both synthetic runtime addresses map to relative offset 0x120. This fixture uses small exactly representable numeric addresses; use the debugger or width-correct integer tooling for real 64-bit addresses. Matching relative offsets is meaningful only when module identity and build match.
8. Worked Examples and Expected Outcomes
Example A: a local is unavailable
Optimisation removed the expected variable or debug information does not describe the current location. Record unavailable and inspect instructions or a different controlled build. An absent debugger value is not zero.
Example B: expression evaluation looks read-only
A proposed 'print' operation calls a method in the debuggee. That may execute code, allocate memory, or alter state. Keep expression evaluation disabled for the AI role and prefer bounded debugger-native inspection approved for the exact context.
Example C: a trace misses a race
The issue disappears while stepping. A debugger changes timing, and one stopped run cannot establish absence of concurrency defects. Use an owned, bounded concurrency test with appropriate instrumentation and retain the timing limitation.
Runtime record:
binary hash + process/session identity
thread + frame + stop reason
actual pointer + bounded bytes read
source/instruction location + initialisation state
input fixture + final result + unavailable observations9. Troubleshooting, Retesting, and Completion Evidence
| Symptom | Likely cause | Discriminating check |
|---|---|---|
| Breakpoint never triggers | Wrong executable, symbol, or code path | Confirm module hash, loaded symbols, and actual call path |
| Address points elsewhere after restart | ASLR or module-base assumptions changed | Reacquire the runtime map and compare correct relative offsets |
| AI asks to attach to another process | A local hypothesis expanded into broader authority | Reject the request and keep the approved session binding |
Retest and completion: Retain both rejecting and accepting traces, actual memory bounds, debugger/tool versions, and the static-to-runtime comparison. Do not claim a missing event or failed read proves safe memory. A real core dump or memory capture must remain outside public model prompts unless expressly approved and sanitised.
10. Real Tool Workflow: Inspect the Known Parser with Native LLDB Commands
Prerequisites: LLDB with support for the platform/architecture, the exact known lab_parser build and symbols from Part 11, and permission to launch your own isolated process. Use a POSIX-style shell for the launcher below or the platform's documented executable path on Windows. Do not attach to an unrelated service.
Use reviewed releases and record the versions actually installed. Command references for this workflow were reviewed on 29 October 2026; product support and flags can change. Instructions and output shapes are not claims of a live assessment.
| Tool | Role | Important boundary |
|---|---|---|
| LLDB | Launch a known fixture and inspect stopped state | Run, next, continue, and expression evaluation are active operations |
| Native frame/disassembly commands | Read a bounded claim-specific view | Unavailable debug values are not zero and object-printing can call code |
| Optional reviewed MCP adapter | Expose a narrow set of inspection operations | Native/community bridge availability varies; generic debugger commands defeat a read-only role |
Step-by-Step Tool Operation
- Confirm LLDB version, executable hash, architecture, debug symbols, and exact input. Review debugger startup files; the launch example disables user initialization loading for this controlled session.
- Create a named breakpoint on parse_record and inspect breakpoint list. A pending breakpoint with zero locations is not proof the function cannot execute; verify symbols and binary identity.
- Run only the known fixture. Confirm process, thread, frame, and stop reason. Use process status and thread backtrace before interpreting locals.
- Inspect available, capacity, and the two header bytes using frame variable. Step deliberately past initialisation before reading length. The command examples are entered one at a time; do not assume one next always reaches the same source location on every build.
- Compare disassembly with the little-endian fixture and observe the rejecting path/final result. If actual memory reads are needed, use only the real pointer reported for this stopped frame with a two-byte bound.
- Exit the known process through the intended control flow or terminate this exact lab process through LLDB, save the bounded transcript, and quit. Preserve unavailable/ambiguous observations and remove any sensitive dump files according to policy.
lldb --version
lldb --no-lldbinit ./lab_parser
(lldb) breakpoint set --name parse_record
(lldb) breakpoint list
(lldb) run
(lldb) process status
(lldb) thread backtrace
(lldb) frame variable available capacity
(lldb) frame variable 'header[0]' 'header[1]'
(lldb) disassemble --name parse_record
(lldb) next
(lldb) frame variable length
(lldb) quitExpected Evidence and Interpretation
For the unmodified fixture, expect header bytes 9 and 0, available=2, capacity=8, and decoded length=9 once initialised. The program's rejecting result is -2. These are source-grounded expectations, not a captured trace from your installed debugger. Do not claim memory corruption: the function shown performs no copy.
LLVM documentation also describes MCP integration for compatible releases; inspect the installed binary and matching documentation rather than assuming any LLDB build includes the same server. Whether native or community, deny generic command and expression tools to a model intended only to inspect a selected lab session. Use explicit session, thread, and frame identity in evidence.
Troubleshooting and Cleanup
If the process cannot launch because of OS debug policy, follow the approved development procedure; do not disable system integrity protections. If variables are unavailable, record that and use the correct instruction/debug location. If quit asks to terminate the current lab process, confirm only after checking its identity. Do not bulk-kill debugger or application processes on the host.