Summary
The lean (non-Python) FLOWS_TO walk models direct call sinks only (IO_SINKS). It never consumes the handle-constructor / handle-method tables (IO_LEAN_HANDLE_CONSTRUCTORS, IO_LEAN_HANDLE_METHODS) — those feed only the io_access READS_FROM/WRITES_TO walk. As a result, a taint that reaches a resource through a handle method (os.Create(p).Write(x), io.open(p):write(x), File::create(p).write(x), fs.createWriteStream(p).write(x), …) emits no FLOWS_TO edge, while the module is still stamped flow_covered = True.
Under the three-verdict model that means a real ENV→FILE leak is certified as NO_FLOW (verified absence) instead of the honest UNKNOWN.
This is uniform across every lean language, not language-specific
Verified end-to-end with the real GraphUpdater (IO capture on):
| Program |
flow_covered |
ENV→FILE FLOWS_TO |
Go, direct os.WriteFile(p, []byte(s)) |
True |
✅ emitted |
Go, handle f,_ := os.Create(p); f.Write([]byte(s)) |
True |
❌ none → false NO_FLOW |
Lua, handle local f = io.open(p,"w"); f:write(s) |
True |
❌ none → false NO_FLOW |
Go (an established covered language) and Lua behave identically for the handle idiom. The boundary is direct-sink vs handle-method, and it is the same for Go, Rust, Java, JS/TS, C#, C, C++, and Lua.
Why this matters more for some languages
Languages with a direct one-shot file sink (Go os.WriteFile, Rust std::fs::write) have a modeled path for the common case; the gap only bites the handle idiom. Lua has no direct file-write sink — every Lua file write is handle-based — so Lua coverage is the starkest instance, though not a different bug.
Options
- Teach the FLOWS_TO lean walk to track handle bindings + handle-method sinks (reuse the io_access handle tables), so tainted
handle.write(x) emits a FLOWS_TO edge to the handle's resource. Cross-language (also fixes Go/Rust/etc.).
- Or make
flow_covered granular enough that a module containing an unmodeled handle write is reported as an UNKNOWN coverage gap rather than NO_FLOW.
Option 1 is the sound, parity-preserving fix.
Repro
Standalone script comparing the three programs above against the real GraphUpdater is available; it reproduces the table deterministically.
Surfaced by Greptile review on #1175 (Lua flow coverage); recorded here as the cross-cutting follow-up since it is not Lua-specific.
Summary
The lean (non-Python) FLOWS_TO walk models direct call sinks only (
IO_SINKS). It never consumes the handle-constructor / handle-method tables (IO_LEAN_HANDLE_CONSTRUCTORS,IO_LEAN_HANDLE_METHODS) — those feed only the io_access READS_FROM/WRITES_TO walk. As a result, a taint that reaches a resource through a handle method (os.Create(p).Write(x),io.open(p):write(x),File::create(p).write(x),fs.createWriteStream(p).write(x), …) emits no FLOWS_TO edge, while the module is still stampedflow_covered = True.Under the three-verdict model that means a real ENV→FILE leak is certified as
NO_FLOW(verified absence) instead of the honestUNKNOWN.This is uniform across every lean language, not language-specific
Verified end-to-end with the real
GraphUpdater(IO capture on):os.WriteFile(p, []byte(s))f,_ := os.Create(p); f.Write([]byte(s))local f = io.open(p,"w"); f:write(s)Go (an established covered language) and Lua behave identically for the handle idiom. The boundary is direct-sink vs handle-method, and it is the same for Go, Rust, Java, JS/TS, C#, C, C++, and Lua.
Why this matters more for some languages
Languages with a direct one-shot file sink (Go
os.WriteFile, Ruststd::fs::write) have a modeled path for the common case; the gap only bites the handle idiom. Lua has no direct file-write sink — every Lua file write is handle-based — so Lua coverage is the starkest instance, though not a different bug.Options
handle.write(x)emits a FLOWS_TO edge to the handle's resource. Cross-language (also fixes Go/Rust/etc.).flow_coveredgranular enough that a module containing an unmodeled handle write is reported as an UNKNOWN coverage gap rather than NO_FLOW.Option 1 is the sound, parity-preserving fix.
Repro
Standalone script comparing the three programs above against the real GraphUpdater is available; it reproduces the table deterministically.
Surfaced by Greptile review on #1175 (Lua flow coverage); recorded here as the cross-cutting follow-up since it is not Lua-specific.