Commit Graph
93 Commits
Author SHA1 Message Date
morluto 7f920465c5 fix(ci): align verification with current lifecycle contracts 2026-11-11 15:10:51 -05:00
7aca4cef64 feat(audit): report what a shipped app exposes to static analysis (#8525)
* feat(audit): report what a shipped app exposes to static analysis

Adds `audit_release_exposure` and `rea audit-release-exposure` to report
source maps, Electron fuse implications, extractable payloads, debug
information, build metadata, and credential markers. Checks carry stable
IDs, observations, statuses, and fixed remediation, while incomplete and
unassessed checks remain visible without implying a security verdict.
Credential findings include only rule IDs and locations. The CLI
supports `--fail_on` severity thresholds and exits 3 when an exposed
check meets the threshold.

* fix(audit): keep incomplete coverage unknown across text, fuses, Go flags and cancellation

Probe files with unrecognized names as text, keep undecided fuse slices
unknown, forward opaque container coverage, read paths from Go build flags,
and compare inventoried digests before accepting owner facts.

Keep typed cleanup failures and attach the partial report when a later owner
is cancelled. Pin the verifier's @electron/fuses version and move the tests
to the named contract family.

* fix(audit): match package members by source spelling and close remaining coverage gaps

Reopen package members by their source spelling and keep caller path
traversal in directory reads. Parse quoted and attached Go build flags, bind a
native image fuse read to its own digest, and keep reader resources and the
audit's partial report when cleanup or cancellation combine failures.

* fix(release-exposure): bind package members by ordinal and follow named map references

Package members were matched to container entries by sourcePath, which is
not unique in a PyInstaller executable, through a queue that only moved when
a member was reopened. Bind by container-relative path and duplicate ordinal,
the numbering inventory uses, so read order cannot change which bytes a
member gets.

A sourceMappingURL naming a file without a map extension was ignored. Local
targets are now examined as maps whatever their name, and a target already
scanned as plain text leaves coverage unknown.

* fix(release-exposure): match data: source map scheme case-insensitively

URL schemes are case-insensitive, so an inline declaration such as
//# sourceMappingURL=DATA:application/json;base64,... was routed to the
local-reference path, which ignores scheme URLs. Its embedded sources were
silently dropped and source_map_sources reported not_observed.

* fix(release-exposure): normalize declared map targets and count unreached ones

A sourceMappingURL that decodes to a decomposed name (cafe%CC%81.json)
never matched the inventoried member, whose logical path is NFC, so the
map was scanned as ordinary text and its sourcesContent went uncounted.
Map-target lookups now use an NFC key on both the record and lookup
side; the declared URL is left as written.

A declared map target that is never reached is now counted as a skipped
map, so the source-map checks read as unknown coverage instead of
not_observed.

* fix(release-exposure): recover indexed leaves after any map format failure

An indexed map whose one leaf has malformed mappings (such as AA) threw a
format failure that only recovered leaves for unsupported sections, so a
valid sibling leaf with sourcesContent was dropped and
javascript.source_map_sources read unknown instead of exposed. Leaves are
now recovered for any format failure, with the envelope parse guarded so
invalid JSON still falls through to the unparsed count.

A nested package that inventory recognized but did not expand was probed
as a plain binary and left the source-map checks at not_observed. It now
marks coverage unknown and records a limitation.

Reader tests assert the javascript.source_map_sources status for the NFC
map-name cases as well.

* fix(release-exposure): count an unexpanded nested package as a readable member

An unencrypted nested package that inventory recognized but did not
expand is still a member its parent package already read. Count it as
readable so packaging.extractable_payload reports exposed, while its
own contents keep coverage unknown.

* fix(release-exposure): bound the indexed leaf fallback by decoder limits

The fallback walk after a map format failure expanded every section and
built a full ancestor path per leaf, so a map refused for nesting depth
could still allocate far past the decoder budgets. Walk only to the
64-level section depth and the shared record budget, keeping leaf maps
without materializing section paths.

* fix(release-exposure): collect UNC roots from Go flag paths

A flag such as -I\\buildhost\sdk\include matched none of the path roots
in GO_FLAG_PATH, so a network path that was the only one in complete
build info left go.build_paths at not_observed. Accept \\host\share
roots alongside the slash-rooted, dot-relative and drive-letter forms.

* fix(release-exposure): keep Electron undecided without positive proof

The desktop owner names Electron only from an ASAR layout, so an asar: false
build has no candidate. A complete scan with no candidate was reported as
not_electron, which outranks unresolved and safe fuse outcomes and hid them
as not_applicable.

Leave the family unknown unless the fuse holder rules Electron out: an app
bundle with no Electron Framework, or a directory with complete coverage
whose native images were all read and hold no sentinel. A directory with no
native image is not proof, so its fuse checks now report unknown.

* fix(release-exposure): decode wide text members before scanning

A member's kind bypassed the NUL guard, so a UTF-16 plist was decoded as
UTF-8, matched nothing and still counted as scanned. A directory holding
only that member reported text.credential_markers as not_observed.

Decode by byte order mark at one boundary instead. UTF-16 is scanned in
either byte order, and inline maps read the same text. UTF-32 and NULs no
mark explains count as skipped, whatever the member's kind. A binary
plist keeps its raw scan. Unnamed members with a mark now reach the
scanner, and the plist decoder shares the same mark detection.

* fix(release-exposure): follow file: URL source map declarations

The scheme guard in noteLocalMapReference returned on every URL scheme, so
a declaration such as file:///release/debug.json was dropped with the remote
ones. A member without a .map name was then scanned as ordinary text and
javascript.source_map_sources reported not_observed.

Convert file: URLs with fileURLToPath and record them in mapTargets through
the same step as relative declarations. A target that is never reached, or
that was already scanned as text, counts as skipped, and so does a malformed
file: URL. A file: URL names one absolute location, so a .map target outside
the inventory stays unassessed rather than being trusted by its name. Remote
schemes stay ignored.

* fix(release-exposure): match backslash-relative Go flag paths

GO_FLAG_PATH accepted dot-relative roots only with a forward slash, so
`-I..\sdk\include` and `-I.\sdk` produced no path and go.build_paths came
back not_observed. Accept either separator after the dots.

The CLI acceptance test now expects unknown for electron.run_as_node on a
directory with no native images, matching the service.

* fix(release-exposure): keep fuses unknown on incomplete inventory

A directory inventory that stops at an acquisition limit never reports
the images it did not reach, so the native image sample has no omission
and safe fuses on an included helper settled to not_observed even though
scan coverage was unknown.

Treat unknown coverage like a truncated sample until the Electron
executable is accounted for. A selected native file is that executable,
so it keeps its decided fuse result.

* fix(release-exposure): decode binary plists before scanning

A binary plist stores each string as ASCII or UTF-16BE. Scanning the raw
bytes as UTF-8 missed any string the encoder kept in UTF-16, such as one
with a non-ASCII character, and still counted the member as fully
scanned, so text.credential_markers read not_observed.

Decode through the plist owner and scan its strings, dictionary keys and
data. A plist that cannot be decoded keeps its raw bytes in the scan and
counts as skipped, so coverage stays unknown.

* fix(release-exposure): keep UTF-8 members that hold a NUL

Any NUL in a member's first 512 bytes made the decoder give up, so a script
with a literal NUL in a string never reached the credential or inline-map
checks. An exposed marker in it read as unknown, and --fail_on high exited 0.

Validity alone cannot tell wide text from UTF-8, since ASCII in UTF-16 is
valid UTF-8. Treat a header that is an eighth or more NUL bytes as wide text
and anything not UTF-8 as undecoded, and scan the rest.

The docs now separate BOM-marked UTF-16, which is scanned, from wide text
with no byte order mark, which is not.

* fix(audit): scan every string a binary plist stores

The plist scan read only the decoded value, which drops objects the root no
longer references and entries a later duplicate key replaced. Those bytes
still ship, but the audit marked the scan complete, so a credential stored
there read as not observed and --fail_on high exited 0.

Read strings, keys and data from the object table instead. A stored object
that cannot be read leaves the member unassessed, and a total byte budget
bounds table entries that overlap to repeat the same text.

* fix(audit): keep packaged native images from ruling out Electron

The applicability check only looked at the "native images" omission. An
Electron executable inside a compressed .NET or PyInstaller member is recorded
under "packaged native images", so a directory whose on-disk hosts had no fuse
sentinel read as not Electron and all four fuse checks as not applicable.

Treat that omission as an unread image too.

* fix(audit): keep readable plist text when one object is unreadable

Throwing on one unreadable stored object discarded every string already
decoded, so a UTF-16BE credential beside a truncated unused object was lost:
the raw bytes that replaced it cannot match NUL-separated text.

Return the decoded text with complete false instead, and keep the raw bytes
in the scan beside it so ASCII in the unread object is still seen. The member
stays unassessed. Text past the budget ends the same way.

* fix(audit): read escaped inline maps and extensionless SEA entries

A data URL may percent-escape its base64 payload, so a map such as one padded
with %3D did not decode and the audit counted no embedded source map. Decode
the escapes before reading the base64.

A directory walk classed a SEA entry by its code path, so a main script whose
code path names no script was never scanned as JavaScript and its inline map
went unseen. Class it by the role the SEA metadata gives it.

* fix(audit): check plist text against the budget before decoding it

decodeBinaryPlistText decoded each stored object before comparing its size
with the remaining budget, and it keeps going after an object is rejected so
later text is still read. A plist under the input limit could repeat a large
range through overlapping table entries and force gigabytes of decoding that
the audit then discarded.

Separate finding a stored payload from decoding it, so a range past the
remaining budget is rejected without allocating its text. Earlier text and
the incomplete result stay as they were.

* fix(audit): count a packaged PE with an unread header as a native image

A package member whose PE signature lies beyond the 4 KiB probe recorded only
a PDB failure. The image was neither read nor listed under packaged native
images, so a directory whose on-disk hosts had no fuse sentinel read as not
Electron and all four fuse checks as not applicable.

Count it as a packaged native image, the omission that keeps the question
open, and keep probing it for text as before.

* fix(audit): keep cleanup-incomplete errors from composed readers

A desktop, fuse, Go or Rust failure became a string on that check, so an
error with unfinished cleanup lost its resources and live bindings while the
audit reported success, and the MCP handler does not close the service after
each request.

Treat unfinished cleanup like cancellation: stop the remaining owners and
return the error with the partial audit. When the owner attached its own
partial, keep the error and replace the partial, so cleanup stays attributed.

* fix(audit): keep safe fuses unknown past a packaged native image

The fuse check counted sampled-out images as possibly unread but not native
images inside a package. A complete directory scan with a safe-fuse image on
disk, no ASAR hint, and an unopened packaged image reported the four Electron
checks as not_observed, although the Electron executable could be that image.

The fuse check, the Go and Rust checks, and the Electron rule-out each decided
this for themselves and had drifted apart. Move the decision to one domain
predicate, nativeImagesMayBeUnread, and use it in all three. A hinted
executable that was read still settles the check.

* fix(audit): settle fuses only on an identified host, and read Bun and HTML maps

A retained helper set electron_executable_included because the rank marks
every .exe or extensionless sibling of resources/app.asar. With more than four
siblings, or an inventory that stopped early, safe fuses on a helper reported
not_observed while the host went unread. Rank stays a sampling hint. The check
now settles only on an executable named as the host, or when inventory was
complete and every executable that could be the host was sampled.

A Bun sourcemap region is named .bunmap, so its role was lost and a complete
audit reported no source maps. Carry the region role from the reader's metadata
into the member view, in the expanded and unexpanded paths, and treat it as a
JavaScript map so an unparsable region stays unresolved, not a debug companion.

An HTML member was scanned as text but its inline scripts were never checked
for source maps. The HTML owner now returns each body, the reader examines it
like a script file, and bodies past the owner's limit count as skipped.

* fix(audit): charge map reads to the count before opening the member

An oversized or unreadable .map returned before readSourceMap counted it, so
the allowance of 32 never ran out. An unexpanded .NET or PyInstaller package
could then decompress every listed map member, about 1 MiB each, up to the
4,096 entry cap.

The count bounds reads, so examineMap now reserves the slot before it opens the
member. A map past the count is skipped unopened, and a linker map past it is
still settled from its bounded prefix. An inline data: map is already in memory
and keeps its charge at the point it is parsed.

* fix(audit): examine scripts HTML loads whatever they are called

isJavaScriptMember knows a script by its suffix or an assigned kind, and the
reader dropped the script references the HTML owner parses. An extensionless
main loaded by <script src="main"> was scanned as ordinary text, so a complete
inventory reported javascript.source_map_sources not_observed with shipped
sources in its inline map.

The reader now resolves each JavaScript script reference against the page and
its base href, and a local target without a JavaScript name is examined as a
script when reached. A target read before its HTML file, never reached, or not
placeable on disk counts as a skipped map, so the check stays unknown.

* fix(audit): charge sibling map reads to the map count

Every generic .map beside a selected file got a full 1 MiB read and a
parse, with no cumulative bound, so the 4,096-entry listing allowed about
4 GiB of reading for one small selection.

Move the slot rule into chargeMapRead, which examineMap and sibling
classification now share. Past the count a sibling reads only the header:
a linker map is still identified, and one that may be a source map stays
a skipped companion.

* fix(audit): report cleanup failures raised by member reads

A close failure during an embedded-package probe, member read or inventory
reaches the shared read filter as an ordinary reader failure. The filter kept
only cancellation and integrity, so the member became a skipped read, inspect
succeeded, and the retained descriptor was reported nowhere. MCP does not close
the service after each request, so no caller saw it.

Propagate any failure that carries cleanup, whatever its reason, so it reaches
releaseExposureFailure and the audit reports its retained resources.

* fix(audit): charge unsuccessful text reads to the byte allowance

An oversized, undecodable, or failed text read left textBytes unchanged, so
every member got the full per-member budget and the cumulative allowance did
not bound what a package decompressed. Count the bytes member reads produce
and charge them whatever the read yields. An oversized file on disk still
costs only its probe.

* fix(audit): bound the paths kept for HTML script targets

A base href is copied into every script target it resolves, so a page under
the text member allowance could retain hundreds of megabytes of paths. Charge
each new target's resolved path to a byte allowance and leave targets past it
unassessed.

* fix(audit): keep examining a page loaded as a script as HTML

A script element naming an .html member promoted that member to a
script, and the text pass then chose one reading. The page's own script
elements were never read, so an inline map in it was lost and a shipped
sourcesContent went unreported.

Run the script reading and the HTML reading independently, and list the
script target allowance among the cases that leave source map coverage
undecided.

* fix(audit): detect credential markers that end in a hyphen

The Google, GitLab, and Slack rules allow a hyphen in the token but ended
on a word boundary, so a token ending in a hyphen at the rule's minimum
length was missed and the audit reported the check as not observed.
Open-ended rules now take the whole token, and the fixed-length Google
rule checks the full token alphabet after its last character.

* fix(audit): confirm FAT identity for probed package members

The package probe treated a decoded FAT table as a universal Mach-O. A
JVM class past the 4 KiB probe shares the FAT magic, so its truncated
prefix decoded as a table, the member counted as an unread native image,
and Electron could not be ruled out of a complete scan.

Pass the member's declared size to the sniff and take identity from
classifyFatContainerEvidence. A member with no usable size, or with
slices past the probe, stays counted as possibly native.

* fix(audit): admit binary plists in package member probes

A package member's probe judged text by its own NUL test, which accepted
byte order marks but not the binary plist signature decodeMemberText also
reads. A PyInstaller executable shipping a binary Localizable.strings was
rejected without a scan or a skipped count, so a UTF-16BE private key
marker inside it could report not_observed.

Name the encodings that explain their own NULs in one predicate, and move
the plist fixture builders to module scope.

* fix(audit): locate Go findings only in images that retain build paths

The exposed decision required non-empty paths, but the locations took
every image whose build info read. An image with no paths was named as a
source of the leak beside the one that had them.

One predicate now decides both.

* fix(audit): keep completed findings when member cleanup fails

A descriptor close failure aborts the member scan, and the scan's state
was dropped with it, so the caller got a typed cleanup failure and no
report. Source maps already counted and credential hits already collected
were lost.

The reader now hands the service what it assembled, with coverage unknown
and a limitation saying so, alongside the failure. The service runs that
scan through its existing stopped-audit path: no later owner runs, and the
report goes on the error as its partial observation.

* fix(audit): keep coverage unknown for nested ASARs in package hosts

A member of a PyInstaller or .NET host reaches the binary probe as a
plain file. An ASAR among them has a NUL-bearing header, so the probe
dropped it with no traversal and no omission, and source map checks could
read not_observed with complete coverage.

Ask the ASAR owner's signature probe, and record a match as a nested
package that was not expanded, as inventory-classified ones already are.

* fix(audit): let native format facts outrank member names

examineMember tried the name-based text and map routes first, so a PE an
owner identified inside an ASAR, named payload.json, was read as text.
Its CodeView path and the packaged-native omission were both lost, and
native.pdb_path read not_applicable.

Route an owner-established native format to the native probe first, and
count an encrypted one as a packaged native image for the same reason.

* fix(audit): report relative paths given to compiler path flags

go.build_paths only kept rooted paths, so retained build settings like
CGO_CFLAGS=-Iprivate-sdk read not_observed even though the setting names
a directory.

Take the relative argument of a C compiler path flag, attached or in the
next field. Scope this to compiler settings, because Go's own tools use
the same letters for non-paths such as -B gobuildid and -T 0x1000.

* test(javascript): expect retained text on SVG inline scripts

The SVG test from main predates this branch's inlineScripts text field,
so each side passed alone and only their merge failed.

* fix(audit): keep an unresolved Electron host's fuses unknown

fuseCheck decided from whatever outcomes the images reported, so a host
with no fuse answer read not_observed as soon as any other image in the
directory reported safe fuses.

Pass the images named as the host into the report and leave a check
unknown when one of them has no answer for it. Other images stay free to
have no fuses, because helpers in a real Electron install have none.

* fix(audit): bound the paths kept for local source map targets

noteMapTarget kept every declared target resolved against its member's
directory, so a short URL under a long directory could retain far more
than the script's own size, with no cumulative limit.

Add the allowance noteScriptTarget already has. A target past it stays
unassessed and counts as a skipped map, and is never read.

* fix(audit): borrow the audit's scope admission for inventory

inspect holds resources.run for the whole audit, and inventoryArtifact
entered the same scope again. With failed cleanup pending from another
audit, the inner run waits for a drain that waits for the outer run.

Add inventoryArtifactInScope, which takes the caller's admission.

* fix(audit): run the exposure verifier's commands under supervision

The verifier launched npm and the CLI with raw execFile and then removed
its temporary tree, so a timed-out child could outlive the tree it was
using.

Use the shared verifier commands and close them before removing the tree.
Their exit error carries stdout on its snapshot, so the audit's exit-code
branch reads it from there.

* fix(audit): read relative -L paths and split flag arguments

The relative-path reader handled C compiler settings only, so a directory
kept through Go's own linker flag, such as -ldflags='-L private-sdk -s -w',
reported go.build_paths as not_observed.

A quoted flag and argument also reached it as one field. The separating
space stayed on the argument, which reported rooted paths twice and kept a
leading space on relative ones.

Give -ldflags its own pattern that matches only -L, so -B gobuildid and
-T 0x1000 stay ignored, and consume the separator in the flag patterns.

* fix(audit): keep embedded leaves beside schema-invalid map fields

The reader recovers independent indexed leaves only after a
SourceMapFormatFailure, but inspectSourceMap let the schema's ZodError
through. A sibling with mappings "!", a section with a bad offset or a
root with a bad field skipped recovery, so a valid leaf with
sourcesContent reported javascript.source_map_sources as unknown.

Normalize schema rejection in SourceMapFormat, which owns these parses,
so every structural failure reaches consumers as a format failure. The
message is unchanged, and the codec and web fetcher already map both
cases to the same result.

* fix(audit): keep packaged executables' own packages unknown

examinePackagedBinary counted a native image inside a package and moved
on, while only on-disk images reached the embedded-package probes. An
ASAR carrying a PyInstaller, Bun, Node SEA or .NET executable with a
source map therefore scanned complete and reported
javascript.source_map_sources as not_observed.

Run the same classifier on packaged images. The probes read carrier
offsets from a file, so the member's bytes stay in a temporary file for
the probe alone. A detected package keeps member coverage unknown, as a
nested ASAR does, and an image past one file's total byte allowance
stays unprobed and unknown. A plain native member leaves coverage intact.

* fix(audit): read an app's sampled images for fuses

A .app target sent only the bundle candidate to the fuse reader, which
resolves to the outer Electron Framework. A sidecar with RunAsNode enabled
was never read, so the check reported not_observed in the app view while
the directory view reported exposed.

Send the bundle candidate first and then the sampled native images. An
image whose digest matches the bundle's read is skipped, so the framework
is not read twice. A sidecar's safe fuses cannot settle the outer framework,
so a bundle read with no answer for a fuse keeps that check unknown.

* fix(audit): read a Node shebang as script identity

The path classifier gives an extensionless executable a resource kind,
so a script such as bin/runner with a `#!/usr/bin/env node` header was
scanned as text without its inline maps being examined or counted. A
complete inventory then reported no shipped sources for it.

The probe holds the first bytes a member reader sees, so establish the
identity there. The script keeps the usual outcomes: a map that is read
counts, and one past an allowance or undecodable stays unknown.

* fix(audit): keep linker strings out of Go build paths

Reading `-X 'main.usage=-L level'` as a library directory made
go.build_paths report `level`, so a plain string could fail an audit
under --fail_on low.

Read the optional package selector once, before the setting's first
flag, and keep the argument of -X attached to it, so a string it sets
is never scanned as another option.

* fix(audit): keep a failed probe directory removal owned

A packaged image's probe copies the member's full bytes into a temporary
directory. When removing it failed, the error became an ordinary
unreadable probe: the audit could still succeed, nothing could retry the
removal, and the client heard nothing about the leftover bytes.

Hold the directory in the reader's resource scope, so close retries it,
and report the unfinished cleanup without replacing a primary failure.

* fix(audit): keep a declared .map from reading as a linker map

A script's sourceMappingURL=assets.map was dropped because the name
already looked like a map. A malformed assets.map then settled as a
linker map, so both source-map checks read not_observed and
native.debug_companions reported an exposure the release does not ship.

Keep a generic .map name as a declared target and read it as a
JavaScript map, in either scan order, so it stays unresolved.

* test(audit): rethrow typed errors from release exposure checks

A failed Result was rewrapped in a plain Error, which dropped its type,
cause and cleanup details from the test output.

* fix(audit): match companions by member identity, not spelling

An ASAR member's location is relative, and a declared map target is
absolute, so a malformed assets.map read before z.js stayed a linker
companion. The examined-map lookup, which is normalized, then returned
early, leaving both source-map checks not_observed.

Index companions by memberLocationKey and keep each reported spelling
under it. Spellings stay distinct because symlinks can make them differ.

* fix(audit): retract only guessed linker maps on a declaration

A script declaring sourceMappingURL=symbols.pdb after the scan reached
symbols.pdb removed the shipped PDB from the companion list, because a
late declaration retracted any companion. native.debug_companions could
then read not_observed.

Keep name-identified companions in their own set, and retract only maps
that were read as non-JSON and guessed to be linker maps.

* fix(audit): record an unfollowed map link as skipped

The directory and ASAR readers do not follow links, so a symlinked
app.js.map dropped out before any map counter moved. Its target was
scanned as plain text, and both source-map checks read not_observed.

Count a link that names a map as skipped wherever entries reach the
audit, and mark it examined so a declaration does not count it twice.

* fix(audit): read relative paths in Go compiler and assembler flags

A binary built with -gcflags=-I private-sdk keeps that setting, but
relative parsing covered only C compiler settings and -ldflags, so the
directory dropped out of go.build_paths.

Give -gcflags and -asmflags their own path options, taken from what
go tool compile and go tool asm document, and strip the package selector
as -ldflags does. The assembler's -D defines a symbol, so it is not one.

* docs(audit): state the packaged image read allowance

Packaged ELF, Mach-O and PE images are read in full and copied to a
temporary file, sharing one max_file_bytes allowance, but the docs and
coverage.allowances still said only a PE gets more than a header read.

Name the allowance and the copies in docs/release-exposure.md,
RELEASE_EXPOSURE_ALLOWANCES and the max_file_bytes description.

* docs(audit): say how go.build_paths treats rooted -X values

A rooted -X value cannot be told from a file path, so the audit reports
it. State that, and why, so the rule is a contract rather than a side
effect of how flags are scanned.

* fix(audit): keep a declared sibling map out of the linker guess

When the selected file declared a sibling map, the sibling classifier
never consulted the declaration. A malformed assets.map then read as a
linker map and raised native.debug_companions.

Leave a declared sibling to the unreached-target accounting, as members
already are, so it stays an unresolved source map.

* fix(audit): keep undecodable Go and Rust metadata unknown

Unsupported meant two things: a container this reader does not handle,
and metadata the image holds in a form the reader cannot decode, such as
a compressed .dep-v0 section. Both read as not_applicable, so an image
that carries metadata reported that the check did not apply.

Mark the second case on the format failure and carry it on
AnalysisUnsupportedTargetError, so the audit records a failed read and
stays unknown. The tools' own errors and messages do not change.

* fix(audit): keep coverage unknown for a PE header past the probe

A packaged member whose PE signature lies beyond the probe prefix stayed
unclassified, so the embedded-package probe never ran and coverage stayed
complete. A map inside such an image then read as not_observed.

Record the member as an unexamined package where it is already marked as
a possible native image.

* fix(audit): read the scripts of a standalone SVG

An SVG can carry a script with an inline source map, but only .html and
.htm members reached the script owner, so such a file left every map
counter at zero and javascript.source_map_sources read not_observed.

Treat .svg as a script-bearing page. htmlArtifactReferences already
reads SVG script elements, so this adds no parsing.

* fix(audit): keep every possible host when no name picks one out

With no named Electron host, every sibling ranks alike and
electron_host_paths stayed empty, so one image's safe fuses settled the
checks even when another possible host reported none.

Keep all sampled candidates as hosts in that case, so a candidate
without fuses leaves the checks unknown.

* fix(audit): read a text-named native member by its bytes

A PE member called payload.json took the text route, so it reached no
native probe, and its CodeView path and packaged-native uncertainty were
lost.

Decide from the bytes the text read already loaded, so real text costs
no extra read and the map and text allowances stay as they were.

* fix(audit): let a map or script read identify a native member

A package lists names without formats, so a PE called payload.js.map
took the map route and never reached a native probe. Its CodeView path
was lost, and with nothing counted as failed, native.pdb_path could read
not_applicable. A PE at a declared script or map target was lost the same
way, because declaration overrode the route.

Decide from the bytes the route already read, in the map route as in the
text route, and keep the listed kind for a promoted script target. Real
text and maps cost no extra read, so those allowances are unchanged.

* fix(audit): name a symlinked sibling debug companion

Selecting an executable skipped every symlink beside it before the name
check, so an app.pdb link left no companion path and no skip count, and
native.debug_companions read not_observed. Selecting the directory
reports the same link.

Check the name first. The map branch already needs a regular file, so no
link is followed.

* fix(audit): keep map coverage unknown for a linked script

A link named app.js to an extensionless payload was skipped as a link,
and its target was read as plain text with no map directives parsed. An
inline map there left no parsed or skipped count, so
javascript.source_map_sources read not_observed. Only links with a map
name were counted.

Count a link under any name that can declare a map, the set an unread
member already uses. It stays one omission when a page or a directive
also names it. The target is still not followed.

* fix(audit): keep a declared JavaScript map beside a selected file unknown

A local sourceMappingURL with a JavaScript map name such as app.js.map
was dropped on the assumption that it would be examined as a member.
When the selection is a file, sibling .js.map files are never read, so
the declared map left javascript.source_map_sources not_observed while
the same target spelled as a file: URL left it unknown. Record every
local target so an unreached one counts as skipped.

* fix(audit): read paths a C compiler forwards to the linker

CGO flags can hand linker options through -Wl,a,b or -Xlinker a. The
compiler flag parser saw -Wl,-L,private-sdk as one field and missed the
relative library directory, so go.build_paths reported not_observed.

Split forwarded options out before reading paths and read them with the
linker's own path options (-L, -T, --library-path, --script). This also
stops -Xlinker -Bsymbolic from being read as the compiler's -B prefix.

* fix(audit): read forwarded linker options by their linker meaning

The forwarded linker flag pattern read -Ttext=0x400000 as a script path
named text=0x400000, and it ignored -I, so -Xlinker -Ilib/ld-custom.so
lost the interpreter path the compiler flag pattern found before the
split.

Exclude the -T section and segment address options (bss, data, text,
text-segment, rodata-segment, ldata-segment) and read -I and
--dynamic-linker as paths.

* fix(audit): split Go flags as Go does and read external linker flags

Go splits a flag value with quoted.Split: a field that starts with a
quote runs to the matching quote, and any other quote is a literal
character. The reader stripped every quote, so CGO_CFLAGS
-I"/home/O'Neil/include" reported /home/ONeil/include, while cc was
given the directory "/home/O'Neil/include", quotes included.

-ldflags kept -extldflags values with the Go linker flags, where only
-L counts, so -extldflags=-Lprivate-sdk reported no path. The values
are now read as the C compiler driver reads them, including -Wl, and
-Xlinker forwarding.

A package member named payload.json or payload.js.map that holds an
ASAR took the text or map route, which checked only native signatures,
so the nested archive was scanned as text and coverage read complete.
The route's own read now recognizes the ASAR signature as well, and the
nested package leaves coverage unknown.

* fix(audit): keep a named script beside a selected HTML file unknown

A script element whose target had a JavaScript name such as app.js was
dropped on the assumption that it would be examined as a member. When
the selection is an HTML file, sibling scripts are never read, so a
sibling holding an inline map left javascript.source_map_sources
not_observed while the same target named app left it unknown. Record
every local script target so an unreached one counts as skipped.

* fix(audit): read a retained -pgo setting as one profile path

`-pgo file` names a single profile, and under -trimpath Go keeps only
its file name, so the flag readers dropped values such as
`private-training.pgo` and split paths with spaces or commas. Report
the value whole, skipping `auto` and `off`.

* fix(audit): read -extldflags paths as the external linker splits them

With -ldflags set to -extldflags '-L "/srv/private sdk"', the outer
flag reader could not match the path behind the inner quote, and the
external reader kept only relative paths, so go.build_paths read
not_observed. Read each -extldflags value once, as go tool link splits
it with quoted.Split, and collect rooted and relative paths from those
same arguments. The rest of the setting is read as before.

* fix(audit): read the Go linker's other path options

go.build_paths read only -L as a Go linker path option, so a retained
-ldflags='-tmpdir=private-obj -s -w' reported no path and --fail_on low
missed the directory.

Read every option `go tool link -h` documents as taking a directory,
path or file: -I, -L, -benchmarkprofile, -capturehostobjs, -cpuprofile,
-fipso, -importcfg, -memprofile, -o, -r and -tmpdir. -extld, -extar and
-libgcc name a program or `none`, so only their rooted values count.

* fix(audit): keep a quoted Go argument whole

goFields already reads -L "/tmp/private - sdk" as two arguments, but
each field was then split again before every whitespace-led dash, so
go.build_paths reported /tmp/private instead of the retained directory.

Split again only a field that begins with a flag, after any package
selector, as a quoted "-L dir -s -w" does. Any other field is one
argument and is kept whole, in -ldflags, -extldflags and CGO flags.

* fix(audit): keep coverage unknown for an unread packaged image

When a listed package member is a native image but its first read
fails, examinePackagedBinary returned before probePackagedImage could
mark its possible embedded package unexamined. Coverage stayed complete
and javascript.source_map_sources could report not_observed for a
payload nobody read.

Mark member coverage unknown wherever a native member goes unread,
including an encrypted one, as an image the probe could not read
already does, and say so in the limitations.

* fix(audit): read rooted Go tool path arguments whole

`-L "/tmp/private,sdk"` and `-L=/tmp/a=b` name one directory, so a `,` or
`=` in a documented path flag's argument is literal. `-trimpath` keeps its
prefix=replacement structure.

* fix(audit): count an unread packaged member once

* fix(audit): keep trimpath rewrites and directory fields apart from selectors

* fix(audit): keep a .map unsettled once a declaration was discarded

* fix(audit): skip env option arguments when reading a Node shebang

* fix(audit): keep companion coverage unresolved for a generic .map link

* fix(audit): count a dropped linker-map guess as a skipped companion

* fix(audit): read env settings after -- in a Node shebang

* fix(audit): route a selected native image named .map by its format

A directly selected or listed native image has no package format, so
examineMember skipped its native-first branch and sent a file such as
release.map through map routing. Its bytes then became a linker-map
guess and marked native.debug_companions exposed. Native format now
outranks the name for every member, packaged or not.

* fix(audit): read forwarded -rpath and -rpath-link as linker paths

A cgo build with CGO_LDFLAGS=-Wl,-rpath,private-runtime kept the
relative directory, but the forwarded linker parser only knew -L, -T,
-I, --library-path, --dynamic-linker and --script, so go.build_paths
reported not_observed. Add -rpath and -rpath-link to the same parser,
in the attached, = and next-field forms. Loader-expanded values such as
$ORIGIN/../lib and @loader_path/Frameworks name no build-host directory
and stay unreported.

* fix(audit): read each entry of a forwarded linker search list

-rpath and -rpath-link take a colon-separated search list. The loader
token filter tested the whole list, so -Wl,-rpath,$ORIGIN:/opt/private-runtime
dropped /opt/private-runtime along with $ORIGIN, and the rooted-path pass
cannot see an entry that follows a colon. Split each search list into its
entries and drop only the loader-expanded ones.

* fix(audit): expand loader tokens only in a linker search list

The runtime loader expands $ORIGIN and @loader_path only in a -rpath or
-rpath-link search list. The token filter also ran on the argument of -T,
-L and -I, so -Wl,-T,$ORIGIN.ld dropped a literal linker script path and
a complete read could report go.build_paths as not observed. Search list
entries keep the filter; every other linker path argument is kept as is.

* fix(audit): count a .map link beside a selected file as a skipped companion

The sibling scan skipped any .map entry that was not a regular file, so a
symlink such as symbols.map -> payload beside a selected executable left
debug companion coverage at not observed. The reader never follows links,
so the linker map such a link may name is unread, not absent. Count a
generic .map sibling link as a skipped companion without following it,
as noteUnfollowedMapLink already does inside a selected directory.

* fix(audit): split a linker search list only for an ELF image

Only the ELF linker reads a -rpath or -rpath-link argument as a
colon-separated list. The Mach-O linker stores each -rpath argument as one
directory, so -Wl,-rpath,@loader_path/lib:extra is a single loader-relative
entry, yet it was split and extra was reported as a build path. Carry the
native image format into the Go flag reader and split search lists only for
ELF; other formats read the whole argument as one entry.

* fix(audit): expand only the image loader's runtime tokens

A runtime search path token is expanded only by the loader of the image's
format. The ELF loader expands $ORIGIN, $LIB and $PLATFORM; dyld expands
@executable_path, @loader_path and @rpath. Each is literal text to the
other loader, so -Wl,-rpath,$ORIGIN:/opt/private-runtime on a Mach-O image
names a literal directory, yet it was dropped and go.build_paths reported
not_observed. Pick the token set from the native image format; the PE
loader reads no search list, so it expands none.

* fix(audit): keep a declared .map link beside a selected script a source map

The sibling scan counted any .map symlink as a skipped debug companion
before checking whether the selected script declared it as its source
map. classifySiblingMap already excludes a declared target that is a
regular file, so a declared symbols.map link added a false unknown to
debug_companions. Apply the declared-target check before either file-type
branch; noteUnreachedTargets still counts the unread map as skipped.

* fix(audit): byte-check members whose kind is only an inventory path role

Inventory gives a packaged member a role from its name, such as resource
for payload.json or source-map for payload.map. The packaged-format guard
treated any kind other than "file" as owner-stated, so a nested ASAR an
ASAR lists under one of those names skipped signature recognition and
cleared coverage. Track whether the package owner stated the kind (Bun
sourcemap region, SEA main script) and only skip the byte check then.

* fix(audit): read Go's linker -r setting as a runtime search list

go tool link -r sets the ELF dynamic linker search path, dir1:dir2:...,
which the linker writes to RUNPATH. It was read as one whole path, so a
release built with -ldflags='-r=$ORIGIN/../lib' reported the
loader-relative entry as a build path and go.build_paths as exposed.
Read -r entry by entry with the same image-loader-aware split and token
filter a forwarded -rpath uses: ELF splits at ':' and drops $ORIGIN
entries; Mach-O keeps the argument as one directory.

* fix(audit): keep a selected package executable's native carrier format

A directly selected PyInstaller executable named release.map recorded the
package format as its member format, even though its ELF or PE carrier was
already recognized. examineMember then missed the native-first branch and
treated the executable as a linker map, reporting a debug companion.
Forward the native carrier format for the selected file, as listed members
already do.

* fix(audit): keep coverage unknown for an unread unclassified member

A packaged member with no format of its own, such as a PyInstaller
payload.bin, recorded only a skipped text member when its read failed
before the header. Member coverage then stayed complete, though the
member could be a native image or carry a nested package. Leave
coverage unknown and say which member could not be read, as an unread
native member already does.

* fix(audit): keep ASAR header admission distinct from max_file_bytes

A directory holding an ASAR whose header exceeds the header admission
was reported as a file-size refusal with max_file_bytes advice, which
cannot fix it. Report the header admission as its own bound instead.

* fix(audit): thread the shared ASAR header admission into release exposure

createReleaseExposureService built ReleaseExposureReader with no admission,
so its inventory and every reopened ASAR reader stayed on the 16 MiB
default and ignored REA_ASAR_MAX_HEADER_BYTES and asar.max_header_bytes.

Resolve the admission in the composition with runtimeAsarHeaderAdmission,
as the Electron and V8 observation providers do, and pass it to both
inventoryArtifactInScope calls, the reopened container readers, and the
nested ASAR signature probe. The refusal now names the setting that can
raise it instead of claiming there is no runtime override.

* fix(audit): keep a nested ASAR probe stopped at the admission undecided

The release exposure reader treated an ASAR signature probe that stopped
at the header admission as "not an ASAR". A packaged member such as
payload.json holding an ASAR whose JSON starts past asar.max_header_bytes
was then read as plain text, its inner maps went unread, and
javascript.source_map_sources could stay not_observed.

A probe that reaches the admission limit, or runs out of a prefix read
after its extents parse, now marks package coverage unknown and records
a limitation, while the member is still examined as text.

* fix(cli): parse audit-release-exposure --max-file-bytes as a decimal token

Main now requires every numeric CLI option to admit decimal tokens and
reject 0x10 and 1e3 (tests/boundary/cli/cliDecimalArguments.test.ts).
Wrap the release exposure maxFileBytes option in cliDecimalField like the
other maxFileBytes options, so the domain field keeps its bounds and default.

* docs(specs): add the release exposure audit specification

Add spec.md, plan.md and tasks.md under docs/specs/artifacts/release-exposure/
as docs/development/documentation.md requires for a new feature, and list
them in docs/specs/README.md. The existing guide at docs/release-exposure.md
keeps describing current behavior and its route is unchanged.

* fix(audit): bind unpacked ASAR member reads to their inventoried digests

A selected ASAR is captured and checked against its inventoried digest, which
binds packed members. An unpacked member is read from its .unpacked companion
on disk instead, and readContainerMember and readEntryBytes never compared
those bytes with the member digest inventory recorded, so a companion
replaced between inventory and the scan was accepted.

readContainerMember now passes the member's inventoried digest for unpacked
entries, and readStreamBounded hashes the full stream, draining past the read
allowance when needed, and fails with "ASAR member changed before
interpretation", which surfaces as an artifact_changed error like the
container check. A regression test replaces a companion after inventory. The
guide, spec, plan and tasks describe the binding.

* fix(audit): let stated JavaScript roles outrank a .map name

isDeclaredMap() routed any member whose name ended in .map to map
handling before the owner's stated role or a page's script declaration
was considered. A Node SEA main script at boot.map, which the SEA reader
marks as javascript, and a .map script an HTML file loads both became
linker-map guesses, so their inline maps were never read and
native.debug_companions was reported exposed.

An owner-stated source-map role or a declared map target still makes a
member a map. Otherwise a stated javascript kind or a script target now
wins, and only then does the .map suffix, or the inventory path role that
repeats it, decide. Regression tests cover both cases, and the guide
notes that a SEA main script is read as a script whatever it is called.

* fix(audit): keep coverage unknown when a packaged text member fails to read

A compressed member named as text, such as payload.json in an unexpanded
PyInstaller host, can fail inflation before any header is returned. The
.bin case already leaves member coverage unknown because an unread header
may hide a nested image or package, but examineTextMember() only counted
the member as skipped text, so otherwise complete coverage stayed
complete and package-dependent checks could settle.

An unread packaged member whose kind its package did not state now goes
through noteUnreadBinary(), which counts it as skipped text, marks
coverage unknown, and records the limitation. A regression test corrupts
the zlib stream of payload.json in a CArchive host and checks the intact
member keeps coverage complete. The guide describes the rule.

* fix(audit): keep the unpacked ASAR digest check inside the read allowance

ea87cf8 made readStreamBounded hash an unpacked ASAR companion to its end
so it could compare against the inventoried digest, which drained the
stream past the read allowance. A 1 GiB unpacked module was read in full
for a 4 KiB header probe and again for a package probe, and a companion
that grew after inventory had no byte limit on that path.

readStreamBounded now stops at the allowance as every other member read
does, and compares the digest only when the member ended within the
allowance. A companion past the allowance stays over_allowance without a
digest verification. A test counts the bytes each scan read pulls from an
8 MiB companion. The guide and spec state the bound.

* fix(audit): read a script target whose bytes are a source map as a map

1d3f89f let a stated javascript kind or an HTML script target outrank a
.map name, so an HTML `<script src="main.map">` that points at a valid
source map with sourcesContent was read only as a script. Its sources
were never counted and javascript.source_map_sources could miss an
exposed finding.

A script role says how a member is loaded, not what its bytes hold. A
.map member or loaded script whose text parses as a source map, a JSON
object with version and mappings, sources, or sections, is now read as
a source map. Anything else is still read as a script, so a SEA main
script named boot.map and a .map script carrying an inline map keep
reaching examineInlineMaps. A regression test covers the map case, and
the guide states the rule.

* fix(audit): retract a linker-map guess when a page loads the map as a script

noteScriptTarget() returned early for a target already read, counting its
source-map coverage as skipped but leaving any linker-map guess the first
read recorded. When a directory or ASAR listed a non-JSON app.map before
the index.html that loads it with <script src="app.map">, app.map stayed in
native.debug_companions and the result depended on enumeration order.
noteMapTarget() already retracted such a guess for a late map declaration.

A late script declaration now retracts the guess too, and still counts the
script as skipped because it was never examined as JavaScript. A test
covers the directory and ASAR order, and the guide states the outcome.

* fix(audit): respect script and native identity for sibling linker maps

classifySiblingMap() guessed a linker map for any sibling .map whose bytes
could not begin a JSON object. A sibling boot.map that a selected index.html
loads with <script src="boot.map">, or an ELF image named release.map beside
a selected executable, was therefore reported in native.debug_companions.

scanSiblings() now skips a sibling that is a script target as well as a map
target, leaving noteUnreachedTargets() to count it, and classifySiblingMap()
records no guess when sniffNativeFormat() recognizes the header. Tests cover
both cases and the guide states the outcome.

* fix(audit): read a script-loaded map from the text that recognized it

scriptHoldsMap() recognizes a .map or script target as a source map from
decodeMemberText(), which honors a UTF-16 byte order mark, but
readScriptAsMap() then passed the raw bytes to readSourceMap(), which reads
UTF-8. A BOM-marked UTF-16 main.map loaded by <script src="main.map"> failed
the second parse and its prefix became a linker-map guess.

readScriptAsMap() now hands the decoded text to readSourceMapText(), which
readSourceMap() also uses for byte input, so both stages read the same text.
A test covers a UTF-16 map with embedded sources.

* fix(release-exposure): honor byte order marks in binary plist data

A binary plist data object holding BOM-marked UTF-16 text was decoded as
UTF-8, so credential markers in it went unmatched while coverage still
read complete. Decode such payloads with the encoding their mark declares,
and leave coverage incomplete for UTF-32, which no reader scans.

* fix(audit): keep an unprobed package member past the text allowance unknown

Once the cumulative text allowance was spent, examineTextMember() counted a
packaged member named like text, such as payload.json in a .NET or
PyInstaller bundle, as skipped text without reading its header. Coverage
stayed complete even though the member could be a native image or a nested
ASAR, while the same bytes named payload.bin kept coverage unknown.

An unexpanded package member whose kind the owner did not state now leaves
coverage unknown with a limitation when the allowance skips it, the same
uncertainty an unread .bin member carries. No extra read is made, so the
allowance still bounds what is opened. A test covers an ASAR with an
embedded source map named payload.json after sixteen full text members.

* fix(release-exposure): require a read host for every Electron layout

A directory with several resources/app.asar layouts counted one named
host as fuse coverage for all of them. Track host coverage per layout so
safe fuses stay unknown when a whole layout's possible hosts were
dropped from the native image sample.

* fix(release-exposure): keep a .map script past the target allowance unassessed

When HTML script references spent the resolved-path allowance, a later
.map script target was dropped without its script role, so its JavaScript
bytes settled as a linker-map guess and debug_companions reported an
exposure. A refused script target now sets mapTargetsDiscarded, the same
flag a refused map target sets, so a later .map stays unassessed instead
of becoming companion evidence.

* fix(release-exposure): read a Node shebang as script in any text member

A selected or listed text file such as runner.txt that starts with a Node
shebang went straight to the text reader, which never gave it a JavaScript
role, so its inline map went unread and source_map_sources read
not_observed. The text reader now takes the role from the bytes it read,
so every route into it sees the shebang, and the probed-text path no longer
sets the role itself.

* fix(release-exposure): exclude Go search lists by position, not by value

compilerPaths dropped every literal path whose text equalled a forwarded
-rpath or -rpath-link list, so CGO_LDFLAGS=-Wl,-L,$ORIGIN -Wl,-rpath,$ORIGIN
lost the literal -L directory and go.build_paths read not_observed. The
literal flags are now read with each search list option and its argument
removed by position, and the search lists are still read entry by entry.

* fix(release-exposure): keep a probed Node launcher's role past the allowance

The probe recognized a Node shebang but handed the text reader the member
without its JavaScript role. Once earlier text spent the shared allowance,
the text reader returned before reading the header again, so an
extensionless launcher added no skipped map and both source-map checks read
not_observed instead of unknown. The probe now passes the role it found.

* fix(release-exposure): decode a map in the encoding its byte order mark names

The map reader, its prefix check and the sibling classifier decoded map
bytes as UTF-8, so a generic symbols.map holding BOM-marked UTF-16 source
map JSON failed to parse and its prefix read as a linker map: a spurious
debug companion and source_map_sources not_observed. Map bytes are now
decoded by their byte order mark at that boundary, and a UTF-32 map, which
this reader does not decode, stays unresolved.

* fix(release-exposure): keep a lone dash inside a quoted Go path argument

splitGoField split a quoted field before every whitespace-led dash, so the
retained -ldflags '-s "-L=/tmp/private - sdk" -w' read the dash inside the
directory as a new flag and goPaths reported /tmp/private. A quoted field
now splits only before a dash that begins a flag, so a dash followed by a
space stays part of the path.

* fix(release-exposure): choose possible Electron hosts within each layout

The named-or-possible host choice ran across every kept image, so a named
a/a.exe removed the unnamed b/launcher.exe of a second ASAR layout from
electron_host_paths, and its unresolved fuses no longer held
electron.run_as_node at unknown. Each layout now chooses its own named or
possible hosts, and the sets are combined.

* fix(release-exposure): read a relative compiler --sysroot as a build path

The compiler path flag list had no --sysroot, and the rooted-path fallback
cannot see a relative operand, so CGO_LDFLAGS=--sysroot=private-sdk left
go.build_paths at not_observed. --sysroot is now a compiler path flag,
attached with = or given as the next field.

* fix(release-exposure): recognize Windows Electron layouts by the owner's path

The desktop owner gives each file a native local_path, such as
C:\ship\App\resources\app.asar on Windows, and desktopPathConventions only
splits on /, so every ASAR basis was skipped: host ranking and
electron_host_paths stayed empty and a named host with no sentinel let a
safe helper read electron.run_as_node as not_observed. Each basis now
carries whether the owner's slash-separated path is an ASAR layout, and the
layout folders read a Windows absolute path with Windows separators on any
host.

* fix(release-exposure): read a Bun module by its JavaScript loader

An extensionless Bun contents member such as $bunfs/root/release carries
loader_name js, but ownerStatedKind kept only the sourcemap region, so the
member was never read as a script and its inline map went unseen. Give a
Bun contents region the javascript role for the js, jsx, ts and tsx
loaders, as SEA main scripts get it.

* fix(release-exposure): read a Node shebang .map as a script

An undeclared `.map` was routed as a source or linker map before its
first bytes were checked, so a Node script named `runner.map` recorded a
linker-map companion and never had its inline map read. When the map
read of an undeclared `.map` starts with a Node shebang, its map slot is
returned and the text reader examines it as JavaScript, so its inline
map is parsed and no companion is reported.

* Detect full-length GitHub refresh tokens

Narrow github.token to gh[opus]_ with 36 characters and give ghr_ its
own 76-character github.refresh_token rule, with a test.

* fix(release-exposure): bind a negative package probe to the inventoried image

An on-disk native image whose embedded-package probe finds nothing clears member coverage, so it now hashes the probed descriptor and compares it with the inventoried digest before accepting absent. A replacement after inventory fails as a changed member instead of reporting complete coverage.

* fix(release-exposure): let an app bundle read settle only its own layout

An app target, an app-bundle fuse read or a sampled selected executable used to mark the Electron executable as included for every ASAR layout, so safe outer fuses reported not_observed even when a nested Electron app's host was never read. Those reads now settle only their own layout; every other discovered layout still needs its own host read, so an omitted nested host keeps fuse checks unknown.

* fix(release-exposure): honor a Node shebang before guessing a sibling linker map

A sibling .map beside the selected file that starts with a Node shebang is a script, as it already is for a member .map, so classifySiblingMap no longer records it as a linker-map debug companion.

* fix(release-exposure): name a bundle's own layout by the owner's path

The settled layout set joined the caller's bundle spelling with Contents, so a selected /tmp/App.app missed the owner's resolved /private/tmp/App.app/Contents layout and safe fuse checks fell to unknown. Each archive basis now carries the owner's relative layout folder, and the bundle's own layout is the one whose relative folder is Contents, looked up by the owner's resolved folder.

* fix(release-exposure): read file-valued linker options as build paths

LINKER_PATH_FLAG listed --script but not the other linker options that read a file, so -extldflags=-Wl,--version-script=private-versions.map left go.build_paths not_observed. --version-script, --dynamic-list and --retain-symbols-file now read their operand as a path, attached or in the next field.

* fix(release-exposure): count a macOS bundle's framework and MacOS hosts in its layout

A .app layout resolves to its Contents folder, but its hosts are Contents/MacOS/* and the Electron framework binary under Contents/Frameworks, so per-layout host ranking and fuse coverage dropped them and one bundle's safe fuses could settle another bundle. Those images now belong to the bundle's layout, with the framework binary ranked as its host.

* fix(release-exposure): keep a bundle's launcher out of its fuse hosts

A macOS bundle's Electron Framework binary carries its fuses. When it
is among a layout's named hosts, it is now the layout's only fuse host;
the MacOS launcher beside it is still sampled but no longer leaves safe
fuse checks undecided when it has no sentinel.

* fix(release-exposure): keep an undecided PE package member out of linker maps

An unexpanded package member named .map whose PE signature lies past the
retained prefix fell through to noteUnreadMap, which read its MZ prefix as
a linker map and reported native.debug_companions exposed with complete
coverage. The map route now records the same undecided-PE uncertainty the
binary route does, so the member stays unexamined and coverage unknown.

* fix(release-exposure): scan the text of an undecided PE map member

A packaged .map member whose MZ header is undecided returned before its
fully read bytes reached the credential scanner, so a PEM key in it read
as unknown instead of exposed. Scan bytes read in full before returning,
while still keeping PE coverage undecided.

* fix(release-exposure): keep an oversized text member's undecided PE header

A packaged text member past the per-member allowance keeps only its prefix,
so an MZ header whose PE signature lies further in returned null and the
text route dropped the native and package uncertainty. Note the undecided
header as the map and binary routes do.

* fix(release-exposure): join Electron layouts by caller path and keep partial discovery unknown

The desktop owner resolves its root, so a target selected through a
symlinked parent got canonical local_paths that never matched the
audit's caller-spelled images, and the named host dropped out of
electron_host_paths. Rebase owner paths under the canonical root onto
the selected path.

When discovery stopped after it found one Electron layout, the basis
list was treated as exhaustive. Report the layouts as incomplete so safe
fuses on the listed host stay unknown.

* fix(release-exposure): keep Electron hosts under a trailing-slash selection

Join the rebased archive path with appendCallerPath so a selected
`/work/app/` yields `/work/app/resources/app.asar`, whose layout folder
matches the inventoried executables' parent.

* fix(release-exposure): keep maps unassessed for unread text past the allowance

A text-named member such as `runner.txt` can start with a Node shebang.
Once the text allowance is spent its header is not read either, so count
every unread member that could hold a script as a skipped map instead of
trusting the filename-derived role.

* fix(release-exposure): keep an unread packaged map member undecided

When a packaged map member yields no header (a failed read or no map
slot), its name is no evidence of its format. Route it through
noteUnreadBinary, as the text route does, so a member that may be a
native image or package keeps coverage unknown.

* fix(release-exposure): keep an undecided sibling PE header out of linker-map guesses

A sibling .map past the map allowance keeps only its probe, so an MZ
header whose PE signature lies further in is undecided. Share the
undecided-PE test with packaged members and count such a sibling as a
skipped companion instead of guessing a linker map.

* fix(release-exposure): keep possible fuse hosts when no ASAR layout names one

Without an ASAR basis no image is ranked, so no image counted as a fuse
host and one image's safe fuses could settle a sentinel-less sibling.
While the desktop family is undecided and no layout names a host, treat
every sampled image as a possible host.

* fix(release-exposure): keep images outside a found ASAR layout as possible fuse hosts

An asar: false build beside a discovered ASAR layout gets no basis, so
its executables were never fuse hosts and the layout's safe fuses could
settle a sentinel-less image in the other build. When no name picks out
a host outside the found layouts, treat every sampled image there as a
possible host, as the no-layout case already does while the family is
undecided.

* fix(release-exposure): keep a dropped host outside a found ASAR layout unread

An asar: false build beside a found ASAR layout names no host, so an
executable outside every layout that the native-image sample dropped may
be that build's unread fuse host. Coverage no longer counts as proven
when such an image was inventoried but not sampled.

* fix(release-exposure): read Go build-path response files named in compiler and linker flags

A CGO setting such as `-Wl,@/tmp/private-link.rsp` or
`-Xlinker @/tmp/private-link.rsp` names a response file the linker reads
options from, and a bare `@FILE` is read by the compiler driver. Report
the file's path, relative or rooted like other path arguments, without
opening it. An option's own value, such as `-install_name
@rpath/libx.dylib`, and dyld runtime tokens are not response files.

* fix(release-exposure): keep a dropped host outside an ASAR layout unread whatever its name

The guard for a sampled-out host beside a found ASAR layout ruled images
out by filename, so an asar: false host named like `b/My.App` was missed
and safe fuses from the sampled layout settled the installation. Every
inventoried native image outside the found layouts that the sample
dropped now keeps the fuse checks unknown.

* fix(release-exposure): keep maps unassessed for an unreadable extensionless launcher

An unclassified file whose initial header probe fails, such as an
extensionless `runner` that became unreadable after inventory, may carry
a Node shebang and a map directive. The probe failure now leaves its
maps unassessed, as an unread `runner.js` or packaged member does,
instead of counting the file as holding no script.

* fix(release-exposure): read a response file named like a dyld token

`-Wl,@rpath/private-link.rsp` names a linker response file, not a dyld
runtime path, so dropping every `@rpath` or `@loader_path` argument hid
it. A dyld token is now skipped only as the value of an option that takes
one, such as `-install_name` or `-dylib_file`.

* fix(release-exposure): keep the partial member scan on ordinary cancellation

A scan cancelled after some members were examined reported its partial
scan only when cleanup also failed, so a clean cancellation dropped the
recovered source-map and credential observations. A cancelled scan now
hands its assembled facts to the interrupted-scan visitor with unknown
coverage and a cancellation limitation, and the caller still receives
the cancellation error carrying that partial audit.

* docs(release-exposure): describe the partial report kept when the member scan is cancelled

Since the member scan keeps what it examined on ordinary cancellation,
the status guide no longer says that cancelling the scan returns no
partial report. It now describes the partial observation and limits the
no-report case to cancellation before the scan can be assembled.

* fix(release-exposure): keep maps unassessed when a text header is cut short

A text-named member read with one or two bytes of the text allowance
left returns only `#` or `#!`, which cannot establish a Node shebang, so
`runner.txt` kept its non-script role and its unexamined inline map was
treated as absent. A partial or missing header that could still start a
shebang line now leaves the member's maps unassessed, as an unread
member's do once the allowance is spent.

* fix(release-exposure): keep dotted executables as possible hosts in ASAR layouts

Inside an ASAR layout, a sibling whose name had a dot and no `.exe`
suffix, such as `my.app`, was neither ranked for the native sample nor
counted as a possible Electron host. With shared libraries filling the
sample, that image could be dropped while the layout still counted its
host as read, so safe fuses on a helper reported `run_as_node` as not
observed. A dotted sibling that is not named like a shared library is
now sampled after plain siblings and counted as a possible host.

* fix(release-exposure): split env -S shebangs on unquoted blanks only

The env shebang reader split the whole line on spaces before handling -S,
so a launcher such as `#!/usr/bin/env -S NODE_OPTIONS='--conditions
development' node` named `development'` as its program and its maps were
never assessed. An -S operand now takes the rest of the line and is split
the way GNU env -S splits it: single and double quotes, backslash escapes,
\_ and \c, and a leading # comment. A string env itself rejects, such as an
unterminated quote, runs no program.

* fix(release-exposure): keep owner facts completed before cancellation

When a Go, Rust, fuse or desktop owner finished its Evidence and then saw
cancellation, completeProviderEvidence returned that Evidence as the
error's partial observation, but the audit only kept the error and
substituted a not-run fact, so a recovered Go build path or Rust package
list reported unknown. Each owner's Evidence-to-fact reading now lives in
one function used for both a successful read and a completed partial, so
the partial passes the same schema and inventory digest checks before its
fact is kept. Later owners still do not run.

* fix(release-exposure): keep env -S programs named by a variable unassessed

`env -S` expands `${NAME}` from the environment, so a launcher such as
`#!/usr/bin/env -S ${NODE_BIN}` may run Node, but the shebang reader kept
the placeholder as the program name and treated the file as plain text,
reporting its inline map as absent. The env splitter now marks fields that
expand a variable, and a program whose name holds one leaves the shebang
undecided: the member's maps count as unassessed, and a `.map` companion
with such a header is not taken as a linker map. Quoted or escaped `${`
stays literal, and a resolved basename such as `${DIR}/bin/deno` still
decides.

* fix(release-exposure): keep the ASAR header admission for selected files

A directly selected app.asar whose header exceeds asar.max_header_bytes
now returns the typed header admission refusal and its recovery, as the
directory scan already does, instead of a generic partial report.

* fix(release-exposure): keep a map member with a variable env -S program unassessed

An undeclared `runner.map` whose `env -S` program is `${NODE_BIN}` may
run Node, so `examineMapMember` now treats an undecided `nodeShebang`
result as an unassessed map and companion, as `classifySiblingMap`
already does, rather than falling through to a linker-map guess.

* fix(release-exposure): read a forwarded -Map linker map path

splitForwardedLinkerFlags hands -Wl,-Map=private-link.map to the linker
side, but LINKER_PATH_FLAG had no Map entry, so the relative map path was
dropped and go.build_paths read not_observed. Add Map to the file-valued
linker options so -Map=FILE and -Map FILE are both reported.

* fix(release-exposure): count unread text in undecided map branches

An oversized .map whose shebang names its program with an unresolved
variable, or a package member with an undecided PE header, returned
before counting its unscanned text, so credential markers could read
not_observed. Count any non-bytes read as skipped text in both branches.

* fix(audit): bound map reads and preserve linker paths

* fix(audit): retain unread package uncertainty

* fix(audit): retain ambiguous linker operands

* fix(audit): preserve compiler path separators

* fix(audit): retain distinct linker map occurrences

* fix(audit): apply map declarations to their exact source path

A declaration recorded only its NFC lookup key, so every member or
sibling sharing that key took the declared JavaScript-map route. On a
normalization-sensitive filesystem an unrelated linker map spelled in
another form lost its companion evidence when the declaring script was
scanned first.

Record the resolved spellings each declaration writes and treat only an
exact match as declared. Another spelling in the bucket is read as an
undeclared map; when no declared spelling is reached, its linker-map
guess stays unresolved coverage rather than companion evidence.

* fix(audit): keep partial inventory from settling fuse-host coverage

When a directory read found ASAR layouts but the inventory stopped
early, a separate asar: false build outside those layouts may never
have been listed. Only sampled-then-dropped images counted as a
possible missed host, so reading every found layout's host reported
safe fuses as not_observed for the whole selection.

In limitNativeSample, treat an incomplete inventory as a possible
missed host outside the found layouts unless the read root is itself
a found layout, where host identity already settles coverage.

* fix(audit): apply script targets to their exact source path

A page's script element was recorded only by its NFC lookup key, so on
a normalization-sensitive filesystem a distinct file spelled in another
normalization form inherited the JavaScript role. An unrelated linker
map such as café.map beside a loaded café.map was never classified
and debug companion coverage could settle as not observed.

Keep the resolved spellings each page wrote under the lookup key, as map
declarations already do, and give the script role only to a member or
sibling whose exact path a page loaded.

* fix(audit): keep partial inventory uncertain beside a root ASAR layout

When the read root was itself a found ASAR layout, an incomplete
inventory was trusted to have listed every possible fuse host. A second
asar: false build under a subdirectory names no layout and sits outside
the root layout, so an inventory that stopped after the root host could
miss its launcher and still report the root's safe fuses as
not_observed. The same launcher, once listed, already kept the result
unknown.

Drop the root-layout exemption in limitNativeSample so any incomplete
inventory beside a found layout keeps fuse-host coverage unknown. A
complete inventory that lists only the named host still settles.

* fix(audit): read the linker's --just-symbols file operand

A Go build that forwarded -Wl,--just-symbols=private.syms through
-extldflags or CGO_LDFLAGS kept the relative symbol file in its build
settings, but the forwarded linker matcher knew only the short -R
spelling. Complete metadata then reported go.build_paths as
not_observed while the equivalent -R build was exposed.

Add --just-symbols to the linker options that read a file. Only -R
turns into -rpath when given a directory, so the $ORIGIN ambiguity
check stays on the short spelling.

* fix(audit): anchor expanded package members to the selected path

An inventory-expanded package member reports its location relative to
the selected path, such as app.asar/boot.map, but its lookup identity
resolved that location against the process working directory. An
on-disk page loading app.asar/boot.map, or an absolute file: map
declaration into the archive, resolved under the selected root, so the
declaration never matched the member it named. A script target with an
inline map then settled as a linker map, and a declared map stayed
skipped.

Resolve every member lookup and declaration base against the selected
path, and keep the reported location as written. Linker-map retraction
compares the same anchored spelling.

* fix(audit): read GCC's -I- include barrier as an option

CGO_CFLAGS=-I- is GCC's include barrier, the same as --include-barrier,
not -I with the directory "-". The compiler path matcher read it as a
build path, so a Go binary retaining it reported go.build_paths exposed.
Skip that exact field before path matching.

* fix(audit): keep packaged-image uncertainty after reading the host

A native image stored inside a package is never opened by a fuse reader,
so reading the Electron host on disk does not settle its fuses. Count the
packaged omission toward a partial fuse sample on its own, so safe host
implications stay unknown instead of not_observed while a packaged image
is unread.

* fix(audit): require every named host in a layout to be sampled

An ASAR layout can name more than one host, such as `a/a.exe` beside
`a/electron.exe`. The sample counted the layout as read when any one
named host was kept, so dropping the other still reported the Electron
executable as included and let safe fuses from the sampled images settle
run_as_node as not_observed. Count a layout as read only when every host
it could have was sampled, reusing the named-host selection the fuse
reducer already applies.

* fix(audit): keep a layout-less bundle read from settling partial inventory

A bundle with no ASAR layout, such as an asar: false build, counted its
framework read as covering the Electron host even when inventory stopped
early or the sample dropped an image. A nested build's host could sit in
the part never listed, so the root framework settled RunAsNode as
not_observed. Apply the same incomplete-inventory guard the layout branch
uses to the bundle read.

* fix(audit): read the linker's --sysroot directory operand

A Go ELF build that forwarded -Wl,--sysroot=private-sdk through
-extldflags or CGO_LDFLAGS kept the relative sysroot in its build
settings, but the forwarded linker matcher did not know the option.
Complete metadata then reported go.build_paths as not_observed, while
the compiler-owned --sysroot was already recognized.

Add --sysroot to the linker options that take a path, so the -Wl comma
form, the -Wl = form and both -Xlinker forms are read.

* test(audit): group Deno boundary regressions

* fix(audit): preserve Go linker file operands

* fix(audit): track script completion per source occurrence

* refactor(audit): give Go path parsing one semantic owner

* refactor(audit): preserve map roles at the parsing boundary

* refactor(audit): consume toolchain operands once

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-11-11 09:57:55 -05:00
Cursor Agent be20bdfb26 feat(settings)!: replace effect with update_mode and pending_restart
Callers read update_mode to see whether update_analysis_settings can change a limit, and pending_restart only when a configured value is not active in this process.
2026-11-11 19:48:33 +08:00
8bc01b48b4 feat(native): list ELF and Mach-O initializer and finalizer functions (#9574)
* feat(native): list ELF and Mach-O initializer and finalizer functions

inspect_native_initializers and inspect-native-initializers list the
functions an ELF or Mach-O image runs before and after main: ELF
DT_INIT, DT_FINI, preinit, init and fini arrays and .ctors/.dtors, and
Mach-O __mod_init_func, __mod_term_func and __init_offsets, with slot
addresses, resolved targets, symbols where available and relocation
facts for relocatable objects. Nothing is executed. Slot, symbol,
relocation and note budgets are shared across tables, sentinels are
matched at the image width, and target-derived names are bounded and
marked untrusted.

* fix(native): keep ELF initializer slots unresolved when relocation coverage is partial

A skipped, unreadable or over-budget section relocation table now keeps
every slot it could reach unresolved instead of reading the stored word as
raw or dropping a .ctors slot as a sentinel. Dynamic writes that only
partly overlap a slot, or write it twice, leave it unresolved. Linked
images no longer reapply retained non-SHF_ALLOC link-time records.

The dynamic relocation walk now pauses at the shared reader's step
boundaries and observes cancellation there, resolved targets need only
their first byte mapped, and unreadable symbol names make the report
incomplete.

* fix(native): stop reporting unchecked Mach-O initializer words as resolved

Object files now apply their own section relocations: an UNSIGNED entry
against a defined symbol gives symbol_addend, one against an undefined
symbol gives unresolved_import, and any other entry leaves the slot
unresolved. Linked images without a fixup decoder, such as 32-bit ones,
keep pointer slots unresolved when an LC_DYLD_INFO bind stream or
LC_DYSYMTAB external relocations could replace the stored word.

LC_DYLD_INFO rebases now report dyld_info_rebase instead of
chained_fixup, and unreadable symbol names make the report incomplete.
Section headers keep their declared reloff and nreloc for this.

* docs(native): move initializer guide under docs/guides/native

The documentation guide assigns analyst workflows to docs/guides/<domain>/.
Move the guide there, place its navigation and directory entries in the
Native binaries group, and update links from the docs index, CLI guide,
spec and authored skill. The public /native-initializers route is
unchanged.

The guide and spec also describe per-slot relocation coverage, retained
link-time records, Mach-O object relocations, undecoded bind streams,
the dyld_info_rebase resolution and unreadable names.

* fix(native): keep IFUNC and partly relocated ELF initializer slots unresolved

An absolute relocation against an STT_GNU_IFUNC symbol no longer reports
the resolver address as the target. Section relocation records now use
their real write width, so a record that writes only part of a slot,
starts inside it or repeats it leaves the slot unresolved instead of
reading a raw zero or dropping a .ctors slot as a sentinel.

On 32-bit ARM the Thumb bit is cleared when locating a target's section
and file offset, while value_va keeps the stored callable value.

* fix(native): locate Thumb Mach-O initializer targets and reject arm64_32 with a 64-bit header

On CPU_TYPE_ARM the Thumb bit is cleared when locating and naming a
target, since nlist values do not carry it. An arm64_32 image that
declares a 64-bit header is rejected as malformed rather than read with
8-byte slots. The guide and spec describe IFUNC relocations, partly
relocated section slots, Thumb targets and this rejection.

* fix(native): index initializer address lookups and keep ambiguous mappings unknown

ELF and Mach-O initializer lookups now build the shared file mapping
index once per image, so each slot and target lookup is logarithmic in
the number of load segments rather than a scan of every mapping.

An address that load segments map ambiguously no longer falls back to a
section header offset. Its file offset is reported as unknown and the
report is incomplete; only genuinely unmapped spans use the section
fallback.

* fix(native): report Mach-O bind names over the name budget

A bound import name longer than the 1024-byte name budget was dropped
without a trace, although it is the only identity of an unresolved
initializer target. It now counts toward the over-long name limitation
and makes the report incomplete.

* test(native): count mapping lookups instead of timing the load index regression

The load-mapping regression asserted a wall-clock bound, which is
sensitive to machine load. It now spies on the shared mapping module and
requires the same number of mapping scans and index builds for 8 and 64
slots, so a per-slot scan of every load segment fails deterministically
on a small fixture.

* fix(native): yield while decoding legacy rebase runs

* fix(native): preserve initializer mapping and coverage

* fix(native): index legacy fixup spans

* fix(native): preserve relocation uncertainty and bound bind lookup work

* fix(native): validate ELF slot mappings and yield section relocation scans

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-11-09 08:58:12 -05:00
43f06eb1e8 feat(native): recover ELF function ranges from unwind tables (#9585)
* feat(native): recover ELF function ranges from unwind tables

inspect_elf_unwind_ranges and inspect-elf-unwind-ranges read .eh_frame
and .eh_frame_hdr from one local ELF32 or ELF64 image and list the
function ranges its FDEs name, with CIE facts, LSDA and personality,
the containing section and an exact symbol where one starts at the
address. Nothing is executed and no disassembler is needed. Pointer
encodings follow the DW_EH_PE rules with raw zero as null, discarded
FDEs are counted and skipped, the .eh_frame_hdr search table is
compared with the decoded FDEs, and record, row and symbol budgets are
bounded. verify:elf:unwind-ranges compares the reported starts with
llvm-dwarfdump or readelf on a compiled C++ program.

* fix(elf): keep unresolved unwind facts out of reported ranges

Skip FDEs whose initial location is indirect and do not locate
.eh_frame through an indirect eh_frame_ptr, since pointer slots are
not functions. Stop an unbounded header-located scan at a CIE with an
unrecognized augmentation, report ignored duplicate PT_GNU_EH_FRAME
headers as incomplete coverage, and accept a null personality stored
under an indirect encoding.

Keep only symbols at decoded FDE starts, cache name observations per
string-table offset, and decode names only for the returned page under
a fixed 16 MiB text allowance, so repeated or long names no longer
grow memory with the symbol count.

* fix(scripts): run the ELF unwind verifier under owned process trees

Launch probes, compiler drivers, the stripper, the frame dump and the
inspection through createVerifierCommands, as verify-binary-hardening
does, so a timeout or interrupt stops the whole tree. The temporary
directory is removed only after the owned commands have closed.

* docs(elf): move ELF unwind ranges guide under docs/guides/native

The documentation guide assigns analyst workflows to docs/guides/<domain>/.
Move the guide there, place its navigation and directory entries in the
Native binaries group, and update links from the docs index, CLI guide,
spec and authored skill. The public /elf-unwind-ranges route is unchanged.

* fix(elf): give unwind text limits their own recovery and skip indirect tables

Throw a dedicated failure when a page's symbol names exceed the 16 MiB
text allowance, and map it to a resource error that tells the caller
to request a smaller limit rather than raise max_ranges.

Read a CIE augmentation with one bounded NUL search and keep it as a
byte view, decoded only for returned CIEs, so a long augmentation is
never built up character by character.

Leave an .eh_frame_hdr search table with an indirect encoding unread:
the header is incomplete, the comparison is unavailable, and the table
no longer bounds a header-located .eh_frame scan.

* fix(elf): resolve header count and table forms and name IFUNC resolvers

Leave a non-zero .eh_frame_hdr FDE count under an indirect encoding
unresolved, since it is the address of a slot that is not followed, and
mark the header incomplete without reading the table.

Treat a DW_EH_PE_omit table encoding as an omitted search table before
testing the indirect bit, so a header that declares a count without a
table stays present with complete coverage, and accept that form in
output validation.

Match STT_GNU_IFUNC symbols at exact FDE starts after function symbols,
with a limitation that the name is the resolver, not the implementation
it selects at run time.

* fix(elf): require allocated unwind pointer bases

* fix(elf): map unwind header relative base

* fix(elf): validate unwind load mappings

* test(elf): cover mapped nonallocated frame agreement

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-11-09 21:42:53 +08:00
f21fda0fc0 feat(elf): report a Linux kernel image's version banner and embedded build config (#9573)
* feat(elf): report a Linux kernel image's version banner and embedded build config

inspect_kernel_build_config and inspect-kernel-build-config read one
local vmlinux, raw arm64 Image, gzip bzImage or zImage, or configs.ko
and report its Linux version banner and the IKCONFIG .config embedded
between IKCFG_ST and IKCFG_ED, without executing it. A config is present
only when a gzip member ends exactly at IKCFG_ED, passes its CRC-32 and
ISIZE checks and parses as .config text; framing that fails is reported
malformed, never absent. bzImage gzip payloads are inflated within a
budget and other codecs are named as unsupported, using codec detection
now shared with the kernel module parser. The config search, inflate
sizes and option counts are bounded, scans honor cancellation, and
target-derived text is marked untrusted.

* fix(elf): reject a gzip member whose header checksum fails

When FHCRC is set, compare the low 16 bits of the header CRC-32 before
inflating, so a damaged header no longer yields a present config or an
inflated bzImage payload.

* fix(elf): detect the bzImage codec inside the declared payload

Codec detection now reads the same payload_offset/payload_length slice that
is inflated, so bytes past a short payload cannot name its codec.

* fix(elf): report a framed config that does not decode as malformed

A damaged or cut-off deflate stream between IKCFG_ST and IKCFG_ED was
skipped and could end as an absent config with complete coverage. It is
now malformed with config_gzip_invalid, and a later valid config still wins.

* fix(elf): scan config lines in place and accept long string values

Config text is read one line at a time instead of split into an array, and
a quoted value is validated before it is decoded once. The 4096-character
string cap is removed: Kconfig has no such limit and max_config_bytes
already bounds every line, so a long CONFIG_EXTRA_FIRMWARE list is no
longer reported as malformed.

* fix(elf): let a live cancellation reach the kernel config parser

The parser ran synchronously, so an abort raised during parsing could not
change the signal before it returned. Inflation now runs on the zlib thread
pool and each scan yields to the event loop once per MiB, so the existing
cancellation checks observe a real abort.

* refactor(elf): carry the refused kernel config budget as a typed fact

Budget failures are now KernelBuildConfigBudgetFailure with a knob field,
and the provider builds its recovery advice from that field instead of
searching the diagnostic message for an option name.

* docs(elf): move the kernel build config guide under docs/guides/native

The documentation guide assigns analyst workflows to docs/guides/<domain>/.
Move the guide there, place its navigation and directory entries in the
Native binaries group, and update links from the docs index, CLI guide,
spec and authored skill. The public /kernel-build-config route is unchanged.

* fix(elf): yield while checking one long config line

A config made of one long quoted value never reached the per-line yield, so
the control-byte check, value validation and SHA-256 ran in one synchronous
block. Each now works one MiB window per event-loop turn, and the line
patterns match only a bounded name prefix instead of the whole value.

* fix(elf): return paged config entries that do not pin the decoded text

Entry names and values were V8 slices of the whole decoded config, so a
one-entry page kept the full text alive. Entries in the returned page are
now standalone copies, so retained memory follows the page size.

* fix(elf): yield while kernel marker searches find nothing

IKCFG_ST, IKCFG_ED and banner searches used one Buffer.indexOf over the
rest of the image, so a large image with no marker never yielded. They now
search overlapping one MiB windows and yield after each window without a
match.

* fix(elf): preserve long kernel config names

* fix(elf): frame kernel configs by gzip member boundaries

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-11-09 21:41:56 +08:00
Matt Van HornandMatt Van Horn 645735fc0f feat(windows): inspect Shell Link (.lnk) shortcut files (#7859)
* feat(windows): inspect Shell Link (.lnk) shortcut files

Add inspect_windows_shortcut / inspect-windows-shortcut to read one
local Shell Link without resolving or executing the target. The report
keeps header flags and FILETIMEs, IDList items, LinkInfo volume and
network names, StringData, ExtraData including Tracker droid facts
labelled derived, and derived observations such as argument length
and a minimized ShowCommand.

* fix(windows): tighten Shell Link string reads and coverage

- Decode counted StringData over its full CountCharacters payload.
- Locate LinkInfo string terminators before charging max_string_bytes
  and before decoding; an unterminated string is a coverage issue.
- Mark an IDList without a TerminalID and an out-of-range
  LinkInfoHeaderSize as partial coverage.
- Report arguments_present and argument_length as null when
  HasArguments is set but the field could not be read.
- Derive target_extension from the joined base path and suffix.
- Keep ISO dates for FILETIME and UUIDv1 timestamps before 1970.
- Retain the completed file snapshot on parse failures.
- Reject an empty CLI path before normalization.
- Select the shortcut contract from its family in focused tests.
- Fixture StringData no longer writes a NUL into the counted payload.

* style: format docs index and Shell Link boundary type

Apply oxfmt to the two files format:check reports so Static checks passes.

* fix(windows): reject network share paths for Shell Link reads

inspect_windows_shortcut is advertised as offline, but the Windows path
schema admits UNC paths, and lstat on one can contact an SMB server and
offer the caller's credentials.

Reject UNC, extended UNC, and other host-prefixed device paths in the
provider before any filesystem call. Extended drive paths such as
\\?\C:\x stay allowed. A mapped network drive cannot be told from a
local drive by its path, and the docs say so.

* fix(windows): decode Unicode Shell Link file-entry names

File-entry items with bit 0x04 of the type byte, such as 0x36, store a
UTF-16LE name, but the parser always read it as Latin-1. The read stopped
at the first zero byte, so report.exe became r and an IDList-only
shortcut reported the wrong target_extension. Read those names as
UTF-16LE.

* fix(windows): read volume labels by exact Shell Link discriminator

VolumeLabelOffset 0x14 selects VolumeLabelOffsetUnicode and is not itself a
label offset, and that Unicode field exists for no other value. The parser
treated any offset of 0x14 or more as having both, so a Unicode-only label such
as "OS" also produced a false ANSI label "O", and an ANSI label stored past 0x14
had its own bytes read as a Unicode offset.

Branch on the exact value in parseVolume, the one place that interprets it, and
report a VolumeID too short to hold the selected field. The fixture wrote both
encodings for a Unicode volume, which hid the false label, so it now writes one.

* fix(windows): limit network share rejection to Windows hosts

POSIX hosts read repeated leading slashes as one local root, so an
accepted selection such as ///tmp/shortcut.lnk is a local file there.
Apply the host-prefixed path check only on Windows, and take the
platform through the provider constructor so the Windows path forms
stay covered on every test host.

* fix(windows): compare shortcut icons only against full paths

A stored IDList filename or relative path cannot show that an icon at a
full path is elsewhere, so the comparison reported a false difference.
Report null unless the icon and a recorded target are both full paths,
and assert the missing TerminalID issue by code and offset instead of
its message.

* fix(windows): keep unread ExtraData blocks unknown

A walk that stopped before or inside ExtraData still reported the blocks
that HasExpString and HasExpIcon expect as absent, though they were never
checked for. Report a block as absent only after the TerminalBlock shows the
section was read in full. Without one the block stays unknown and coverage
stays partial.

* fix(native): ignore LNK network provider type unless ValidNetType is set

CommonNetworkRelativeLink NetworkProviderType is only meaningful when the
ValidNetType flag (0x2) is set. Return null otherwise.

* fix(native): accept any ExtraData TerminalSize below 4 as the terminal

MS-SHLLINK requires TerminalSize to be less than 4, not exactly 0. Treat
1, 2 and 3 as the terminal block instead of a malformed block.

* fix(native): derive shortcut target facts from the stored environment target

Include the EnvironmentVariableDataBlock literal target among the target
candidates for target_extension and the icon comparison. Variables are not
expanded and the target is not opened.

* fix(windows-shortcut): keep unexpanded variable paths unresolved in icon comparison

A stored environment target such as C:\Users\%USERNAME%\app.exe could be
compared as different from C:\Users\Alice\app.exe. Variable-bearing paths
are never expanded, so the icon comparison now returns null unless a fixed
target matches the icon exactly.

* test: drop redundant ok assertion before typed error guard

The testing guide asks for the typed error to be thrown before asserting a
successful Result value, and says a preceding .ok assertion adds no coverage.

* fix(windows): point out-of-range LinkInfo offsets at their field

A LinkInfo, VolumeID or CommonNetworkRelativeLink offset that points
past its structure was reported at base + offset. With an offset such
as 0xffffffff that position exceeds uint32, so the report failed its
schema and the service discarded the whole partial result.

Report such issues at the offending offset field instead, which keeps
every issue offset and coverage.failing_offset inside the file. Cover
each LinkInfo offset field in the parser tests and the
LocalBasePathOffset case through the stable filesystem service.

* fix(windows): require RFC 4122 variant before deriving Tracker UUID facts

A version nibble of 1 alone does not identify a version-1 UUID. Also require the clock_seq_hi_and_reserved top bits to be 10, so other GUID layouts keep their raw value and derive no MAC or timestamp.

* fix(windows): ignore DeviceNameOffset fields unless ValidDevice is set

MS-SHLLINK 2.3.2 says DeviceNameOffset and DeviceNameOffsetUnicode are
ignored when the ValidDevice flag is clear. parseNetwork read both
regardless, so garbage in those fields became device-name facts or
malformed_structure issues. Read the flags once and only read the device
name fields when ValidDevice is set.

* fix(windows): keep unsupported Tracker headers as raw data

Check TrackerDataBlock Length (0x58) and Version (0) before decoding.
A block with any other header is retained as raw bytes with a
malformed_structure issue, so it no longer yields MAC and timestamp
facts under complete coverage.

* fix(windows): compare shortcut icon paths by normalized location

Collapse "." and ".." segments and repeated separators in the icon
comparison as text only, never opening a path. ".." stops at the drive
or UNC share root, and \\?\ and \\.\ paths stay verbatim.

Join LocalBasePath with CommonPathSuffix as the target and use the bare
base only when the suffix is empty, as the network share branch does.

Read the selected shortcut with lstat and open only, so a rejected
symlink's destination is never resolved.

* fix(windows): keep the UNC share in the root after repeated separators

normalizePath took the host and the next split part as the UNC root, so
\\server\\share\..\app.exe kept an empty share and let .. remove the
real share. Drop empty parts before picking the root so paths on
different shares stay different.

* fix(cli): parse windows shortcut limits as decimals

The four inspect-windows-shortcut limit options now go through
cliDecimalField like the other inspection commands, so values such as
0x10 are rejected and cliDecimalArguments.test.ts passes.

* fix(native): reject linked parents of windows shortcuts

A parent directory symlink or junction pointing at a UNC share was
followed by the first lstat, since the network check reads only the
path text and the symlink rejection covers only the final component.
On Windows, lstat each ancestor from the drive root down and reject
the path at the first link, before anything follows it.

* docs(native): move Windows shortcut guide and add its feature spec

Move the Shell Link guide to docs/guides/native/windows-shortcut.md and
list it under Native binaries in the site navigation, keeping the
/windows-shortcut public route. Update README, translated README, CLI
page and authored skill links to the new physical path.

Add docs/specs/native/windows-shortcut/ with spec.md, plan.md and
tasks.md, as required by docs/development/documentation.md.

* fix(native): admit the windows shortcut parent walk

On Windows the ancestor link walk ran before the provider's resource
admission, so close() could finish while the walk kept calling lstat.
Run the walk inside the same admission as the read, and check the
caller's signal before each ancestor lstat.

* fix(native): walk the effective parents of windows shortcuts

The ancestor link walk checked every prefix of the path as written, so
C:\alias\..\real\a.lnk was refused when C:\alias is a directory link,
although Windows removes alias\.. before opening the file. Walk the
ancestors left after removing . and .. components, keeping the
caller's spelling, never climbing above an absolute root, and leaving
\\?\ paths as written since Windows does not normalize them.

* test(native): assert the linked parent as a structured fact

The parent link regression matched the phrase "junction, which" in the
refusal message, so a wording edit would fail it. Report the linked
ancestor as observed.linked_ancestor on the input issue and assert that
fact instead of the explanatory text.

* test(native): bound the shortcut close gate and release it in finally

Race the parent walk lstat acknowledgement against inspection settling
with awaitAcknowledgement, then release the gate and join the inspect
and close work in finally so a failure reports its cause instead of
waiting for the Vitest timeout.

* fix(native): walk Windows shortcut parents lazily

windowsAncestors built and kept every parent prefix before the first
lstat, so a path with thousands of components allocated memory
quadratic in its length. It now keeps only the normalized components
and yields each parent from a running prefix as the walk reaches it.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-11-09 20:19:44 +08:00
morluto bf271ba3fb fix(capture): retain safe caller path selections (#9626)
* fix(cli)!: retain repeated WASM input paths

BREAKING CHANGE: caller_input_paths values may now be ordered arrays of strings as well as strings. Clients must accept both forms.

* fix(capture): retain safe caller path selections

* fix(evidence): retain auxiliary selections in direct summaries

* test(wasm): separate SDK schema and acquired-input coverage
2026-11-09 20:12:45 +08:00
morluto 0b42dc56d1 perf(tests): reuse CLI sessions for analysis data cases 2026-11-09 07:59:49 +08:00
morluto 5044e20171 test: focus diagnostics and evidence checks on behavior (#9303)
* test: focus configuration diagnostics on their owners

Preserve shape errors, selected paths, recovery guidance, and unchanged-file behavior across parser, setup, and uninstall boundaries.

* test: focus artifact and evidence checks on behavior

Retain format, identity, malformed-input, and cleanup coverage while removing fixed prose and closing the admitted test source.

* refactor(debugger): use configured session admission limits

* test: focus evidence and diagnostics on behavior

* fix(test): restore verifier export and formatting

* test(config): verify debugger limit ranges

* test(rizin): keep fixture executable authoritative

* test(rizin): isolate debugger fixture environment
2026-11-08 16:39:20 +00:00
morlutoandCursor Agent b81da90354 feat(native): page retained procedure references (#8588)
* feat(native): page retained feature traces

* fix(snapshot): retain linked source evidence for replay

* test(snapshot): verify linked foreign evidence stays scoped

* feat(native): page retained procedure references

* docs(mcp): clarify imported residual questions

* docs(mcp): guide trace reference classification

* fix(cli): report result sources when Evidence publication fails

A trace keeps linked sources on the result. Use those records for the
undelivered source closure as well as the bundle, so a refused output
does not report them as not captured.

* ci: rerun pull request checks

The merge of origin/main did not start the CI workflow for this head.

* fix(native): document shared procedure endpoints and the required selector

Describe kind shared and items.endpoints instead of inline procedure copies.
State that procedure-references requires a positional selector or --procedure.

* fix(native): count procedure-reference endpoints before keeping a row

Include the shared endpoint table in the page budget, with JSON punctuation,
and store a new procedure only when its row is accepted.

* fix(native): measure a procedure endpoint once before its comparison key

Size the candidate before building one canonical identity, and reuse that
key for the selected-procedure and shared-endpoint checks.

* fix(native): reject an oversized procedure endpoint before canonicalizing it

When the JSON size already exceeds every stored identity, skip the canonical
string and keep the row out of the page.

* fix(native): describe procedure-reference direction and coverage

Avoid execution wording in the tool description, and give the CLI direction
option the help text the catalog requires.

* fix(native): compare endpoint size with UTF-8 identity length

Keep one running maximum of stored identity bytes. A multibyte selected
procedure is recognized when its JSON no longer fits the page.

* test(cli): count procedure-references as a dedicated command

The MCP tool has a CLI command, so it is no longer an MCP-only exception.

* test(native): catch the string-length endpoint comparison

Use a 200,000-character multibyte selected procedure and a shorter ASCII
endpoint so the old JavaScript length check drops the selected row.

* ci: rerun after the binary-layout toolchain timeout

The required check failed because the toolchain install exceeded 15 minutes.
The same job completed on the previous head, and this commit does not change
the procedure-reference behavior.

* test: stabilize capture decoder exhaustion coverage

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-11-08 02:16:24 +00:00
morluto 352a846127 test: reduce brittle assertions and test-only runtime paths (#9289)
* test: focus assertions on behavioral evidence

Prune redundant and wording-coupled checks while retaining diagnostic, parser, recovery, and cleanup coverage.

* refactor: remove test-only wrappers and duplicate checks

Exercise production operations through test fixtures, remove unused adapters, and keep managed-member uncertainty guidance under its existing owner.

* test: focus process validation on diagnostic facts

* refactor: remove test-only runtime helpers

Move semantic evidence fixture setup out of runtime and remove unused Windows settle recovery helpers while checking configured defaults independently.

* test: focus artifact diagnostics on format facts

* test: verify retained cleanup guidance delivery
2026-11-08 02:02:31 +08:00
morlutoandCursor Agent 31278c7b5a fix!: qualify call paths by provider discovery (#8443)
* fix!: qualify call paths by provider discovery

* fix: preserve compact call coverage schema guidance

* fix: preserve call discovery reasons in path evidence

* fix(evidence): retain discovery limits in call verification

* test: declare discovery in reconstruction controls

* fix: preserve call discovery across facet limits

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-11-08 01:21:29 +08:00
morlutoandCursor Agent 60f9a4c69a fix(native): show function match limitations by default (#8558)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-11-08 01:10:38 +08:00
morlutoandCursor Agent 2e2f981ccf test: assert absolute-path facts without frozen sentences (#9231)
* test: assert absolute-path facts without frozen sentences

Sibling path tests now require the field path, invalid_format, and regex
expectation, and they record that each negative case ran. NUL rejection
follows the argument that contains the byte instead of searching for the
word NUL.

* test: retain actionable NUL path diagnostics

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-11-07 22:46:17 +08:00
gensokyo-devandgensokyo-dev 98c589e420 perf(tests): reuse bounded CLI workers for native matching errors (#9163)
Co-authored-by: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
2026-11-07 00:08:27 +08:00
2SinceJulyand2SinceJuly 6c08320654 feat(process): add a non-executing dry run to capture_process_scenario (#8469)
* feat(process): add a non-executing dry run to capture_process_scenario

Preparation prompts asked an agent to prepare a scenario without running
it but could only suggest the tool that launches the target. Add
dry_run: true so the CLI and MCP resolve the executable, working
directory, observation paths and admission checks, then return planned
Evidence under the rea.process-scenario-plan predicate. Planned Evidence
is not capture Evidence and comparison tools refuse it as a capture.

The preparation prompt now suggests the dry-run call, and the scenario
identity commitments exclude the operation-only field.

Fixes #8398

* fix(process): redact planned sensitive input and share admission

Address review findings on the dry-run contract:

- Redact sensitive input events in planned Evidence and preserve only
  their byte length.
- Recheck cancellation after the last awaited admission step.
- Run the sensitive-matcher and filesystem-root admission checks that a
  capture performs before launch.
- Assert the prompt advertises the dry_run field without freezing
  explanatory wording.

* test(process): hold the final admission step for cancellation

Add a preparation host seam so the regression can cancel while the last
filesystem admission call is pending, then assert the returned
AnalysisCancelledError retains the caller's reason.

* refactor(process): remove the unused plan alias

Keep the plan schema as the single exported contract and satisfy the
unused-export check.

* fix(process): report admission-time aborts as planning cancellation

`prepareProcessScenarioPlan` returned the admission error verbatim, so an
abort that landed while `admitFilesystemObservationRoots` awaited its
`lstat` surfaced as a cancelled `ProcessCaptureError` with
`operation: process_capture` even though no process ran. Convert that
result through the same `cancelled()` projection the neighbouring checks
use, and pin it with a test.

* fix(contracts): keep the process result invariants on the dry-run union

The output schema for `capture_process_scenario` became a union of the
capture and plan shapes, and the union no longer carried the description
that advertised the cross-field invariants to MCP clients — the boundary
test reads `normalized_result.description`. Attach a description to the
union that names both variants and keeps the capture invariants verbatim.

* test(process): exercise the post-admission cancellation check

The previous regression aborted immediately after starting preparation and
released the held admission afterwards. Preparation awaits runtime path
resolution first, so that abort was always observed by the pre-admission
check; deleting the post-admission "if (aborted()) return cancelled();"
still left the test green.

Signal from the admitRoots seam when admission is actually entered, abort
only after that point, then let admission resolve ok. Preparation now has
to reach the check between a successful admission and the ok result, and
the assertion pins both the AnalysisCancelledError tag and the caller's
reason as its cause.

Verified red-green: removing only the post-admission check fails this test
(expected true to be false at prepared.ok) while the other two pass, and
restoring it passes all three.

* test(process): gate the admission seam on acknowledgement

Race the admission acknowledgement against planning's own settlement and a
bounded deadline, and release and join planning in `finally`, so a planning
result that arrives before admission fails the test instead of stalling it
until the suite timeout.

* fix(contracts): hold plans to the scenario refinements and redact inputs

`processScenarioPlanSchema` described the resolved dry-run response but
dropped every cross-field check the input contract applies, so a plan
could advertise out-of-order events, an event past `timeout_ms`, or a
terminal beyond the renderer cell budget while claiming it had been
validated. Extract those checks into `scenarioRefinements` and refine the
plan's scenario with them.

The plan's input event also carried `sensitive` and an optional
`input_bytes`, which accepted a sensitive event that still shipped its
data. Model the public and sensitive forms as separate exact variants:
public input forbids `input_bytes`, sensitive input requires an empty
`data` plus its byte length.

---------

Co-authored-by: 2SinceJuly <202121119+2SinceJuly@users.noreply.github.com>
2026-11-06 20:49:20 +08:00
560674e2de feat(native): report binary hardening for ELF, PE and Mach-O (#8524)
* feat(native): report binary hardening for ELF, PE and Mach-O

Documents inspect_binary_hardening and its CLI equivalent, including
per-property states, supporting evidence, read budgets, and one report
per Mach-O slice. Explains that missing protection imports remain
unknown, signature presence does not establish validity, and hardened
runtime requires inspect_signature. Adds hardening links throughout the
documentation. The supplied diff also removes the advanced CodeQL
workflow and configuration, along with snapshot-binding mismatch
guidance.

* fix(native): validate declared payloads and dynamic tables before hardening rows

Run the shared range checks for ELF, read imports from the dynamic tables
rather than section metadata, and keep an unresolved symbol count apart from
an absent table. Check every Mach-O command before building rows and retain
only the offsets the rows need.

Make the verifier's readelf output wide, check the emitted format before
parsing Mach-O, and place the sources before the libraries in the link.

* fix(native): share Mach-O segment admission with hardening reads

A segment with a section count its cmdsize cannot hold passed the hardening
reader and could enable restrict_segment. Move the size, section count and
file range checks to a shared admission that readMachoImage and the hardening
reader both use, and pin the ELF load-size case with a regression test.

* fix(hardening): reject Mach-O section payloads past the slice and empty ELF interpreters

Segment admission never checked each section's payload range, so a
file-backed section past the slice still produced rows. Decode sections with
the section header owner and mark the slice malformed when a payload leaves
it; zero-fill and empty sections own no file bytes and are exempt.

admitInterp accepted a NUL first byte in a longer extent, which counted an
empty interpreter as PIE evidence. It now requires a nonempty path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KuWHGvzJkrhCqjtEHUwqyH

* fix(native): read CFG instrumentation from decoded GuardFlags

Use the IMAGE_GUARD_CF_INSTRUMENTED flag that guardFlagRecord already
decodes instead of masking the raw GuardFlags word a second time.

* fix(native): reject ELF table spans a second PT_LOAD remaps

The hardening reader mapped DT_STRTAB, DT_SYMTAB and hash tables through
the one PT_LOAD containing the table's first address. A second PT_LOAD
starting inside the table could map the rest of it from other file bytes
and the reader still reported parsed import evidence.

Cap each mapping at the next PT_LOAD that starts inside it, reject a
requested range that crosses that boundary, and stop hash reads at the
same limit, matching the conflicting-mapping refusal in readElfSymbols.

* fix(native): dedupe ELF note tables only on shared boundaries

The hardening reader dropped any note table whose bytes another table
already covered. A covering SHT_NOTE can hold a non-GNU note whose
descriptor is itself a complete GNU property note, while PT_NOTE names
that inner note directly. Walking only the outer stream missed the GNU
marker and reported CET disabled.

Drop an alias only when it starts and ends on boundaries of admitted
tables, which are note boundaries of the walked streams. Any other
covered alias leaves the feature rows unknown, as a partial overlap
already does.

* fix(native): admit every Mach-O symbol record after a canary match

The Mach-O stack protector scan returned as soon as it found an
undefined ___stack_chk_fail, so a later undefined symbol whose n_strx
lay outside the string table was never checked. The same damaged table
parsed or failed as malformed depending only on record order.

Keep the match and finish the bounded record walk. Records after the
match are only checked against the string table bounds, so their names
are not read or charged to the scan budget. The ELF reader already
admits its whole symbol table before matching names.

* fix(native): accept ELF load aliases with the same file translation

The hardening reader treated every PT_LOAD that starts inside a table,
or that also contains the table's address, as a conflicting mapping.
A load with the same address-to-file translation names the same file
bytes, so tables it overlaps still have one interpretation, and the
canonical ElfLoadMap already merges such spans.

Treat equivalent loads as one interpretation. A load with a different
translation, or the zero-filled tail of an equivalent load, still ends
the usable range, so a remapped table is still rejected.

* fix(native): keep same-span ELF note tables with different alignments unknown

A section and a program header that share endpoints were treated as one
note stream even when their effective note alignments differed. Walking the
same bytes at 4 and 8 can frame different notes, so dropping the second table
could hide a GNU property word and report CET or BTI as disabled.

Require matching effective alignment at both endpoints, and no alignment
change inside the covered span, before treating a table as an alias.
Otherwise mark the feature rows unknown. The fixture's .note.gnu.property
section now declares the class's note alignment, as toolchains emit it.

* refactor(native): read LC_SYMTAB through the shared fixed-word decoder

Move the Mach-O fixed uint32 command layouts out of MachoCommandNames
into domain/apple/machoLoadCommands so there is one owner of those
offsets. MachoCommandNames and the hardening reader now both decode
through it; the hardening reader keeps its own bounds and import policy.

* fix(native): skip Mach-O common definitions in canary imports

A common definition sets N_UNDF|N_EXT like an import but stores its
allocation size in n_value. An object holding only
`int __stack_chk_fail;` therefore reported stack_protector: enabled.

Require a zero n_value before an undefined external counts as an
import, for both nlist widths.

* refactor(native): read linkedit data commands through the shared fixed-word decoder

readMachoLinkeditDataCommand reproduced the dataoff/datasize offsets that
FIXED_WORD_FIELDS already owns. Take the command and decode through
decodeMachoFixedWordFields, as readMachoSymtabCommand does, keeping the
truncation and endianness behavior.

* fix(native): skip empty SHT_NOTE sections in ELF hardening

A zero-sized SHT_NOTE owns no bytes, so the section directory marks it
not file-backed and the hardening reader rejected the whole image as
malformed, while readElfImage accepted the same bytes. Skip empty note
sections before the file-backing check; they have nothing to walk.

* fix(scripts): run hardening verifier commands through the shared owner

The binary hardening verifier started its probes and compiler drivers with
raw spawnSync, which bypassed the src/process/ supervision that AGENTS.md
requires. Every launch now goes through createVerifierCommands, and the
temporary root is removed only after the owner closes cleanly.

* fix(scripts): clean up hardening verifier commands on Ctrl-C

SIGINT or SIGTERM ended Node before withVerifierCleanup ran, so detached
compiler trees and the temporary root could outlive the verifier. The
verifier now turns those signals into an abort that every owned command
receives, so cleanup closes the owner and removes the root before exit.

* fix(scripts): fail the hardening verifier when a signal lands outside a command

Only the owned commands read the interruption signal, so a SIGINT or
SIGTERM during the final cleanup or between commands was recorded and
then dropped, and the verifier exited 0. After cleanup settles, the
verifier now throws the interruption reason. A failure the operation or
its cleanup already raised still leaves first and is not replaced.

* fix(native): fail Mach-O slices with a malformed encryption command

The hardening reader turned every malformed status from the shared
encryption reader into an unknown `encrypted` row, so a short
LC_ENCRYPTION_INFO_64 or an encrypted range past the slice still left the
slice parsed with a full report.

Only two conflicting encryption commands stay an unknown row now. Any other
encryption defect fails the slice like other structural command failures.

* fix(native): ignore STB_LOCAL undefined ELF symbols as imports

A local undefined __stack_chk_fail or __memcpy_chk is never resolved
against another object, so it does not establish an external import.
Skip STB_LOCAL bindings when reading protector signs, and give the
fixture's positive symbols STB_GLOBAL binding.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 01:07:00 +08:00
027c768f07 fix(captures): classify unresolvable capture paths as capture_path input (#7814)
* fix(captures): classify unresolvable capture paths as capture_path input

Fixes #7636

* test(captures): cover unresolvable descriptor paths

---------

Co-authored-by: Abelo9996 <Abelo9996@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-11-05 18:30:07 +08:00
morlutoandCursor Agent 2b740cab36 fix(native): preserve function byte source qualifications (#8606)
* fix(native): preserve function byte source qualifications

* fix(native): recover oversized inline matching sources

* test(native): verify matching byte source recovery

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-11-05 04:01:16 +08:00
星闌 b1b6cb64c2 fix(analysis-view)!: distinguish unavailable native collections (#8291) 2026-11-04 23:53:45 +08:00
Kaoru0822andClaude Opus 5.5 710fad8acf feat(evidence): discover missing Evidence links without transferring records (#8633)
* feat(evidence): discover missing Evidence links without transferring records

An agent could only learn that a retained record's source was absent by reading
complete records or by having a downstream analysis refuse. The summary said
nothing about links, and complete-record retention was easy to mistake for
source closure.

- Add detail: links to get_evidence_bundle and CLI inspect-evidence-bundle:
  paged link rows with retained or missing status and no record payloads, for
  selected records against the whole session or file.
- Add retained-closure traversal, the walk composed workflows require, reporting
  complete or incomplete. Direct reads make no closure claim.
- Count direct and missing direct links in the summary.

Fixes #8454

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(evidence): count a repeated Evidence link once in every delivery

The Evidence schema does not deduplicate evidence_links, so the summary counted
a repeated link twice while link rows and counts reported it once. Both now read
one helper for a record's distinct direct links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(evidence): validate link delivery against the advertised schemas

The test never called tools/list, so neither the shared validator nor the MCP
client checked its replies against what each profile advertises. A schema that
rejected the links result could have passed. Fetch the advertised definition and
validate every request and reply against it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 05:14:34 -08:00
morluto 60149282fc test: strengthen integration oracles and verification (#8295)
* test: strengthen integration oracles and verification

* test: bound lifecycle gates and strengthen workflow checks

* test: verify complete paged and extracted results

* test: preserve bytecode and cancellation evidence

* test: strengthen retained evidence and fixture cleanup

* test: require cross-projection bridge identities

* test: retain fixture owners through cleanup failures

* test: verify graph examples and project selectors

* test: tighten provider and lifecycle regressions

* test: consolidate hex coverage and bound cutter cleanup

* test: bound subprocess output waits and strengthen graph checks

* test: check mcp recovery and preserve cleanup owners

* test: verify structured mcp argument failures

* test: preserve artifact cleanup and gdb limit evidence

* test: preserve evidence identities and bound provider gates

* test: retain browser and comparison cleanup owners

* test: bound startup and strengthen fixture oracles

* test: bound lifecycle gates and verify recovered evidence

* test: verify public outcomes and lifecycle ownership

* test: assert precise evidence and release lifecycle gates

* test: verify selected facts and bound cleanup waits

* test: check retained evidence and decoded records

* test: verify final reports and retain cleanup ownership

* test: bound reviewed startup gates and retain cleanup

* test: verify PE errors and preserve MCP cleanup

* test: preserve MCP fixture cleanup ownership

* test: release provider gates before late startup
2026-11-01 00:15:28 +08:00
morlutoandCopilot Autofix powered by AI 675d4076de test: strengthen regression oracles and cleanup ownership (#8152)
* test: strengthen suite oracles and cleanup ownership

* test: reuse setup across independent scenarios (#8153)

* Potential fix for pull request finding 'CodeQL / Improper code sanitization'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-10-31 00:22:16 +08:00
gensokyo-dev f60e42a9ae fix(mcp)!: bound validation work before tool dispatch (#7905)
Reuse the pinned SDK's combined array-element and object-member admission
before canonical validation, deferred runtime loading and tool execution.
Share a configurable finite allowance across eager/deferred HTTP and stdio,
and project refusals to bounded, actionable transport resource errors.

Preserve canonical CLI/application contracts, caller-selected input values,
ordinary validation semantics and complete Evidence access. Exercise actual
transport boundaries, real artifact/TShark workflows, packaged installation,
continued service and downstream identity/capacity regressions.

Fixes #7882

BREAKING CHANGE: MCP tool arguments now default to 1,024 combined array
elements and object members per call. Select a larger positive finite
REA_MCP_MAX_INPUT_ELEMENTS allowance and restart for larger inline inputs,
or use retained/file selectors where supported. Inputs are never truncated.

Assisted-by: Reimu (gensokyo-skills:reimu-incident-triage)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
2026-10-29 05:56:38 +00:00
morluto f9acfdcf49 fix(cli): retain committed evidence receipts on output failure (#7906) 2026-10-29 05:11:02 +08:00
morluto cc5a4062ff fix(browser): preserve unknown WebMCP comparison facts (#7833) 2026-10-29 05:09:22 +08:00
morluto b0880d660f fix(qlog): exclude hidden custom events from incomplete facts (#7825) 2026-10-29 04:58:53 +08:00
morluto 0a272d5cf5 fix(capture): bound preview headlines by source format (#7832)
* fix(capture): bound preview headlines by source format

* test(capture): cover scalar HAR preview extensions
2026-10-29 01:56:07 +08:00
ShuaixinHuang 9593bc536b fix(protobuf): reject malformed descriptor field declarations (#7730) 2026-10-28 19:52:11 +08:00
gensokyo-devand555aaditya 03f17cf41d feat(android): apply caller-selected R8 mapping evidence (#7456)
Apply caller-selected R8/ProGuard mappings to shared Android inspections, preserving raw identities, exact verification, ambiguity and provenance. Add retained summaries and bounded/cached parsing, with measured real R8/Conscrypt workflows and review fixes.

Fixes #6329.

Co-authored-by: 555aaditya <107977627+555aaditya@users.noreply.github.com>
2026-10-27 15:47:08 +08:00
morluto 4191974010 fix(environment): share retained response declarations
Preserve status independently of malformed content and use one declaration reader for route grouping, state extraction and manifest eligibility. Verify the partial facts through CLI and MCP views.
2026-10-27 05:37:47 +08:00
morluto cc5a96933a test(mcp): validate selected tools without compiling the entire catalog 2026-10-26 18:26:33 +08:00
morluto eb51eb6915 fix(process): preserve selected launch paths and completed command output 2026-10-26 16:46:53 +08:00
morluto c994870f6b fix(inspector): route file locations through URL semantics 2026-10-26 16:02:14 +08:00
69a2faa120 feat: add FreeBuff agent support for REA setup (#2736)
* feat: add FreeBuff agent support for REA setup

Register FreeBuff as a supported setup client using the shared user
agents directory (~/.agents/mcp.json with mcpServers, skills in
~/.agents/skills), which matches what FreeBuff Desktop loads. Adds the
client definition, boundary tests, supported-agent docs, and the skill
reference client list.

* fix(freebuff): detect FreeBuff from its profile directory, not shared .agents

The ~/.agents marker made FreeBuff appear installed for any home where
REA had installed shared skills (the default skill destination, also
Gemini CLI's), producing false doctor failures. Detect FreeBuff from
its own fixed profile directory ~/.config/freebuff-desktop instead,
and add a regression test covering a home with only the shared agents
skill directory.

* fix(setup): require product evidence for FreeBuff discovery

Address review findings at their producing boundaries and preserve directly affected consumers. Source inspection and git diff --check completed. Tests and automated checks were not run at the user’s explicit request.

* fix(uninstall): retain unverified shared client configuration

* fix(clients): preserve scoped removal evidence

* fix(doctor): omit undefined selectedClientIds under exactOptionalPropertyTypes

Passing selectedClientIds explicitly as undefined fails typecheck on this
branch (TS2379), which blocks the focused test workflow before any test
can run. Spread the option only when defined.

* fix(update): separate ownership evidence from product applicability

rea update's maintenance scope inspected only product applicability
evidence, so when a FreeBuff profile was absent but REA already owned a
version-pinned row in the shared ~/.agents/mcp.json, the plan omitted
freebuff and left the integration stale. FreeBuff now declares its
registration storage as shared, which proves ownership of an owned row
but never product applicability, and maintenance requests ownership
evidence through that declaration instead of reinterpreting the shared
paths. Doctor and setup keep readiness evidence, foreign rows stay out
of maintenance, and the Doctor exactOptionalPropertyTypes fix makes the
focused test workflow runnable.

* test(dotnet): split oversized operand extent diagnostics describe

The describe callback reached 162 lines (max 150), failing
max-lines-per-function and blocking the pre-push check for this branch.
Move the single-case extent scenarios into their own describe; no test
content or order changes.

* fix(update): preserve ZCode's disabled shared registration during maintenance

The shared user agents file doubles as ZCode's fallback, where
`enable: false` disables the registration. Ownership-based maintenance
read that row as a FreeBuff registration, whose format does not
recognize the flag, so the row was selected and the emitted setup
command rewrote it, dropping the flag and silently re-enabling a
registration the user had disabled.

registrationActivation now honors `enable: false` for any client whose
registration storage is shared, because the activation fact binds every
reader of that storage. Maintenance leaves such rows out; doctor
reports them as stale, matching ZCode's own treatment.

* fix(doctor): let explicit client selection authorize the config read

When an ACL or an inaccessible shared parent made every access probe
fail, a client selected by name was omitted before the configuration
read could return its unreadable-config detail, so doctor reported it
as not detected with installation remediation: an unresolved selected
path collapsed to absence.

An explicit selection now authorizes the read outright and the probes
serve automatic discovery only. A selected client with an unreadable
config reports invalid with the read failure; one with no config
reports missing; automatic discovery still probes before inspecting.

* fix(uninstall): share shared-storage checks between ZCode and FreeBuff

clients() queued ~/.agents/mcp.json twice: once as zcode_fallback and once
as freebuff. ZCode has no discoveryPaths, so its strict read still blocked
uninstall on malformed shared text, despite FreeBuff's new retain-and-continue
behavior. For a valid owned entry, ZCode removed it first, so FreeBuff reported
skipped rather than removed.

Make the shared file's checks and removal one shared decision:
- Add zcodeDiscoveryPaths to ZCode so it has its own applicability evidence.
- Gate zcode_fallback push on applicable, not just zcodeFallbackPath presence.
- Report shared storage ownership as one decision for both clients:
  FreeBuff and ZCode now use clientOwnershipEvidencePaths together, so a
  recognizable REA-owned row proves ownership once for the shared storage, and
  both clients report the same result consistently.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(uninstall): merge duplicated host and prove shared ownership from owned rows

Uninstall.ts declared systemUninstallHost twice, preventing module load,
with the exported copy returning only clients and the second copy missing
clientWriteBlocker. Merge into one exported host carrying both blockers.

hasOwnershipEvidence used access() existence, so an unrelated malformed
~/.agents/mcp.json established ownership and blocked every removal.
Ownership now requires a readable, recognizable REA-owned row via the
injected filesystem, so unrelated shared data stays retained.

Also cover ZCode discoveryPaths in its path test.

* fix(uninstall): keep denied product-local configurations fail-closed

Unproven product applicability marked every configuration path of that client unverified, so a denied ZCode profile turned an unreadable product-local config into a retained item: uninstall continued and could report complete with REA still registered. The retain-and-continue exception now applies only to storage REA does not own, classified once by clientSharedRegistrationPaths (shared configuration plus another product's same-scope fallback).

Covers the Greptile P1 on Uninstall.ts. Validation: the new test fails on the pre-fix Uninstall and passes after it; freebuff/zcode/updateMaintenance/bom/dsh suites green; clientLifecycle, clientRegistrationStatus and claudeDesktop keep the same 8 host failures as the pre-merge baseline (symlink EPERM, stale-vs-aligned on Windows).

* fix(update): retain FreeBuff when ZCode reads the shared fallback

Maintenance deduplicated clients on the path each inspection read, so ZCode's fallback view of ~/.agents/mcp.json marked that file "covered" and dropped FreeBuff from the scope. ZCode setup writes ~/.zcode/cli/config.json and deliberately leaves the fallback untouched (docs/client-configuration.md), so the emitted command refreshed only ZCode while the shared row both products load stayed pinned to the old package version. Each setup action rewrites only its own configuration path and no other action rewrites the shared row, so every selected client keeps its own maintenance action; the read-path covered/deduped filter is removed.

Covers the Codex P2 on UpdateMaintenance.ts:104. Validation: the new regression fails on e2fdec384 with {clients:["zcode"]} and passes with {clients:["freebuff","zcode"]}; updateMaintenance 31/31, setupClientConfigCleanupOwnership, cliUpdateAlias, updateOutput, cliStartupImports green; tsc --noEmit, oxlint (0 errors) and oxfmt --check clean; clientRegistrationStatus keeps the same 4 symlink/stale-vs-aligned failures this Windows host has on base e2fdec384 (verified by stashing the change).

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* fix(uninstall): preserve completed reads as shared ownership evidence

hasOwnershipEvidence caught every read failure and kept scanning, so a handle-close failure after a successful read — a RegularFileCleanupFailure whose completed outcome still contains the exact shared-file text — was discarded as "no proof". FreeBuff's applicability-unverified storage therefore never got its ownership fact, and once the retained handle blocked later reads, readClientConfiguration downgraded them to retained: inspection passed, removals soft-failed, and runUninstall(true) could purge REA data while the active shared registration remained. The catch now parses the completed outcome for the ownership text while missing or unreadable storage and malformed data keep their proves-nothing semantics, so ownership holds and every later read failure stays blocking.

Covers the Codex P2 on Uninstall.ts:252. Validation: the new regression fails on 4dbfdf1a9 (freebuff reports no blocking failure while the poisoned-scope error lands on claude_code) and passes after; setupClientConfigCleanupOwnership 25/25; the other 21 Uninstall-consumer suites are byte-identical to base 4dbfdf1a9 (61 pre-existing Windows-host failures: symlink EPERM, stale-vs-aligned); tsc --noEmit, oxlint (0 errors, same 6 warnings) and oxfmt --check clean.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* fix(uninstall): keep owned-row proof through removal refusals

hasOwnershipEvidence treated any clientRegistrationRemoval refusal as no ownership: an effective REA-owned rea member shadowed by an earlier foreign duplicate (shadowed_unowned_member), or an owned native row beside a foreign V1 fallback (foreign_legacy_fallback), discarded the already-parsed positive row fact. With FreeBuff unverified and close failures recurring on the preflight/removal rereads, those failures were then downgraded to retained, so runUninstall(true) removed the skill and purged REA data while the active owned row remained. The refusal now carries the owned rows that motivated it — the refusal only refuses the edit — and hasOwnershipEvidence evaluates that fact on both paths; preflight and removal keep refusing the unsafe edit exactly as before.

Covers the Codex P2 on Uninstall.ts:269. Validation: the new regression fails on 225cb214a (freebuff reports no blocking failure while the poisoned-scope error lands on claude_code) and passes after; setupClientConfigCleanupOwnership 26/26; the 21 Uninstall-consumer suites are byte-identical to the recorded 225cb214a baseline (61 pre-existing Windows-host failures: symlink EPERM, stale-vs-aligned); tsc --noEmit, oxlint (0 errors, same 6 warnings) and oxfmt --check clean.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* fix(uninstall): keep unresolved discovery probes from softening shared failures

The discovery probe swallowed every stat failure, so a FreeBuff profile denied with EACCES/EIO was indistinguishable from an absent profile: shared storage was marked applicability-unverified and, when ~/.agents/mcp.json also remained unreadable, the ownership and removal reads softened to retained. runUninstall(true) then removed the skill and purged REA data while an installed FreeBuff could still hold an active registration — the regression even reports complete. Absence (ENOENT) is now the only state that leaves shared paths unverified; any other probe failure leaves applicability unresolved, and shared-config failures stay blocking.

Covers the Codex P2 on Uninstall.ts:318. Validation: the new regression fails on 7572a7082 with status complete and purged data and passes after; freebuffClientConfiguration 13/13; the Uninstall-consumer batches are byte-identical to the recorded baselines (61 pre-existing Windows-host failures: symlink EPERM, stale-vs-aligned); tsc --noEmit, oxlint (0 errors, same warning rules) and oxfmt --check clean.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* fix(uninstall): retain completed configuration reads as removal evidence

A close failure after a completed read discarded the text that read
produced, so an owned registration row the text proves softened to
retained. The completed text now drives removal, keeping shared-config
failures blocking when ownership evidence exists.

Co-Authored-By: Anthropic <noreply@anthropic.com>

* fix(uninstall): stop removal when a cleanup retry leaves a handle unreleased

A completed reread whose handle the cleanup retry cannot release now fails
blockingly instead of serving the text: with no owned row, the soft path
let removeAll purge managed data before close() reported the incomplete
cleanup. The completed text is served only after the retry releases the
handle; an unreleased handle is REA's own failure, never applicability
uncertainty, so it bypasses the retained softening.

Co-Authored-By: Anthropic <noreply@anthropic.com>

* fix(uninstall): keep the ZCode fallback when its profile probe is unresolved

A denied or failed profile probe leaves ZCode's applicability unresolved,
never absent, but the fallback branch dropped the shared row on
applicability alone: with the native config and FreeBuff's profile both
absent, an installed ZCode kept its uninspected registration while
uninstall purged skill and data. Unresolved probes now keep the fallback
in scope with the same fail-closed treatment as other unresolved shared
paths; definitive absence still keeps ZCode out of another product's
primary storage.

Co-Authored-By: Anthropic <noreply@anthropic.com>

* fix(setup): preserve a shared row's enable:false during FreeBuff repair

The shared agents file doubles as ZCode's fallback, so its `enable: false`
binds every reader: doctor now reports the disabled row as stale for
FreeBuff, but following that remediation rebuilt the generic JSON entry
without the flag and silently re-enabled a registration ZCode disabled.
Repairs of shared storage now carry the activation fact forward, while a
client's own native storage keeps its established single-reader repair
contract; the denied-profile uninstall regression also pins the
fallback's new fail-closed inspection beside the denied read.

Co-Authored-By: Anthropic <noreply@anthropic.com>

* test(uninstall): retarget the completed-reread regression to a released retry

The concurrent-writer regression held the reread handle open forever,
which the fail-closed path now serves: it duplicated the unreleased-handle
regression while documenting the opposite contract. The fixture's close
spy gains a failure limit so the cleanup retry releases the handle, and
the test again proves the completed text drives removal of the
concurrently added owned row, with purge verified against the cache
layout purge actually removes.

Co-Authored-By: Anthropic <noreply@anthropic.com>

* fix(doctor): report a preserved shared disable as a manual conflict

Preserving a shared row's enable:false left doctor recommending a setup
run that registrationCurrent correctly treats as already current, so the
scoped doctor returned needs_human forever. The shared-enable
interpretation now has one owner, sharedEnableFlagBinds, used by both
the activation rule and the remediation: a row disabled through the
shared flag reports the manual conflict and states that rea setup will
not change it, instead of an endlessly ineffective repair.

Co-Authored-By: Anthropic <noreply@anthropic.com>

* fix(doctor): keep a native ZCode disable on the setup repair path

The manual-conflict remediation matched a native zcode row through
sharedEnableFlagBinds, but setup drops enable:false on single-reader
storage, so a native disabled registration was told setup would not
change it. The remediation now reads setup's own writer rule,
exported as setupPreservesDisabledActivation: shared storage and the
zcode fallback view keep the manual-conflict message, while a native
row keeps the working rea setup instruction. Regressions pin both
remediations.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* revert: drop the unrelated managed-CIL describe split

The split does not belong to FreeBuff client support and only widens this
PR's diff.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* fix(uninstall): remove the shared agent registration once for both clients

ZCode's ~/.agents/mcp.json fallback and FreeBuff's own configuration queued the
same physical file twice, so ZCode removed the row and FreeBuff then reported
it absent, losing the outcome for the client that owns the file.

Resolve every configuration path to one owner -- the client whose
configuration it is -- and project that single inspection and removal to each
client that loads it. A fallback reader still contributes its discovery probe,
so an installed or unresolved reader keeps the shared file fail-closed.

Discovery also accepted a dangling ~/.config/freebuff-desktop link, which setup
and doctor reject because they follow the target, so an unrelated malformed
shared file blocked every other removal. Resolve the link before treating the
profile as installation evidence, keeping lstat for removal safety.

Also date the FreeBuff release wording against the published 6.7.0 package.

* fix(uninstall): keep a row per client when two clients own one file

OPENCODE_CONFIG can point OpenCode at FreeBuff's configuration file, where
OpenCode writes `mcp.rea` and FreeBuff writes `mcpServers.rea`. Grouping by
filename gave that path a single owner, so uninstall removed only OpenCode's
row, reported that removal for FreeBuff, and could purge managed data with
FreeBuff's registration still active.

Share the decision only through a fallback view of another client's row: every
client that declares the path as its own configuration keeps its own
inspection and removal, and the fallback reader reports each of them.

* fix(clients): preserve discovery and fallback registration identity

Follow product discovery link targets without losing configuration entry
presence or explicitly selected inspections. Associate fallback readers with
the format owner's registration table and project outcomes by owner and path,
so OpenCode removal cannot be reported as a ZCode fallback removal.

All three new boundary regressions fail against the previous source and pass
with the fixes. Validation: check:fast; 217 focused tests passed; the two
changed test files were rerun with 50 passing tests. No installed FreeBuff
client was launched.

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
2026-10-25 11:29:17 +08:00
morluto f3ed1657f8 feat(mcp): support runtime analysis limit recovery (#7049)
* feat(mcp): support runtime analysis limit recovery

* refactor(config): remove unused limit re-exports

* fix(android): keep fixture release metadata dependency-free

* fix(settings): preserve provider budgets and recovery facts

* fix(tests): resolve lint plugins from their repository owner

* refactor(mcp): verify guidance delivery without frozen wording

* fix(android): align availability with runtime timeout

* test(recovery): assert limit facts instead of frozen wording

* fix(mcp): share runtime settings with the default JADX provider

* test(mcp): bound acknowledgements and release held work on failure

* fix(catalog): declare runtime settings tools as MCP-only
2026-10-25 09:45:26 +08:00
morluto 475f09d5ca chore(lint): enforce buffer and cancellation regression guards 2026-10-25 07:31:42 +08:00
b4ba96a9a5 feat(firmware): inspect flattened devicetree blobs (#7009)
* feat(firmware): inspect flattened devicetree blobs

Adds inspect-devicetree and inspect_devicetree for FDT v16/v17 blobs
without target execution or external dependencies. Results include
validated headers, reservations, a node/property tree with bounded raw
previews, and derived standard values and selected phandle references.
Parsing enforces file, tree, and decoded representation budgets and
rejects malformed input without partial success. Summary delivery
supports retained Evidence pagination, with tests covering parsing,
service validation, CLI delivery, and MCP access after the input is
removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe

* fix(firmware): reject conflicting devicetree phandles and bound page summaries

A node whose phandle and linux,phandle disagree no longer resolves under
either value; the conflict is reported as a limitation. Finalization
failures after a completed read retain the file-snapshot-hash partial
observation, matching the Go provider. Devicetree view pages carry
fixed-size context and counts instead of repeating summary strings, so a
large bootargs no longer pushes every page over the MCP response budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-24 22:45:22 +08:00
dba8a31978 feat(elf): inspect Linux kernel module metadata (#7010)
* feat(elf): inspect Linux kernel module metadata

Adds inspect-kernel-module and inspect_kernel_module for ELF32/ELF64
ET_REL Linux kernel modules, read statically without executing them.
Reports ordered .modinfo entries with duplicates preserved, __versions
symbol CRCs, the presence of .gnu.linkonce.this_module and __ksymtab,
.init.text and .exit.text sizes, and an appended module signature
trailer with its PKCS#7 byte range and SHA-256. gzip, xz and zstd
containers are reported unsupported with external decompression as the
next probe. Malformed headers and exhausted entry budgets return no
partial success.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe

* fix(elf): bound kernel module ksymtab listing and tighten contracts

Charge listed __ksymtab* sections against max_entries, omit section names
longer than 256 bytes with ksymtab_name_exceeds_limit, and aggregate the
non-file-backed limitation so repeated huge names cannot inflate Evidence.
Use fixedTupleSchema for the signature pad bytes with projection parity
tests, and assert the late-cancellation reason and retained observation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe

* fix: resolve merge integration errors

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-24 22:35:20 +08:00
c6a2576aba fix(contracts): reject NUL bytes in objdump and rizin local path inputs (#3600)
Co-authored-by: qorvr <330186599+qorvr@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-24 23:13:34 +09:00
3d84215bb2 feat(pe): inspect TLS callbacks and load configuration (#7008)
* feat(pe): inspect TLS callbacks and load configuration

Adds inspect-pe-load-config and inspect_pe_load_config for PE32 and
PE32+ images, with artifact hashes and file ranges in the returned
evidence. Reports callback addresses and mappings, plus size-covered
security-cookie, SafeSEH, CFG, EH-continuation and XFG fields;
flag-derived mitigation claims are explicitly labeled inferred. Missing
directories remain null, and malformed storage or exhausted read budgets
return errors without partial success. Adds documentation and tests for
service validation, CLI/MCP access, malformed inputs and stable-file
handling.

x86 images that declare the legacy 64-byte load-config directory are
read to the structure's own Size when those bytes are file-backed, as
the Windows loader does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe

* fix(pe): retain snapshot facts and bound load-config section lookup

Keep the completed stable-read path, SHA-256 and byte count as a
file-snapshot-hash partial observation when descriptor close fails or is
cancelled. Report the checked Size span as structure_location and add it
to Evidence locations beside the declared directory range. Resolve
callback sections through a sorted segment index built once per image,
keeping the ambiguity failure. Pin every load-config field to offsets
transcribed from the PE/COFF spec and llvm COFF.h at its exact Size
boundary for PE32 and PE32+.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 22:06:41 +08:00
781760a195 feat(apple): inspect IPS crash reports (#6511)
* feat(apple): inspect IPS crash reports

Add inspect_apple_ips for caller-selected Apple .ips files. Parse the
first-line metadata object and, when bug_type is the string 309, the
crash body: process identity, exception and termination facts, threads,
usedImages, lossless integer registers, and documented classification
rules. Other bug types including 288 stackshots return no crash result.
Opened Mach-O and dSYM UUID joins remain unobserved.

Related to #4297

* fix(apple): keep IPS register literals exact

Use the arm64e fixture integer that matches 0x8527000193ed4944 instead
of an IEEE-754-rounded decimal, and count the lossless JSON example
length from the source text.

* fix(apple): budget IPS parsing and keep typed presence

Parse the IPS metadata and crash body through a projection of the
documented members. Members the report does not interpret are
syntax-checked without being materialized, and thread, frame, image and
the new max_registers budgets run before each row is parsed, so a large
selected file fails with a typed resource error instead of first
expanding every row. Fixed allowances bound JSON nesting, unlisted key
names and termination reasons.

Present values whose JSON type differs from the documented one are no
longer read as absence: they are listed in coverage.type_mismatches and
coverage becomes partial, including wrong-type or missing threads and
usedImages collections. osVersion keeps the producer dictionary or the
string form. Frame roles follow physical frame depth, so the top frame
of a non-faulting thread is current_instruction and inlined rows share
the following frame's role. SIGKILL classes compare the canonical hex
once, so 0xdeadd0cc no longer matches dead10cc. Invalid UTF-8 is refused
instead of being replaced, and JSON strings are decoded by copying
unescaped spans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g

* fix(apple): bound integer text expansion and reconcile coverage

* refactor(apple): retain image metadata at its owning row

* refactor(apple): validate row capacity through reconciled coverage

* test(apple): verify resource recovery at the public error boundary

* docs(apple): record complete IPS task and corpus measurements

* docs(apple): record integrated checks and stdio corpus evidence

* docs(apple): record PE integration and baseline cleanup controls

* fix(native): bound IPS string decoding and preserve invalid identities

* refactor(native): use exact optional IPS source evidence

* docs(native): verify IPS escape capacity and complete-task costs

* test(native): verify IPS through production stdio MCP

* style(tests): format IPS advertised schema bindings

* fix(native): preserve unresolved IPS references and recorded frame semantics

* docs(native): record IPS reference review evidence

* fix(apple-ips): enforce JSON and report consistency

* docs(apple-ips): record final consistency verification

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-24 16:02:59 +08:00
4ef916da03 feat(pe): inspect import and delay-load tables (#6509)
* feat(pe): inspect import and delay-load tables

Add inspect_pe_imports / inspect-pe-imports to read PE32/PE32+ import
and delay-load directories without executing the image. The report keeps
DLL names, named and ordinal functions, ILT/IAT RVAs, and a pefile
import hash of the standard import table. A missing directory is
observed absence, not proof the image never imported APIs.

* fix(pe): default PE import fixture options

Allow peImportImage() without an argument so CLI and filesystem tests
can use the KERNEL32 sample without repeating an empty options object.

* test(pe): keep import path cases on the dedicated MCP contract

inspect_pe_imports already rejects relative MCP paths in its own
boundary test. Leave the shared absolute-path matrix to tools whose
handler issue projection matches that suite's expected shape.

* fix(native): index virtual-to-file mapping lookups

mappedFileOffset scans every mapping twice per span, so a parser that
translates one span per table entry costs sections x entries. Add
fileMappingLookup, which builds an O(n log n) interval index once and
answers each span in O(log n) with the same accept, reject and
translation results. A differential test checks it against
mappedFileOffset over randomized overlapping mappings and zero fills.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g

* fix(pe): report partial import evidence and exact pefile hashes

- Withhold imphash with an explicit reason whenever the standard import
  table is partial (unreadable names or thunks, missing tables), or when
  pefile 2024.8.26 would hash a different row sequence: invalid or
  truncated names, ordinal 0, descriptors without functions, tables its
  bounds or repeat/spread heuristics discard, and its 8192-thunk import
  symbol cap. Delay-load issues do not withhold it.
- Hash ordinal imports with pefile's complete ordlookup tables and its
  ord<decimal> fallback instead of declining unmapped ordinals.
- Report a nonterminal descriptor whose FirstThunk is zero, and a
  delay-load descriptor whose IAT or INT RVA is zero, as malformed_entry
  partial coverage instead of a complete empty inventory.
- Directory evidence lists the file spans actually consumed for the
  descriptor array and terminator; discontiguous mappings produce
  separate spans and separate Evidence ranges.
- Carry exhausted limits as PeImportLimitFailure.limit and derive
  recovery from it; the fixed 16 MiB retained-text ceiling reports a
  fixed allowance instead of max_file_bytes advice.
- Translate RVAs through the indexed mapping lookup and yield with a
  cancellation check every 1024 table entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g

* fix(pe): read only own ordinal tables for DLL names

DLL names such as constructor and __proto__ pass pefile's name rule but
resolved inherited object members, throwing before the ord<n> fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g

* fix(native): preserve PE import facts and prove hash compatibility

* test(native): verify PE import workflows and measured costs

* refactor(native): narrow PE facts across the container union

* docs(native): record integrated PE import validation

* fix(native): bound every PE import RVA span

* docs(native): measure RVA guard completed workflows

* fix(pe): reconcile import coverage with retained rows

* fix(pe): retain partial imports at descriptor mapping gaps

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-24 11:44:12 +08:00
morluto 0b1af48a37 fix(protobuf): preserve MIME admission and transport facts
Parse MIME parameters once with strict quoted-value and duplicate handling,
and compare equivalent default binary profiles. Keep excluded HAR MIME
unknown, explicit framing independent, and nominal gRPC sanitization intact
across malformed or repeated declarations.

Add public CLI/MCP and native-header regressions with source identity and
partial-coverage assertions. Preserve upstream wire and schema admission.

Fixes #6836
2026-10-24 06:24:55 +08:00
morluto 53601d0bdf merge: consolidate upstream identity and representation owners 2026-10-24 05:13:41 +08:00
morluto a8fc3307cd fix(protobuf): own descriptor file identity before registry admission
Coalesce equal parsed file definitions and reject conflicting same-name definitions before registry import resolution. Preserve field presence, known and opaque facts through one budgeted SDK parse while retaining original descriptor bytes and digest.

Keep unique files in their original order, reuse the existing admission traversal, and cover public CLI/MCP import, ordering, presence and source-provenance controls. Document REA ambiguity policy separately from protoc precedence.
2026-10-24 05:05:22 +08:00
morluto d13a036e34 fix(reconstruction): retain scoped unknowns in overall claims
Select active constituent-dimension unknowns for an overall comparison claim while preserving comparison identity and exact scope for individual claims. Use the same selected facts for classification, citations, limitations and recommended probes; remove redundant aliases.

Cover stdio MCP registration, aggregate and independent claims, resolved latest revisions, source provenance and foreign comparison scopes.

Fixes #6879
2026-10-24 05:05:22 +08:00
morluto 3e3c9eadb5 fix(protobuf): reject ambiguous descriptor identities
Validate message-local field numbers and fully qualified registry identities during existing descriptor admission before constructing the schema registry. Preserve repeated wire occurrences, enum aliases and independent scopes, with bounded name allocation and retained capture provenance through CLI/MCP refusals.

Fixes #6834
2026-10-24 04:42:09 +08:00
morluto 62f7b5e4cd fix(native): require observed symbol origin for named identity 2026-10-24 04:14:17 +08:00