Merge remote-tracking branch 'origin/main' into fix/full-test-audit-20261110

This commit is contained in:
morluto committed 2026-11-10 16:02:00 -05:00
commit 6377a1831f
11 files changed
+349 -81

No files matched your search

+8 -3
View File
@@ -32,9 +32,14 @@ otherwise.
followed by a version token beginning with a digit and ends at a NUL or
newline within 512 bytes. `version` is that token. `builder` is the first
parenthesized group (`user@host`). `compiler` is the second group up to the
last `) #`, so nested parentheses in compiler strings are kept. The banner is
`absent` when no such string was found in the searched bytes and `unknown`
when the bytes were not searched.
last `) #`, so nested parentheses in compiler strings are kept. Since Linux
6.1, kbuild also leaves the weak banner of `init/version.o`. It is compiled
before the build number and timestamp exist, so unless `KBUILD_BUILD_VERSION`
and `KBUILD_BUILD_TIMESTAMP` are both set, it has a space right after `#` or at
its end. When a later banner repeats that text up to `#` without that gap, the
later banner is reported; otherwise the first one is. The banner is `absent`
when no such string was found in the searched bytes and `unknown` when the
bytes were not searched.
`config` is `present` only when all of the following hold:
+4 -1
View File
@@ -62,7 +62,10 @@ arguments are present, argument character length, target extension, whether
`ShowCommand` is minimized, and whether the icon location differs from the
recorded target path. That comparison is `null` unless the icon and a recorded
target are both full paths: a stored filename, relative path, or `%VAR%` form
cannot show that two locations differ. This workflow does not emit verdicts
cannot show that two locations differ. Windows stores the `LinkInfo` path only
as ANSI when the system code page can represent it. An ANSI path with non-ASCII
bytes is therefore not compared with a Unicode icon location, or the reverse,
because the code page is unobserved. This workflow does not emit verdicts
about intent.
A missing optional section is observed absence of that structure. It is not
@@ -26,7 +26,10 @@ these files.
read through the stable artifact read and never executed.
2. The banner is the first printable `Linux version ` string followed by a
version token that starts with a digit and ends at a NUL or newline within
512 bytes. Version, builder and compiler are split from it. The scan is
512 bytes. Version, builder and compiler are split from it. A banner with a
space right after `#` or at its end is the kbuild placeholder from
`init/version.o`, built without a build number or timestamp; a later banner
that repeats its text up to `#` without that gap replaces it. The scan is
linear in the searched bytes.
3. A config is `present` only when `IKCFG_ST` is followed by a gzip member that
ends exactly at the next `IKCFG_ED`, inflates within `max_config_bytes`,
+3
View File
@@ -524,6 +524,9 @@ const projectError = (
RETRYABLE_CODES.has(code) ||
(error instanceof AnalysisCapabilityUnavailableError &&
error.retryable) ||
(error._tag === "ProcessCaptureError" &&
"retryable" in error &&
error.retryable === true) ||
(error instanceof ArtifactOperationError &&
error.readerLimit?.can_increase === true &&
!error.cleanupIncomplete) ||
+25
View File
@@ -511,6 +511,31 @@ it("finds a banner after overlapping unterminated repeats in one linear scan", a
});
});
it("reports the link-time banner after the kbuild placeholder banner", async () => {
// Linux 6.1+ keeps init/version.o's weak banner, built before the build
// number and timestamp exist, ahead of the banner the kernel prints.
const head = "Linux version 6.18.0 (u@h) (gcc 13.2.0) #";
const other = "Linux version 1.0.0 (x@y) (cc) #2";
const linked = `${head}1 SMP PREEMPT Tue Oct 6 19:13:07 UTC 2026`;
// No build number, then a set KBUILD_BUILD_VERSION with no timestamp.
for (const placeholder of [`${head} SMP PREEMPT `, `${head}1 SMP PREEMPT `]) {
const bytes = Buffer.from(
`${placeholder}\n\0${other}\n\0${placeholder}\n\0${linked}\n`,
);
expect((await parse(bytes)).banner).toMatchObject({
text: linked,
version: "6.18.0",
builder: "u@h",
compiler: "gcc 13.2.0",
location: { offset: bytes.length - linked.length - 1 },
});
// Without a matching link-time banner the placeholder is still reported.
expect(
(await parse(Buffer.from(`${placeholder}\n\0${other}\n`))).banner,
).toMatchObject({ text: placeholder, location: { offset: 0 } });
}
});
it("honors cancellation during the marker, inflate, and banner scans", async () => {
const input = inspectKernelBuildConfigInputSchema.parse({
path: artifactPath,
+72 -29
View File
@@ -556,11 +556,58 @@ const absentBanner = (status: "absent" | "unknown"): Report["banner"] => ({
const isPrintable = (value: number): boolean => value >= 0x20 && value <= 0x7e;
/**
* Text up to and including `#` when the banner's `UTS_VERSION` lacks a build
* number or a timestamp, otherwise null. Since Linux 6.1, kbuild compiles
* `init/version.o` with a temporary `UTS_VERSION` that leaves both empty
* unless `KBUILD_BUILD_VERSION` or `KBUILD_BUILD_TIMESTAMP` is set, so that
* object's weak `linux_banner` has a space right after `#` or at its end. The
* banner the kernel prints comes from `init/version-timestamp.o` in the final
* link: the same text up to `#`, then the build number and timestamp.
*/
const kbuildPlaceholderPrefix = (text: string): string | null => {
const marked = text.lastIndexOf(") #");
if (marked < 0) return null;
return text.startsWith(" ", marked + 3) || text.endsWith(" ")
? text.slice(0, marked + 3)
: null;
};
/**
* First `Linux version <digit>...` banner in the printable run that ends at
* `end`. Only hits within BANNER_TEXT_BYTES of the terminator fit the window.
*/
const bannerInRun = (
bytes: Buffer,
at: number,
end: number,
): {
readonly hit: number;
readonly text: string;
readonly parsed: NonNullable<ReturnType<typeof parseBannerText>>;
} | null => {
const run = bytes.subarray(0, end);
for (
let hit = run.indexOf(BANNER_PREFIX, Math.max(at, end - BANNER_TEXT_BYTES));
hit >= 0;
hit = run.indexOf(BANNER_PREFIX, hit + 1)
) {
const first = bytes[hit + BANNER_PREFIX.length] ?? 0;
if (first < 0x30 || first > 0x39) continue;
const text = bytes.toString("latin1", hit, end);
const parsed = parseBannerText(text);
if (parsed !== null) return { hit, text, parsed };
}
return null;
};
/**
* First printable, NUL- or newline-terminated `Linux version <digit>...`
* string of at most BANNER_TEXT_BYTES. Every hit inside one printable run
* shares that run's terminator, so each run is scanned once and the search
* resumes after it: the scan is linear in the searched bytes.
* string of at most BANNER_TEXT_BYTES. A kbuild placeholder banner is
* replaced by a later banner that repeats its text up to `#` and is not a
* placeholder itself; without one, the placeholder is reported. Every hit inside one
* printable run shares that run's terminator, so each run is scanned once and
* the search resumes after it: the scan is linear in the searched bytes.
*/
const findBanner = async (
bytes: Buffer,
@@ -569,10 +616,14 @@ const findBanner = async (
): Promise<Report["banner"]> => {
let from = 0;
let turnAt = TURN_STRIDE;
let placeholder: {
readonly banner: Report["banner"];
readonly prefix: string;
} | null = null;
for (;;) {
assertArtifactReadActive(signal);
const at = await findNext(bytes, BANNER_PREFIX, from, signal);
if (at < 0) return absentBanner("absent");
if (at < 0) return placeholder?.banner ?? absentBanner("absent");
let end = at + BANNER_PREFIX.length;
while (end < bytes.length && isPrintable(bytes[end] ?? 0)) {
end += 1;
@@ -585,31 +636,23 @@ const findBanner = async (
from = end + 1;
if (end >= bytes.length || (bytes[end] !== 0 && bytes[end] !== 0x0a))
continue;
// Only hits within BANNER_TEXT_BYTES of the terminator fit the window.
const run = bytes.subarray(0, end);
for (
let hit = run.indexOf(
BANNER_PREFIX,
Math.max(at, end - BANNER_TEXT_BYTES),
);
hit >= 0;
hit = run.indexOf(BANNER_PREFIX, hit + 1)
) {
const first = bytes[hit + BANNER_PREFIX.length] ?? 0;
if (first < 0x30 || first > 0x39) continue;
const text = bytes.toString("latin1", hit, end);
const parsed = parseBannerText(text);
if (parsed === null) continue;
return {
status: "present",
text,
version: parsed.version,
builder: parsed.builder,
compiler: parsed.compiler,
location: located(space, hit, end - hit),
trust: UNTRUSTED,
};
}
const found = bannerInRun(bytes, at, end);
if (found === null) continue;
const banner: Report["banner"] = {
status: "present",
text: found.text,
version: found.parsed.version,
builder: found.parsed.builder,
compiler: found.parsed.compiler,
location: located(space, found.hit, end - found.hit),
trust: UNTRUSTED,
};
const prefix = kbuildPlaceholderPrefix(found.text);
if (placeholder === null) {
if (prefix === null) return banner;
placeholder = { banner, prefix };
} else if (prefix === null && found.text.startsWith(placeholder.prefix))
return banner;
}
};
@@ -644,6 +644,96 @@ it("compares an icon with a full target path, ignoring case and separators", asy
expect(iconDifference(different)).toBe(true);
});
it("compares an icon with a target only in a shared or ASCII encoding", async () => {
// Windows stores the LinkInfo path only as ANSI when the system code page can
// represent it. 0xB9 0xA4 0xBE 0xDF is "工具" in code page 936; Latin1 differs.
const gbk = "D:\\\u00b9\u00a4\u00be\u00df\\app.exe";
const iconFor = async (
options: Parameters<typeof windowsShortcutBytes>[0],
): Promise<boolean | null | undefined> =>
iconDifference(await parse(windowsShortcutBytes(options)));
const ansiLinkInfo = (localBasePath: string) => ({
localBasePath,
unicode: false,
});
expect(
await iconFor({
linkInfo: ansiLinkInfo(gbk),
iconLocation: "D:\\工具\\app.exe",
}),
).toBeNull();
// A different comparable target does not outweigh the one that cannot be read.
expect(
await iconFor({
linkInfo: ansiLinkInfo(gbk),
extraData: [{ kind: "environment", unicode: "C:\\Other\\x.exe" }],
iconLocation: "D:\\工具\\app.exe",
}),
).toBeNull();
// An ANSI icon cannot be compared with a Unicode target either.
expect(
await iconFor({
unicode: false,
linkInfo: { localBasePath: "D:\\工具\\app.exe", unicode: true },
iconLocation: gbk,
}),
).toBeNull();
// Both ANSI: the same code page, so the bytes compare.
for (const [iconLocation, differs] of [
[gbk, false],
["D:\\\u00b9\u00a4\u00be\u00df\\other.exe", true],
] as const)
expect(
await iconFor({
unicode: false,
linkInfo: { localBasePath: gbk },
iconLocation,
}),
).toBe(differs);
// ASCII is the same text in every ANSI code page, so it still compares.
for (const [iconLocation, differs] of [
["C:\\Tools\\app.exe", false],
["C:\\Tools\\other.exe", true],
] as const)
expect(
await iconFor({
linkInfo: ansiLinkInfo("C:\\Tools\\app.exe"),
iconLocation,
}),
).toBe(differs);
// A joined path keeps each part's encoding: an ASCII ANSI share with a
// Unicode suffix, or a Unicode share with an ASCII ANSI suffix, compares.
for (const [linkInfo, iconLocation, differs] of [
[
{ netName: "\\\\server\\share", commonPathSuffix: "工具\\app.exe" },
"\\\\server\\share\\工具\\app.exe",
false,
],
[
{ netName: "\\\\server\\share", commonPathSuffix: "工具\\app.exe" },
"\\\\server\\share\\工具\\other.exe",
true,
],
] as const)
expect(
await iconFor({
linkInfo: { ...linkInfo, unicode: true, networkUnicode: false },
iconLocation,
}),
).toBe(differs);
expect(
await iconFor({
linkInfo: {
netName: "\\\\server\\共享",
commonPathSuffix: "app.exe",
unicode: false,
networkUnicode: true,
},
iconLocation: "\\\\server\\共享\\app.exe",
}),
).toBe(false);
});
it("compares an icon with a target after collapsing dot segments as text", async () => {
const iconFor = async (localBasePath: string, iconLocation: string) =>
iconDifference(
+93 -45
View File
@@ -295,66 +295,117 @@ const isFullPath = (path: string): boolean =>
const hasUnexpandedVariable = (path: string): boolean =>
/%[^%\\/]+%/u.test(path);
/**
* A recorded path and where its non-ASCII text came from. Every ANSI code page
* agrees with ASCII, so only non-ASCII characters depend on the encoding. A
* non-ASCII ANSI byte is a character from the unobserved system code page,
* which its Latin1 projection does not name.
*/
type RecordedPath = {
readonly path: string;
/** Holds non-ASCII text projected from ANSI bytes. */
readonly ansi: boolean;
/** Holds non-ASCII UTF-16 text. */
readonly unicode: boolean;
};
const isAscii = (text: string): boolean =>
[...text].every((char) => char <= "\u007f");
const recorded = (path: string, ansi: boolean): RecordedPath => {
const nonAscii = !isAscii(path);
return { path, ansi: ansi && nonAscii, unicode: !ansi && nonAscii };
};
/** Two paths compare as text only when their non-ASCII parts share one encoding. */
const comparableText = (left: RecordedPath, right: RecordedPath): boolean =>
!((left.ansi || right.ansi) && (left.unicode || right.unicode));
const iconDiffersFromTarget = (
icon: string | undefined,
targets: readonly string[],
icon: WindowsShortcut["string_data"]["icon_location"],
targets: readonly RecordedPath[],
): boolean | null => {
if (icon === undefined || !isFullPath(icon) || hasUnexpandedVariable(icon))
return null;
const location = normalizePath(icon);
const fixedTargets = targets.filter(
(target) => isFullPath(target) && !hasUnexpandedVariable(target),
if (icon === null) return null;
const text = icon.value;
if (!isFullPath(text) || hasUnexpandedVariable(text)) return null;
const iconPath = recorded(text, icon.encoding === "ansi");
const location = normalizePath(text);
const fullTargets = targets.filter((target) => isFullPath(target.path));
const fixedTargets = fullTargets.filter(
(target) =>
!hasUnexpandedVariable(target.path) && comparableText(target, iconPath),
);
if (fixedTargets.some((target) => normalizePath(target) === location))
if (fixedTargets.some((target) => normalizePath(target.path) === location))
return false;
// A variable-bearing target could name the icon's file, so no difference is claimed.
if (targets.some((t) => isFullPath(t) && hasUnexpandedVariable(t)))
return null;
// A variable-bearing target, or one whose ANSI bytes the icon text cannot be
// compared with, could name the icon's file, so no difference is claimed.
if (fullTargets.length > fixedTargets.length) return null;
return fixedTargets.length === 0 ? null : true;
};
/** LinkInfo local and network base paths, each joined with CommonPathSuffix. */
const linkInfoTargets = (
info: NonNullable<WindowsShortcut["link_info"]>,
): RecordedPath[] => {
const suffixText = info.common_path_suffix_unicode ?? info.common_path_suffix;
const suffix =
suffixText === null || suffixText.length === 0
? null
: recorded(suffixText, info.common_path_suffix_unicode === null);
// Each part keeps its own encoding, so an ASCII ANSI part never hides a
// comparable Unicode part.
const withSuffix = (base: RecordedPath): RecordedPath =>
suffix === null
? base
: {
path: `${base.path.replace(/\\$/u, "")}\\${suffix.path.replace(/^\\/u, "")}`,
ansi: base.ansi || suffix.ansi,
unicode: base.unicode || suffix.unicode,
};
const values: RecordedPath[] = [];
const local = info.local_base_path_unicode ?? info.local_base_path;
if (local !== null && (suffix !== null || local.length > 0))
values.push(
withSuffix(recorded(local, info.local_base_path_unicode === null)),
);
const net = info.common_network_relative_link;
if (net !== null) {
const share = net.net_name_unicode ?? net.net_name;
if (share !== null)
values.push(withSuffix(recorded(share, net.net_name_unicode === null)));
}
return values;
};
const targetPaths = (report: {
readonly link_info: WindowsShortcut["link_info"];
readonly string_data: WindowsShortcut["string_data"];
readonly id_list: WindowsShortcut["id_list"];
readonly extra_data: WindowsShortcut["extra_data"];
}): string[] => {
const values: string[] = [];
const info = report.link_info;
if (info !== null) {
const local = info.local_base_path_unicode ?? info.local_base_path;
const suffix = info.common_path_suffix_unicode ?? info.common_path_suffix;
if (local !== null && suffix !== null && suffix.length > 0)
values.push(
`${local.replace(/\\$/u, "")}\\${suffix.replace(/^\\/u, "")}`,
);
else if (local !== null && local.length > 0) values.push(local);
const net = info.common_network_relative_link;
if (net !== null) {
const share = net.net_name_unicode ?? net.net_name;
if (share !== null && suffix !== null && suffix.length > 0)
values.push(
`${share.replace(/\\$/u, "")}\\${suffix.replace(/^\\/u, "")}`,
);
else if (share !== null) values.push(share);
}
}
if (report.string_data.relative_path !== null)
values.push(report.string_data.relative_path.value);
}): RecordedPath[] => {
const values: RecordedPath[] =
report.link_info === null ? [] : linkInfoTargets(report.link_info);
const relative = report.string_data.relative_path;
if (relative !== null)
values.push(recorded(relative.value, relative.encoding === "ansi"));
if (report.id_list !== null) {
const lastFile = [...report.id_list.items]
.reverse()
.find((item) => item.kind === "file_entry");
if (lastFile?.kind === "file_entry") values.push(lastFile.short_name);
// Type bit 0x04 marks a UTF-16LE name; otherwise the name is ANSI.
if (lastFile?.kind === "file_entry")
values.push(
recorded(lastFile.short_name, (lastFile.type_byte & 0x04) === 0),
);
}
// A stored literal target, never expanded: `%VAR%` text stays as written.
for (const block of report.extra_data.blocks) {
if (block.decoded.kind !== "environment_variable") continue;
const stored =
block.decoded.target_unicode.length > 0
? block.decoded.target_unicode
: block.decoded.target_ansi;
if (stored.length > 0) values.push(stored);
const unicode = block.decoded.target_unicode.length > 0;
const stored = unicode
? block.decoded.target_unicode
: block.decoded.target_ansi;
if (stored.length > 0) values.push(recorded(stored, !unicode));
}
return values;
};
@@ -389,7 +440,7 @@ const observationsOf = (
{
kind: "target_extension",
evidence_kind: "derived",
extension: extensionOf(targets[0] ?? null),
extension: extensionOf(targets[0]?.path ?? null),
},
{
kind: "show_command_minimized",
@@ -399,10 +450,7 @@ const observationsOf = (
{
kind: "icon_location_differs_from_target",
evidence_kind: "derived",
value: iconDiffersFromTarget(
report.string_data.icon_location?.value,
targets,
),
value: iconDiffersFromTarget(report.string_data.icon_location, targets),
},
];
};
@@ -76,6 +76,33 @@ it("preserves the compiler output and live cleanup owner at capture normalizatio
expect(error.capturedOutput?.exit_code).toBe(7);
});
it("marks a timed-out Darwin compiler inspection as retryable", () => {
const compiler = new DarwinProcessOwnershipInspectionError(
"compiler timed out",
{
compilerOutcome: {
outcome: "timed_out",
code: "ETIMEDOUT",
signal: null,
stderr: "compiler deadline exceeded",
stderrTruncated: false,
outputTruncated: false,
killed: true,
timeoutMs: 5_000,
},
},
);
const normalized = normalizeCaptureFailure(compiler, undefined);
expect(normalized).toMatchObject({
retryable: true,
capturedOutput: undefined,
});
if (!(normalized instanceof ProcessCaptureError))
throw new Error("Expected normalized capture failure");
expect(projectAnalysisError(normalized)).toMatchObject({ retryable: true });
});
it("retains compiler facts and cleanup dependency through capture finalization", () => {
const { cleanup, error } = fixture();
let finalized: unknown;
@@ -15,6 +15,8 @@ interface ProcessCaptureErrorOptions extends AnalysisErrorOptions {
readonly executionFailure?: string;
readonly partialObservation?: PartialProcessCaptureObservation;
readonly cleanupReport?: ProcessCaptureCleanupReport;
/** Whether the producer identified a transient failure that can be retried. */
readonly retryable?: boolean;
}
/** Typed application failure produced by controlled process capture. */
@@ -29,6 +31,11 @@ export class ProcessCaptureError extends AnalysisError {
| PartialProcessCaptureObservation
| undefined;
override readonly cleanupReport: ProcessCaptureCleanupReport | undefined;
readonly #retryable: boolean;
get retryable(): boolean {
return this.#retryable && !this.cleanupIncomplete;
}
constructor(message: string, options?: ProcessCaptureErrorOptions) {
super(message, options);
@@ -41,6 +48,7 @@ export class ProcessCaptureError extends AnalysisError {
this.executionFailure = options?.executionFailure ?? message;
this.partialObservation = options?.partialObservation;
this.cleanupReport = options?.cleanupReport;
this.#retryable = options?.retryable === true;
}
/** Add final capture observations while preserving primary facts and cleanup ownership. */
@@ -82,6 +90,7 @@ export class ProcessCaptureError extends AnalysisError {
...(executionFailure === undefined ? {} : { executionFailure }),
...(partialObservation === undefined ? {} : { partialObservation }),
...(cleanupReport === undefined ? {} : { cleanupReport }),
...(this.#retryable ? { retryable: true } : {}),
}).bindCleanupDependencies([
this,
...(secondary === undefined ? [] : [secondary]),
@@ -135,6 +144,9 @@ export const normalizeCaptureFailure = (
...(cause.capturedOutput === undefined
? {}
: { capturedOutput: cause.capturedOutput }),
...(cause.compilerOutcome?.outcome === "timed_out"
? { retryable: true }
: {}),
});
return cause.cleanupFailure === undefined
? normalized
+11 -2
View File
@@ -67,6 +67,13 @@ export type FixtureLinkInfo = {
readonly deviceName?: string;
readonly networkProviderType?: number;
readonly networkFlags?: number;
/**
* Write LinkInfo's Unicode fields: header 0x24, Unicode volume label and
* network names. Defaults to the link's IsUnicode.
*/
readonly unicode?: boolean;
/** Write Unicode network names; defaults to `unicode`. */
readonly networkUnicode?: boolean;
};
export type FixtureExtraBlock =
@@ -278,7 +285,9 @@ const linkInfoBytes = (info: FixtureLinkInfo, unicode: boolean): Buffer => {
? ansiZ(info.localBasePath ?? "C:\\target.exe")
: Buffer.alloc(0);
const network =
flags & 0x2 ? networkLinkBytes(info, unicode) : Buffer.alloc(0);
flags & 0x2
? networkLinkBytes(info, info.networkUnicode ?? unicode)
: Buffer.alloc(0);
const suffixAnsi = ansiZ(info.commonPathSuffix ?? "");
const localWide =
unicode && flags & 0x1
@@ -512,7 +521,7 @@ export const windowsShortcutBytes = (
if (flags & LINK_FLAG.HasLinkTargetIDList && idList !== null)
parts.push(idListBytes(idList));
if (flags & LINK_FLAG.HasLinkInfo && linkInfo !== null)
parts.push(linkInfoBytes(linkInfo, unicode));
parts.push(linkInfoBytes(linkInfo, linkInfo.unicode ?? unicode));
const strings: Buffer[] = [];
if (flags & LINK_FLAG.HasName) strings.push(stringDataField(name, unicode));
if (flags & LINK_FLAG.HasRelativePath)