Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/research/IMAGE_SIGNATURE_BYTES_2026-10-07.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Exact image signature bytes

- Status: validated source and regression evidence
- Created / verified: 2026-10-07
- Source: origin/main at 3cb2ecfe1bfbb98252651885311d309f83749428
- Issue: [#697](https://github.com/openpi-dev/openpi/issues/697)
- Supersedes: none

## Verified facts

ASCII decoding cleared high bits in GIF and RIFF/WEBP signatures, accepting corrupted image bytes. Cursor input and Web HTTP admission regressions failed before the fix and pass after exact Buffer comparisons. Valid GIF87a, GIF89a and WEBP signatures remain accepted; PNG/JPEG behavior is unchanged.

## Verification boundary

Production input and HTTP tests; no model or installed runtime acceptance is claimed.

Full repository validation results and CI are recorded on the linked PR. Local
Windows failures are retained separately and are not reported as passing.
12 changes: 8 additions & 4 deletions extensions/ai-providers/cursor/input-images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@ function detectImageMimeType(
return "image/jpeg";
}
if (bytes.length >= 6) {
const signature = Buffer.from(bytes.subarray(0, 6)).toString("ascii");
if (signature === "GIF87a" || signature === "GIF89a") return "image/gif";
const signature = Buffer.from(bytes.subarray(0, 6));
if (
signature.equals(Buffer.from("GIF87a")) ||
signature.equals(Buffer.from("GIF89a"))
)
return "image/gif";
}
if (
bytes.length >= 12 &&
Buffer.from(bytes.subarray(0, 4)).toString("ascii") === "RIFF" &&
Buffer.from(bytes.subarray(8, 12)).toString("ascii") === "WEBP"
Buffer.from(bytes.subarray(0, 4)).equals(Buffer.from("RIFF")) &&
Buffer.from(bytes.subarray(8, 12)).equals(Buffer.from("WEBP"))
) {
return "image/webp";
}
Expand Down
32 changes: 32 additions & 0 deletions tests/extensions/ai-providers/cursor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,38 @@ test("Cursor image-path conversion is scoped to interactive Cursor input", async
);
});

test("Cursor image-path conversion requires literal GIF and WebP signature bytes", async (t) => {
const directory = await mkdtemp(join(tmpdir(), "openpi-cursor-signatures-"));
t.after(() => rm(directory, { recursive: true, force: true }));
for (const signature of ["GIF87a", "GIF89a", "RIFF\0\0\0\0WEBP"]) {
const bytes = Buffer.from(signature);
const path = join(
directory,
signature.startsWith("GIF") ? "fixture.gif" : "fixture.webp",
);
const event = {
type: "input" as const,
source: "interactive" as const,
text: JSON.stringify(path),
};
const context = { model: { provider: "cursor" } } as ExtensionContext;
await writeFile(path, bytes);
const valid = await transformCursorImageInput(event, context);
assert.equal(valid.action, "transform");
for (let index = 0; index < bytes.length; index++) {
if (signature.startsWith("RIFF") && index >= 4 && index < 8) continue;
const corrupted = Buffer.from(bytes);
corrupted[index]! |= 0x80;
await writeFile(path, corrupted);
assert.deepEqual(
await transformCursorImageInput(event, context),
{ action: "continue" },
`signature byte ${index}`,
);
}
}
});

test("Cursor multi-turn request omits prior thinking outside OMP's Kimi-only replay", async () => {
const built = await buildCursorRequest(MODEL, {
messages: [
Expand Down
42 changes: 42 additions & 0 deletions tests/web/web-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3539,6 +3539,48 @@ test("admits the advertised image byte limits through the HTTP body boundary", a
}
});

test("prompt image admission requires literal GIF and WebP signature bytes", async () => {
const cwd = await mkdtemp(join(tmpdir(), "openpi-web-image-signatures-"));
let dispatches = 0;
const runtime = testRuntime(cwd, async () => {
dispatches++;
return { pendingFollowUps: 0 };
});
const { host, launched, headers } = await startTestHost(runtime);
const post = (mimeType: string, bytes: Buffer) =>
fetch(`${launched.origin}/api/prompt`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
sessionId: runtime.sessionManager.getSessionId(),
sessionPath: mutationSessionPath(runtime.sessionManager),
content: "Signature fixture; no model call.",
images: [{ mimeType, data: bytes.toString("base64") }],
}),
});
try {
for (const signature of ["GIF87a", "GIF89a", "RIFF\0\0\0\0WEBP"]) {
const mime = signature.startsWith("GIF") ? "image/gif" : "image/webp";
const bytes = Buffer.from(signature);
for (let index = 0; index < bytes.length; index++) {
if (signature.startsWith("RIFF") && index >= 4 && index < 8) continue;
const corrupted = Buffer.from(bytes);
corrupted[index]! |= 0x80;
const response = await post(mime, corrupted);
assert.equal(response.status, 400, `signature byte ${index}`);
await response.arrayBuffer();
}
const response = await post(mime, bytes);
assert.equal(response.status, 202);
await response.arrayBuffer();
}
assert.equal(dispatches, 3);
} finally {
await host.stop();
await rm(cwd, { recursive: true, force: true });
}
});

test("replays one prompt admission after a browser timeout", async () => {
const cwd = await mkdtemp(join(tmpdir(), "openpi-web-prompt-retry-"));
let sendCalls = 0;
Expand Down
8 changes: 4 additions & 4 deletions web/host/web-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,12 @@ function hasImageSignature(bytes: Buffer, mimeType: WebPromptImage["mimeType"])
if (mimeType === "image/jpeg")
return bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff;
if (mimeType === "image/gif") {
const signature = bytes.subarray(0, 6).toString("ascii");
return signature === "GIF87a" || signature === "GIF89a";
const signature = bytes.subarray(0, 6);
return signature.equals(Buffer.from("GIF87a")) || signature.equals(Buffer.from("GIF89a"));
}
return (
bytes.subarray(0, 4).toString("ascii") === "RIFF" &&
bytes.subarray(8, 12).toString("ascii") === "WEBP"
bytes.subarray(0, 4).equals(Buffer.from("RIFF")) &&
bytes.subarray(8, 12).equals(Buffer.from("WEBP"))
);
}

Expand Down
Loading