mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 18:52:48 +00:00
`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>
136 lines
4.6 KiB
TypeScript
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,
|
|
},
|
|
};
|
|
};
|