fix(native): retain all breakpoint locations within budget (#7975)

* fix(native): retain all breakpoint locations within budget

* fix(native): describe incomplete locations accurately

* test(native): cover incomplete location warning

* test(native): bind lipo failure to a real fixture

* test(native): use valid Mach-O fixture for lipo errors

* test(native): drop fixture change already in main

* test(native): share breakpoint limitation wording with formatter

* test(native): assert limitation delivery via owner constants

* style(native): format breakpoint limitation projection
This commit is contained in:
STDquantum authored and GitHub committed 2026-11-04 19:38:15 +08:00
1 parent 094ec5eb3f
commit 2ae7878fc7
4 files changed
+132 -23

No files matched your search

+13 -15
View File
@@ -26,7 +26,6 @@ ARGUMENT_REGISTERS = {
"x86_64h": ["rdi", "rsi", "rdx", "rcx", "r8", "r9"],
}
MAX_SELECTOR_BYTES = 1024
MAX_REPORTED_LOCATIONS = 64
MAX_OTHER_STOPS = 256
MAX_TRACE_BYTES = 8 * 1024 * 1024
MAX_TRACE_FRAMES = 65_536
@@ -505,20 +504,19 @@ def _breakpoint_report(target, created, config):
if not _wanted(config["breakpoints"][index], location["symbol"] or ""):
continue
count += 1
if len(locations) < MAX_REPORTED_LOCATIONS:
remaining_bytes = (
MAX_BREAKPOINT_LOCATION_BYTES - retained_location_bytes
)
if not _fits_trace_budget(location, remaining_bytes):
locations_truncated = True
continue
location_bytes = len(json.dumps(location).encode("utf-8"))
addition = location_bytes + (1 if locations else 0)
if addition > remaining_bytes:
locations_truncated = True
continue
locations.append(location)
retained_location_bytes += addition
remaining_bytes = (
MAX_BREAKPOINT_LOCATION_BYTES - retained_location_bytes
)
if not _fits_trace_budget(location, remaining_bytes):
locations_truncated = True
continue
location_bytes = len(json.dumps(location).encode("utf-8"))
addition = location_bytes + (1 if locations else 0)
if addition > remaining_bytes:
locations_truncated = True
continue
locations.append(location)
retained_location_bytes += addition
report.append({"index": index, "location_count": count, "locations": locations})
return report, locations_truncated
+28
View File
@@ -430,6 +430,34 @@ class NativeTraceBudgetTests(unittest.TestCase):
self.assertEqual(report[0]["locations"], [])
self.assertFalse(locations_truncated)
def test_breakpoint_report_keeps_all_locations_under_aggregate_budget(self):
class Location:
def __init__(self, index):
self.index = index
def GetAddress(self):
return self.index
class Breakpoint:
def GetNumLocations(self):
return 65
def GetLocationAtIndex(self, index):
return Location(index)
with patch.object(
tracer,
"_location",
side_effect=lambda address, _target: {"address": address, "symbol": None},
), patch.object(tracer, "_wanted", return_value=True):
report, locations_truncated = tracer._breakpoint_report(
object(), [[Breakpoint()]], {"breakpoints": [{}]}
)
self.assertEqual(report[0]["location_count"], 65)
self.assertEqual(len(report[0]["locations"]), 65)
self.assertFalse(locations_truncated)
def test_breakpoint_report_bounds_aggregate_location_json(self):
tracer.lldb.LLDB_INVALID_ADDRESS = -1
symbol_name = (
+9 -7
View File
@@ -40,6 +40,11 @@ export const NATIVE_CALL_OBSERVATION_LIMITATIONS = [
"The observation window is checked about once a second, and recording pauses the process at every entry, which slows it.",
] as const;
export const NATIVE_CALL_BREAKPOINT_LOCATION_LIST_INCOMPLETE_LIMITATION =
"The breakpoint location list is incomplete; location_count gives the full matching count.";
export const NATIVE_CALL_BREAKPOINT_LOCATION_BUDGET_LIMITATION =
"Resolved breakpoint locations exceeded the aggregate 8388608-byte (8 MiB) metadata budget; omitted locations are counted in location_count. This metadata limit does not change the process outcome. This release has no runtime override.";
/** Launch the active Mach-O under LLDB, record requested call entries, then stop it. */
export const observeNativeCalls = async (
target: BinaryTarget,
@@ -216,16 +221,13 @@ const nativeCallFacts = (
`Breakpoints ${unresolved.join(", ")} matched no code in any image loaded while observing; their calls could not be observed.`,
]),
...(run.breakpoints.some(
({ location_count: count, locations }) => locations.length < count,
({ location_count: count, locations }) =>
locations.length < count && !breakpointLocationLimit,
)
? [
"Breakpoints with many matching locations list only the first 64; location_count gives the total.",
]
? [NATIVE_CALL_BREAKPOINT_LOCATION_LIST_INCOMPLETE_LIMITATION]
: []),
...(breakpointLocationLimit
? [
"Resolved breakpoint locations exceeded the aggregate 8388608-byte (8 MiB) metadata budget; omitted locations are counted in location_count. This metadata limit does not change the process outcome. This release has no runtime override.",
]
? [NATIVE_CALL_BREAKPOINT_LOCATION_BUDGET_LIMITATION]
: []),
...(trace.stdout === null || trace.stderr === null
? ["One or more target output captures were unavailable."]
@@ -13,6 +13,10 @@ import type { AnalysisError } from "../../../../src/domain/analysisErrorBase.js"
import { EvidenceIntegrityError } from "../../../../src/domain/evidenceErrors.js";
import { nativeCallPartialObservationSchema } from "../../../../src/domain/native/nativeCallPartialObservation.js";
import { nativeCallObservationResultSchema } from "../../../../src/domain/native/nativeCallObservation.js";
import {
NATIVE_CALL_BREAKPOINT_LOCATION_BUDGET_LIMITATION,
NATIVE_CALL_BREAKPOINT_LOCATION_LIST_INCOMPLETE_LIMITATION,
} from "../../../../src/native/NativeCallObservation.js";
import { err, ok, type Result } from "../../../../src/domain/result.js";
import type {
NativeCallTrace,
@@ -216,6 +220,80 @@ describe("observe_native_calls projection: projects completed observations", ()
});
});
describe("observe_native_calls projection: keeps locations within the metadata budget", () => {
it("projects every breakpoint location when the aggregate metadata budget allows it", async () => {
const locations = Array.from({ length: 65 }, (_, index) => ({
...LOCATION,
load_address: `0x${(0x100000928 + index * 4).toString(16)}`,
file_address: `0x${(0x100000928 + index * 4).toString(16)}`,
}));
const observed = await observe(
new FixtureTracer(
ok({
debugger: {
path: "/usr/bin/lldb",
sha256: "a".repeat(64),
version: null,
},
run: run({
breakpoints: [
{ index: 0, location_count: locations.length, locations },
{ index: 1, location_count: 1, locations: [LOCATION] },
],
}),
stdout: { text: "", bytes: 0, truncated: false, complete: true },
stderr: { text: "", bytes: 0, truncated: false, complete: true },
terminated: true,
}),
),
{ breakpoints: BREAKPOINTS },
);
if (!observed.ok) throw observed.error;
const result = nativeCallObservationResultSchema.parse(
observed.value.result,
);
expect(result.breakpoints[0]?.location_count).toBe(65);
expect(result.breakpoints[0]?.locations).toHaveLength(65);
expect(result.coverage).toEqual({
status: "complete",
event_limit_reached: false,
resource_limit_reached: false,
unresolved_breakpoints: [],
});
});
it("uses a generic warning when locations are missing without a budget signal", async () => {
const observed = await observe(
new FixtureTracer(
ok({
debugger: {
path: "/usr/bin/lldb",
sha256: "a".repeat(64),
version: null,
},
run: run({
breakpoints: [
{ index: 0, location_count: 2, locations: [LOCATION] },
{ index: 1, location_count: 1, locations: [LOCATION] },
],
}),
stdout: { text: "", bytes: 0, truncated: false, complete: true },
stderr: { text: "", bytes: 0, truncated: false, complete: true },
terminated: true,
}),
),
{ breakpoints: BREAKPOINTS },
);
if (!observed.ok) throw observed.error;
const result = nativeCallObservationResultSchema.parse(
observed.value.result,
);
expect(result.limitations).toContain(
NATIVE_CALL_BREAKPOINT_LOCATION_LIST_INCOMPLETE_LIMITATION,
);
});
});
describe("observe_native_calls projection: reports partial observation coverage", () => {
it("retains normal exit and location counts when breakpoint metadata is bounded", async () => {
const observed = await observe(
@@ -259,7 +337,10 @@ describe("observe_native_calls projection: reports partial observation coverage"
unresolved_breakpoints: [],
});
expect(result.limitations).toContain(
"Resolved breakpoint locations exceeded the aggregate 8388608-byte (8 MiB) metadata budget; omitted locations are counted in location_count. This metadata limit does not change the process outcome. This release has no runtime override.",
NATIVE_CALL_BREAKPOINT_LOCATION_BUDGET_LIMITATION,
);
expect(result.limitations).not.toContain(
NATIVE_CALL_BREAKPOINT_LOCATION_LIST_INCOMPLETE_LIMITATION,
);
expect(result.limitations.join(" ")).not.toContain("process was killed");
});