🐛 fix(core): reject invalid LSP framing

This commit is contained in:
csh
2026-07-14 09:03:47 +08:00
parent 101b69e84f
commit 71b519793a
2 changed files with 123 additions and 18 deletions
+47
View File
@@ -18,6 +18,15 @@ def raw_frame(body: bytes) -> bytes:
return f"Content-Length: {len(body)}\r\n\r\n".encode("ascii") + body
def content_type_frame(message: dict) -> bytes:
body = json.dumps(message, separators=(",", ":")).encode("utf-8")
header = (
f"Content-Length: {len(body)}\r\n"
"Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n"
).encode("ascii")
return header + body
def read_messages(data: bytes) -> list[dict]:
messages = []
offset = 0
@@ -294,6 +303,43 @@ def assert_cancellation_and_failures(server: Path) -> None:
client.kill()
def assert_framing(server: Path) -> None:
invalid_payloads = [
b"Content-Length: 2junk\r\n\r\n{}",
b"Content-Length: 2\r\nContent-Length: 2\r\n\r\n{}",
b"Content-Length: 0\r\n\r\n",
b"Content-Length: 16777217\r\n\r\n",
b"Content-Length: 10\r\n\r\n{}",
b"Content-Length: 2\n\n{}",
b"X-Length: 2\r\n\r\n{}",
b"Content-Length: 2\r\nContent-Type: application/json\r\n\r\n{}",
]
for index, payload in enumerate(invalid_payloads):
result = run_raw(server, payload)
if result.returncode != 1:
raise RuntimeError(f"invalid framing case {index} should return code 1")
if result.stdout:
raise RuntimeError(f"invalid framing case {index} should not process its body")
valid_payload = b"".join([
content_type_frame({
"jsonrpc": "2.0",
"id": 31,
"method": "initialize",
"params": {},
}),
frame({"jsonrpc": "2.0", "id": 32, "method": "shutdown"}),
frame({"jsonrpc": "2.0", "method": "exit"}),
])
valid = run_raw(server, valid_payload)
if valid.returncode != 0:
raise RuntimeError("canonical Content-Type should be accepted")
if response_by_id(valid.stdout, 31).get("error"):
raise RuntimeError("initialize with canonical Content-Type should succeed")
if response_by_id(valid.stdout, 32).get("result", "missing") is not None:
raise RuntimeError("continuous framing should reach shutdown")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--server", type=Path, required=True)
@@ -302,6 +348,7 @@ def main() -> int:
assert_lifecycle(args.server)
assert_json_rpc_errors(args.server)
assert_cancellation_and_failures(args.server)
assert_framing(args.server)
return 0