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:
1 parent
094ec5eb3f
commit
2ae7878fc7
4 files changed
+132
-23
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user