fix: harden apply-patch v2 remote reads
This commit is contained in:
@@ -281,6 +281,7 @@ async function executePlannedOperation(executor: ApplyPatchV2Executor, operation
|
||||
const readBlockBytes = 45_000;
|
||||
const writeB64ArgvLimit = 48_000;
|
||||
const writeB64ChunkChars = 12_000;
|
||||
const remoteReadBlockMarker = "UNIDESK_APPLY_PATCH_V2_BLOCK";
|
||||
|
||||
async function readRemoteText(executor: ApplyPatchV2Executor, target: string): Promise<string> {
|
||||
if (executor.fs) {
|
||||
@@ -333,8 +334,8 @@ async function readRemoteText(executor: ApplyPatchV2Executor, target: string): P
|
||||
let actualBytes = 0;
|
||||
for (let blockIndex = 0; actualBytes < expectedBytes; blockIndex += 1) {
|
||||
const read = await checkedRemoteV2(executor, "read-b64-block", [target, String(blockIndex), String(readBlockBytes)]);
|
||||
const encoded = read.stdout.replace(/\s+/gu, "");
|
||||
const chunk = encoded.length === 0 ? Buffer.alloc(0) : Buffer.from(encoded, "base64");
|
||||
const expectedChunkBytes = Math.min(readBlockBytes, expectedBytes - actualBytes);
|
||||
const chunk = decodeRemoteReadBlock(read.stdout, target, blockIndex, expectedChunkBytes);
|
||||
if (chunk.length === 0) {
|
||||
throw new ApplyPatchV2Error("remote apply-patch v2 read returned an empty block before EOF", {
|
||||
path: target,
|
||||
@@ -366,6 +367,50 @@ async function readRemoteText(executor: ApplyPatchV2Executor, target: string): P
|
||||
return contentBuffer.toString("utf8");
|
||||
}
|
||||
|
||||
function decodeRemoteReadBlock(stdout: string, target: string, blockIndex: number, expectedChunkBytes: number): Buffer {
|
||||
const lines = stdout.split(/\r?\n/u);
|
||||
const markerIndex = lines.findIndex((line) => line.startsWith(`${remoteReadBlockMarker} `));
|
||||
let encoded = "";
|
||||
let declaredBytes: number | null = null;
|
||||
let declaredSha256: string | null = null;
|
||||
if (markerIndex >= 0) {
|
||||
const [, bytesText, sha256] = lines[markerIndex].split(/\s+/u);
|
||||
declaredBytes = Number(bytesText);
|
||||
declaredSha256 = sha256 ?? null;
|
||||
if (!Number.isSafeInteger(declaredBytes) || declaredBytes < 0 || !/^[0-9a-f]{64}$/u.test(declaredSha256 ?? "")) {
|
||||
throw new ApplyPatchV2Error("remote apply-patch v2 read block returned invalid metadata", {
|
||||
path: target,
|
||||
blockIndex,
|
||||
marker: lines[markerIndex].slice(0, 200),
|
||||
});
|
||||
}
|
||||
encoded = (lines[markerIndex + 1] ?? "").replace(/\s+/gu, "");
|
||||
} else {
|
||||
encoded = stdout.replace(/\s+/gu, "");
|
||||
}
|
||||
const decoded = encoded.length === 0 ? Buffer.alloc(0) : Buffer.from(encoded, "base64");
|
||||
if (declaredBytes !== null) {
|
||||
if (decoded.length !== declaredBytes) {
|
||||
throw new ApplyPatchV2Error("remote apply-patch v2 read block byte count mismatch", {
|
||||
path: target,
|
||||
blockIndex,
|
||||
expectedBytes: declaredBytes,
|
||||
actualBytes: decoded.length,
|
||||
});
|
||||
}
|
||||
const actualSha256 = sha256Hex(decoded);
|
||||
if (actualSha256 !== declaredSha256) {
|
||||
throw new ApplyPatchV2Error("remote apply-patch v2 read block sha256 mismatch", {
|
||||
path: target,
|
||||
blockIndex,
|
||||
expectedSha256: declaredSha256,
|
||||
actualSha256,
|
||||
});
|
||||
}
|
||||
}
|
||||
return decoded.length > expectedChunkBytes ? decoded.subarray(0, expectedChunkBytes) : decoded;
|
||||
}
|
||||
|
||||
async function writeRemoteText(executor: ApplyPatchV2Executor, target: string, content: string): Promise<void> {
|
||||
const contentBuffer = Buffer.from(content, "utf8");
|
||||
if (executor.fs) {
|
||||
@@ -473,7 +518,14 @@ function remoteV2Script(operation: RemoteV2Operation, args: string[]): string[]
|
||||
" block_index=$2",
|
||||
" block_size=$3",
|
||||
" case \"$block_index:$block_size\" in *[!0-9:]*|:*) printf 'invalid read block args\\n' >&2; exit 2;; esac",
|
||||
" dd if=\"$target\" bs=\"$block_size\" skip=\"$block_index\" count=1 2>/dev/null | base64 | tr -d '\\n'",
|
||||
" tmp=$(mktemp)",
|
||||
" dd if=\"$target\" bs=\"$block_size\" skip=\"$block_index\" count=1 2>/dev/null > \"$tmp\"",
|
||||
" bytes=$(wc -c < \"$tmp\" | tr -d '[:space:]')",
|
||||
" digest=$(sha256_file \"$tmp\")",
|
||||
` printf '${remoteReadBlockMarker} %s %s\\n' "$bytes" "$digest"`,
|
||||
" base64 < \"$tmp\" | tr -d '\\n'",
|
||||
" printf '\\n'",
|
||||
" rm -f -- \"$tmp\"",
|
||||
" ;;",
|
||||
" write-b64-argv)",
|
||||
" target=$1",
|
||||
|
||||
Reference in New Issue
Block a user