Pysa Model Debugger
Overview
Systematic workflow that identifies exactly where and why taint flow is lost by comparing two Pysa result directories. Accepts static analysis issue URLs (e.g., https://www.internalfb.com/security/static_analysis/issue/<issue_instance_id>?database=<database>).
REQUIRED BACKGROUND: Load the pysa-json-models skill to understand trace element syntax (ports, call info, kinds).
When to Use
- User reports a Pysa false negative (issue found in run A, missing in run B)
- User provides a static analysis issue URL
Prerequisites
The user must provide:
- An issue URL:
https://www.internalfb.com/security/static_analysis/issue/<issue_instance_id>?database=<database> - Two Pysa output directories: one where the issue IS found (FOUND), one where it is NOT (NOT-FOUND)
Workflow
Step 1: Extract Issue Metadata
From the URL, extract:
- Issue instance ID: the numeric ID in the path (e.g.,
216172782209137158) - Database: the
databasequery parameter (e.g.,xdb.pysa-instagram-sharded.1)
Get the issue handle:
db <database> -e "SELECT handle FROM issues WHERE id=(SELECT issue_id FROM issue_instances WHERE id=<issue_instance_id>);"
If the query fails, ask the user for help.
Step 2: Parse the Handle
Example handle:
accounts.service.Service.async_is_phone_suspicious:5120:0:Call|accounts.service.Service._async_helper|0|f:ec82abb59a9d0e207fe4f5acc361f0ad
Extract:
- Root callable: everything before the first
:(e.g.,accounts.service.Service.async_is_phone_suspicious) - Issue code: the number after the first
:(e.g.,5120)
Step 3: Explorer Tool
All commands below use buck run to invoke the explorer. The shorthand <explorer> means:
buck run fbcode//tools/pyre/tools/pysa_model_explorer_cli:pysa_model_explorer --
Run <explorer> --help for usage details.
Suppressing build noise: Always append 2>/dev/null to explorer commands to suppress buck build output that clutters the results. Only retry without 2>/dev/null if the command produces empty or unexpected output, so you can see the actual error message from buck.
Step 4: Verify the Issue
Confirm the issue exists in FOUND:
<explorer> /tmp/FOUND get-issues <root-callable> --handle <handle>
If not found, ask the user for help.
Confirm it does NOT exist (or is very different) in NOT-FOUND:
<explorer> /tmp/NOT-FOUND get-issues <root-callable> --code <code>
The result should be empty or contain very different issues (different locations).
Step 5: Investigate Where Taint Is Lost
Check these options in order: A → B → C.
Option A: Source Trace (Forward)
In the issue from FOUND, examine "traces" → entries with "name": "forward".
Each root (trace element) may have multiple kinds with different "length" values. Pick the root whose minimum "length" across its kinds is smallest (missing length = 0, which is always shortest).
If it's an origin: the source comes from a user-annotated function. Check if at least one of the leaves have a model in NOT-FOUND:
<explorer> /tmp/NOT-FOUND get-model <leaf-callable> --show sources --kind <kind>
If none of the leaves have source taint in NOT-FOUND, taint is lost at one of these leaf callables. If at least one does, the source trace is intact — move to Option B.
If it's a call with "resolves_to": [<callee-callable>, ...]: check if those callables have source taint in NOT-FOUND. If there are multiple entries (overrides), check all of them — taint is lost only if ALL are missing:
<explorer> /tmp/NOT-FOUND get-model <callee-callable> --show sources --kind <kind>
Interpret the results:
- No entries → taint is lost here or deeper. Keep diving into that callable's model in FOUND and repeat.
- Entries with wrong ports (e.g., issue has
"port": "formal(a)"in the"call"section, but model has sources for"formal(b)") → these don't count. Taint is still lost. - Matching entries found → source trace is intact. Move to Option B.
When you find a missing source, double-check it exists in FOUND:
<explorer> /tmp/FOUND get-model <leaf-or-callee-callable> --show sources --kind <kind>
Option B: Sink Trace (Backward)
Same process as Option A, but for "name": "backward" traces. Use --show sinks instead of --show sources with get-model.
Diving Deeper (Options A/B)
When Option A or B identifies a callee whose model is present in FOUND but missing in NOT-FOUND, you need to find exactly which function in the call chain lost the taint. Recurse into that callee's model:
- Examine the callee's model in FOUND:
# For a missing source (Option A):
<explorer> /tmp/FOUND get-model <first-source-callee> --show sources --kind <kind>
# For a missing sink (Option B):
<explorer> /tmp/FOUND get-model <first-sink-callee> --show sinks --kind <kind>
Pick the frame with the shortest length (same strategy as in Option A/B).
Identify the next callee: if the frame is a
"call", use"resolves_to"to get the callee. If it's an"origin", use the leaf names.Check the next callee's model in NOT-FOUND:
<explorer> /tmp/NOT-FOUND get-model <next-callee> --show sources --kind <kind>
- Interpret:
- Model exists in NOT-FOUND → taint is lost within the previous callee (e.g.,
<first-source-callee>). Use the Option C strategy on that callable: read its source code, check its call graph and TITO models to find the break. - Model missing in NOT-FOUND → taint is lost deeper. Recurse: go back to step 1 with the next callee.
- Model exists in NOT-FOUND → taint is lost within the previous callee (e.g.,
Continue until you find the function where taint is present in its callees but lost in its own model. Then apply Option C's strategy (call graph + TITO checks) to that function to pinpoint the root cause.
Option C: Taint Lost in Root Callable
If both forward and backward traces are correct, taint is lost within the root callable itself.
Read the source code using
"filename"and location from the issue. Filenames are usually relative to~/fbsource.Check the call graph:
<explorer> /tmp/NOT-FOUND get-call-graph <root-callable>
If you know the relevant line numbers (e.g., from the issue location or source code), use --start-line and --end-line to filter to that range:
<explorer> /tmp/NOT-FOUND get-call-graph <root-callable> --start-line <line> --end-line <line>
This avoids noise from irrelevant call graph edges in large functions.
Verify calls to source and sink are present at the right locations.
- Check intermediate calls. For flows like:
a = source()
b = foo(a)
sink(b)
foo is an intermediate call. Check:
- Is
fooresolved in the call graph? - If resolved (say, to
my_module.foo), check its TITO model:
<explorer> /tmp/NOT-FOUND get-model my_module.foo --show tito
- Look at propagations. If TITO is missing, keep diving deeper: investigate
foo's call graph and models to find where propagation breaks.
Step 6: Produce Summary
Your output must include:
- Issue description: root callable, code, handle, filename
- Path to the lost taint: chain of callables from root to where taint is lost (include all callable names and ports)
- Which trace: source (forward) or sink (backward)
- Root cause: why taint is lost and where exactly
Quick Reference: Explorer Commands
| Command | Purpose |
|---|---|
<explorer> <dir> get-issues <callable> --handle <handle> |
Find specific issue |
<explorer> <dir> get-issues <callable> --code <code> |
Find issues by code |
<explorer> <dir> get-issues <callable> --show-leaf-names |
Show leaf callables |
<explorer> <dir> get-model <callable> --show sources --kind <kind> |
Check source taint |
<explorer> <dir> get-model <callable> --show sinks --kind <kind> |
Check sink taint |
<explorer> <dir> get-model <callable> --show tito |
Check TITO propagation |
<explorer> <dir> get-call-graph <callable> |
Check call resolution (--start-line, --end-line to filter by line range) |
<explorer> <dir> get-overrides <callable> |
List all overrides for a method |
<explorer> <dir> search <regex> |
Search for callables |
<explorer> is shorthand for buck run fbcode//tools/pyre/tools/pysa_model_explorer_cli:pysa_model_explorer --.
Additional flags: --show-features, --show-tito-positions, --show-class-intervals, --format text.
Common Mistakes
| Mistake | Fix |
|---|---|
| Searching the wrong trace first | Always check forward (A) → backward (B) → local (C) in order |
| Ignoring port mismatches | formal(a) ≠ formal(b) — matching kind with wrong port means taint is still lost |
| Not cross-checking against FOUND | Always confirm the model exists in FOUND before concluding it's missing in NOT-FOUND |
| Skipping call graph check | Unresolved calls are a common root cause for lost taint |
| Stopping at first missing model | The missing model might itself be caused by a deeper missing propagation — keep diving |
Checking only one resolves_to entry |
When there are multiple entries (overrides), check ALL of them — taint flows if any has the model |
Using get-model on Overrides{module.Class.foo} |
Override targets don't have models. Use get-overrides module.Class.foo to list all overrides, then get-model on each override |
| Using grep/jq on raw JSON files | Always use pysa_model_explorer — it handles indexing and filtering efficiently |
Forgetting --show-leaf-names on origins |
Without this flag, you can't see which callables originated the taint |
Forgetting 2>/dev/null on explorer commands |
Buck build output clutters results — always append 2>/dev/null and only remove it to diagnose errors |
| Using knowledge_load | Avoid using knowledge_load on the given issue URL, prefer local results |