mirror of
https://github.com/flectar/mail.git
synced 2026-11-11 17:57:54 +00:00
109 lines
5.6 KiB
Python
109 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Exercise the packaged PDF worker without initializing UI or account databases.
|
|
|
|
Usage: python3 scripts/test-pdf-preview.py target/debug/flectar-mail [output.png]
|
|
Creates an original two-page vector/text fixture and checks page selection,
|
|
colors, dimensions, malformed inputs and protocol limits. Requires staged PDFium.
|
|
On Linux it repeats the checks as on a kernel without Landlock, where the
|
|
worker's sandbox blocks all file access instead.
|
|
"""
|
|
import ctypes
|
|
import pathlib
|
|
import platform
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import zlib
|
|
|
|
|
|
def fixture():
|
|
streams = [b"0.1 0.4 0.8 rg 20 20 160 100 re f BT /F1 16 Tf 20 150 Td (Flectar PDF preview) Tj ET", b"0.8 0.2 0.1 rg 20 20 160 100 re f BT /F1 16 Tf 20 150 Td (Second page) Tj ET"]
|
|
objects = [b"<< /Type /Catalog /Pages 2 0 R >>", b"<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>"]
|
|
for content in (6, 7):
|
|
objects.append(f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /Font << /F1 5 0 R >> >> /Contents {content} 0 R >>".encode())
|
|
objects.append(b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>")
|
|
objects += [b"<< /Length " + str(len(s)).encode() + b" >>\nstream\n" + s + b"\nendstream" for s in streams]
|
|
data = b"%PDF-1.4\n"; offsets = [0]
|
|
for i, obj in enumerate(objects, 1):
|
|
offsets.append(len(data)); data += f"{i} 0 obj\n".encode() + obj + b"\nendobj\n"
|
|
xref = len(data)
|
|
data += f"xref\n0 {len(offsets)}\n0000000000 65535 f \n".encode()
|
|
data += b"".join(f"{offset:010} 00000 n \n".encode() for offset in offsets[1:])
|
|
return data + f"trailer\n<< /Size {len(offsets)} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode()
|
|
|
|
|
|
def png(path, width, height, pixels):
|
|
def chunk(kind, value):
|
|
return struct.pack(">I", len(value)) + kind + value + struct.pack(">I", zlib.crc32(kind + value))
|
|
raw = b"".join(b"\0" + pixels[y * width * 4:(y + 1) * width * 4] for y in range(height))
|
|
pathlib.Path(path).write_bytes(b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b""))
|
|
|
|
|
|
# seccomp_data.arch for the architectures whose landlock_create_ruleset is 444.
|
|
AUDIT_ARCHES = {"x86_64": 0xC000003E, "aarch64": 0xC00000B7}
|
|
|
|
|
|
def without_landlock():
|
|
"""Make landlock_create_ruleset fail with ENOSYS in the worker, as it does
|
|
on kernels built without Landlock, such as Raspberry Pi OS's."""
|
|
def instruction(code, k, jump_true=0, jump_false=0):
|
|
return struct.pack("=HBBI", code, jump_true, jump_false, k)
|
|
load, jump_equal, ret = 0x20, 0x15, 0x06
|
|
program = b"".join([
|
|
instruction(load, 4), # seccomp_data.arch
|
|
instruction(jump_equal, AUDIT_ARCHES[platform.machine()], 0, 3),
|
|
instruction(load, 0), # seccomp_data.nr
|
|
instruction(jump_equal, 444, 0, 1),
|
|
instruction(ret, 0x00050000 | 38), # SECCOMP_RET_ERRNO | ENOSYS
|
|
instruction(ret, 0x7FFF0000), # SECCOMP_RET_ALLOW
|
|
])
|
|
|
|
class Program(ctypes.Structure):
|
|
_fields_ = [("length", ctypes.c_ushort), ("filter", ctypes.c_void_p)]
|
|
|
|
def install():
|
|
libc = ctypes.CDLL(None, use_errno=True)
|
|
buffer = ctypes.create_string_buffer(program)
|
|
filter_program = Program(len(program) // 8, ctypes.cast(buffer, ctypes.c_void_p))
|
|
# PR_SET_NO_NEW_PRIVS, then PR_SET_SECCOMP with SECCOMP_MODE_FILTER.
|
|
if libc.prctl(38, 1, 0, 0, 0) or libc.prctl(22, 2, ctypes.byref(filter_program), 0, 0):
|
|
raise OSError(ctypes.get_errno(), "could not hide Landlock from the PDF worker")
|
|
return install
|
|
|
|
|
|
def check(binary, output=None):
|
|
check_mode(binary, output, None)
|
|
if sys.platform == "linux":
|
|
check_mode(binary, None, without_landlock())
|
|
|
|
|
|
def check_mode(binary, output, preexec):
|
|
mode = " without Landlock" if preexec else ""
|
|
data = fixture()
|
|
for index, zoom in [(0, 100), (1, 100), (0, 200)]:
|
|
start = time.monotonic()
|
|
result = subprocess.run([binary, "--render-pdf-page"], input=struct.pack("<III", index, zoom, len(data)) + data, capture_output=True, timeout=22, preexec_fn=preexec)
|
|
assert result.returncode == 0, (result.returncode, result.stderr.decode(errors="replace"))
|
|
magic, count, width, height = struct.unpack("<4sIII", result.stdout[:16])
|
|
pixels = result.stdout[16:]
|
|
assert magic == b"FPD1" and count == 2
|
|
assert 0 < width <= 2880 and 0 < height <= 2880
|
|
assert len(pixels) == width * height * 4
|
|
center = pixels[(height // 2 * width + width // 2) * 4:][:4]
|
|
expected = (26, 102, 204, 255) if index == 0 else (204, 51, 26, 255)
|
|
assert all(abs(a - b) <= 1 for a, b in zip(center, expected)), center
|
|
print(f"Page {index + 1} at {zoom}%{mode}: {width}x{height}, {time.monotonic() - start:.3f}s")
|
|
if output and index == 0 and zoom == 100: png(output, width, height, pixels)
|
|
for payload in [struct.pack("<III", 9, 100, len(data)) + data, struct.pack("<III", 0, 100, 4) + b"oops", struct.pack("<III", 0, 100, 16 * 1024 * 1024 + 1), struct.pack("<III", 0, 999, 0), b"short"]:
|
|
result = subprocess.run([binary, "--render-pdf-page"], input=payload, capture_output=True, timeout=22, preexec_fn=preexec)
|
|
assert result.returncode != 0 and not result.stdout
|
|
print(f"PASS{mode}: pages, zoom, bitmap bounds, colors, malformed input and oversized requests")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if sys.argv[1] == "--fixture":
|
|
pathlib.Path(sys.argv[2]).write_bytes(fixture())
|
|
else:
|
|
check(str(pathlib.Path(sys.argv[1]).resolve()), sys.argv[2] if len(sys.argv) > 2 else None)
|