Files
reverse-engineering-anything/src/desktop/DebianPackageFile.ts
T
2SinceJulyandgensokyo-dev d68383f98d fix(desktop): keep denied path probes unknown instead of absent
`isHostFile` folded every stat failure into "not a file", so a library
candidate under a denied directory became a missing library, and an
unprobeable apt release file or package file read as absent.

`probeHostFile` reports ENOENT/ENOTDIR as absent and every other
failure as unknown. Apt release discovery, the package-file check and
the library walk keep those unknowns: a candidate under a directory the
linker cannot search (EACCES) is kept as a last resort and reported
unreadable rather than missing. Any other failure keeps its search
position, since glibc's open_path continues only past ENOENT and EACCES.

Fixes #9703

Squashed from #9722 and rebased onto main. Adapted on rebase: only
EACCES counts as searched past (checked on glibc 2.36, where a looping
symlink candidate ends the search), the probe reuses the domain
LibraryFileProbe type, and a stray comment is dropped.

Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-11-14 12:13:50 +01:00

136 lines
4.6 KiB
TypeScript

import { stat } from "node:fs/promises";
import { basename } from "node:path";
import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
import { ArtifactFileCleanupFailure } from "../artifacts/ArtifactFileOperation.js";
import {
PACKAGE_FILE_FIELDS,
packageFileMismatch,
parseDpkgDebFields,
} from "../domain/desktop/debianPackageFile.js";
import type { DesktopPackage } from "../domain/desktop/desktopCapture.js";
import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js";
import {
sameRegularFileState,
type RegularFileState,
} from "../filesystem/RegularFile.js";
import {
StableFileHashFailure,
observeStableFile,
stableFileHashCause,
} from "../filesystem/StableFileHash.js";
import {
hostReadFailure,
hostFileKind,
isRefusedRead,
} from "./HostFileRead.js";
import type { QueryRunner } from "./HostQuery.js";
type PackageFileFacts = Pick<
DesktopPackage,
"package_file" | "package_file_reason"
>;
/**
* The file's SHA-256, size and identity, streamed through one admitted
* descriptor that must hold still while it is read, or why it could not be
* read. A close that fails is thrown with the handle's cleanup owner, which
* fails the capture, even when the capture was also cancelled.
*/
const hashPackageFile = async (
path: string,
signal: AbortSignal | undefined,
): Promise<
| {
readonly sha256: string;
readonly bytes: number;
readonly state: RegularFileState;
}
| string
> => {
let lease: AnalysisCleanupLease | undefined;
try {
return await observeStableFile(path, {
...(signal === undefined ? {} : { signal }),
retainFailedClose: (owner) => {
lease = new AnalysisCleanupLease(() => owner.close(), [path]);
},
});
} catch (cause: unknown) {
if (cause instanceof RegularFileCleanupFailure && lease !== undefined)
throw new ArtifactFileCleanupFailure(cause, lease);
const failure = hostReadFailure(cause);
if (failure !== cause) throw failure;
const read = stableFileHashCause(cause);
if (!(cause instanceof StableFileHashFailure) || !isRefusedRead(read))
throw cause;
return `${path} could not be read: ${read instanceof Error ? read.message : String(read)}`;
}
};
/**
* The `.deb` file the caller says installed the application: its control
* fields from `dpkg-deb --field`, then its SHA-256 and size, recorded only
* when it holds the installed package's name, version and architecture;
* otherwise why not. The file itself is never copied.
*/
export const inspectPackageFile = async (
path: string,
installed: DesktopPackage,
runQuery: QueryRunner,
signal: AbortSignal | undefined,
): Promise<PackageFileFacts> => {
const refused = (reason: string): PackageFileFacts => ({
package_file_reason: reason,
});
if (
installed.kind !== "deb" ||
installed.version === null ||
installed.architecture === null
)
return refused(
`dpkg reports no installed version of the application (${installed.kind}${installed.name === null ? "" : ` ${installed.name}`}), so ${path} was not matched against it`,
);
const regularFile = await hostFileKind(path);
if (regularFile === null)
return refused(
`${path} could not be checked, so it was not read as a file`,
);
if (!regularFile) return refused(`${path} is not a regular file`);
// dpkg-deb and the hash read the file apart; its identity must hold across both.
const before = await stat(path).catch(() => undefined);
if (before === undefined) return refused(`${path} could not be read`);
const outcome = await runQuery(
"dpkg-deb",
["--field", path, ...PACKAGE_FILE_FIELDS],
signal,
);
if (outcome === "missing")
return refused(
`dpkg-deb is not installed, so the control fields of ${path} were not read`,
);
if (outcome === "failed" || outcome.exitCode !== 0)
return refused(`dpkg-deb could not read ${path} as a Debian package`);
const fields = parseDpkgDebFields(outcome.stdout);
if (fields === undefined)
return refused(
`dpkg-deb did not report the Package, Version and Architecture of ${path}`,
);
const mismatch = packageFileMismatch(fields, installed);
if (mismatch !== undefined) return refused(`${path} ${mismatch}`);
const hashed = await hashPackageFile(path, signal);
if (typeof hashed === "string") return refused(hashed);
if (!sameRegularFileState(hashed.state, before))
return refused(
`${path} changed while it was read, so its control fields and its hash could describe different files`,
);
return {
package_file: {
name: basename(path),
sha256: hashed.sha256,
size: hashed.bytes,
...fields,
},
};
};