Skip to content

Commit 10a8857

Browse files
committed
fix: harden PostgreSQL source cache
1 parent 676d5a5 commit 10a8857

3 files changed

Lines changed: 394 additions & 25 deletions

File tree

scripts/pg_compat/common.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import re
55
import tempfile
66
from pathlib import Path
7+
from urllib.parse import urlsplit
78

89

910
REQUIRED_ROLES = ("previous", "target")
@@ -14,6 +15,41 @@
1415
"pg_version_num",
1516
"postgres_sha256",
1617
)
18+
COMMIT_PATTERN = re.compile(r"^[0-9a-f]{40}$")
19+
PG_VERSION_PATTERN = re.compile(r"^[0-9]+(?:\.[0-9]+)+$")
20+
SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$")
21+
22+
23+
def _require_nonempty_string(value, name):
24+
if not isinstance(value, str) or not value.strip():
25+
raise ValueError(f"{name} must be a non-empty string")
26+
27+
28+
def _validate_version_pin(version, role):
29+
prefix = f"versions.{role}"
30+
for field in REQUIRED_VERSION_FIELDS:
31+
if field not in version:
32+
raise ValueError(f"missing required pin: {prefix}.{field}")
33+
34+
_require_nonempty_string(version["branch"], f"{prefix}.branch")
35+
_require_nonempty_string(version["pg_version"], f"{prefix}.pg_version")
36+
37+
if not isinstance(version["commit"], str) or not COMMIT_PATTERN.fullmatch(
38+
version["commit"]
39+
):
40+
raise ValueError(f"{prefix}.commit must be 40 lowercase hexadecimal characters")
41+
if not PG_VERSION_PATTERN.fullmatch(version["pg_version"]):
42+
raise ValueError(f"{prefix}.pg_version must use numeric dotted components")
43+
if isinstance(version["pg_version_num"], bool) or not isinstance(
44+
version["pg_version_num"], int
45+
):
46+
raise ValueError(f"{prefix}.pg_version_num must be an integer")
47+
if not isinstance(
48+
version["postgres_sha256"], str
49+
) or not SHA256_PATTERN.fullmatch(version["postgres_sha256"]):
50+
raise ValueError(
51+
f"{prefix}.postgres_sha256 must be 64 lowercase hexadecimal characters"
52+
)
1753

1854

1955
def load_pins(path):
@@ -23,6 +59,10 @@ def load_pins(path):
2359

2460
if not isinstance(pins, dict) or "libpg_query_url" not in pins:
2561
raise ValueError("missing required pin: libpg_query_url")
62+
_require_nonempty_string(pins["libpg_query_url"], "libpg_query_url")
63+
parsed_url = urlsplit(pins["libpg_query_url"])
64+
if not parsed_url.scheme or not parsed_url.netloc:
65+
raise ValueError("libpg_query_url must be a non-empty absolute URL")
2666

2767
versions = pins.get("versions")
2868
if not isinstance(versions, dict):
@@ -32,9 +72,7 @@ def load_pins(path):
3272
version = versions.get(role)
3373
if not isinstance(version, dict):
3474
raise ValueError(f"missing required pin: versions.{role}")
35-
for field in REQUIRED_VERSION_FIELDS:
36-
if field not in version:
37-
raise ValueError(f"missing required pin: versions.{role}.{field}")
75+
_validate_version_pin(version, role)
3876

3977
return pins
4078

scripts/pg_compat/fetch_libpg_query.sh

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
55
PINS="${PG_COMPAT_PINS:-${ROOT}/tests/pg_compat/upstream_pins.json}"
66
CACHE="${PG_COMPAT_CACHE:-/tmp/parsersql-pg-compat}"
77
WITH_POSTGRES_SOURCE=0
8+
LOCK_DIR="${CACHE}/.pg_compat.lock"
9+
LOCK_OWNER="$$-${RANDOM}-${RANDOM}"
10+
LOCK_HELD=0
811

912
usage() {
1013
echo "Usage: $0 [--with-postgres-source]" >&2
@@ -24,6 +27,32 @@ fi
2427

2528
cd "$ROOT"
2629

30+
release_lock() {
31+
if [[ "$LOCK_HELD" -ne 1 || ! -d "$LOCK_DIR" ]]; then
32+
return
33+
fi
34+
if [[ -f "$LOCK_DIR/owner" ]] && [[ "$(cat "$LOCK_DIR/owner")" == "$LOCK_OWNER" ]]; then
35+
rm -f "$LOCK_DIR/owner"
36+
rmdir "$LOCK_DIR" 2>/dev/null || true
37+
fi
38+
LOCK_HELD=0
39+
}
40+
41+
acquire_lock() {
42+
mkdir -p "$CACHE"
43+
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
44+
echo "PostgreSQL compatibility cache is locked: ${CACHE}" >&2
45+
exit 1
46+
fi
47+
printf '%s\n' "$LOCK_OWNER" > "$LOCK_DIR/owner"
48+
LOCK_HELD=1
49+
}
50+
51+
trap release_lock EXIT
52+
trap 'exit 129' HUP
53+
trap 'exit 130' INT
54+
trap 'exit 143' TERM
55+
2756
LIBPG_QUERY_URL="$(
2857
python3 - "$PINS" <<'PY'
2958
import sys
@@ -34,6 +63,8 @@ print(load_pins(sys.argv[1])["libpg_query_url"])
3463
PY
3564
)"
3665

66+
acquire_lock
67+
3768
pin_values() {
3869
local role="$1"
3970
python3 - "$PINS" "$role" <<'PY'
@@ -142,21 +173,29 @@ fetch_postgres_source() {
142173
local actual_sha256
143174

144175
mkdir -p "$postgres_root"
176+
if [[ -f "$archive" ]]; then
177+
actual_sha256="$(sha256_file "$archive")"
178+
if [[ "$actual_sha256" != "$expected_sha256" ]]; then
179+
echo "Removing invalid cached archive ${archive}: expected ${expected_sha256}, got ${actual_sha256}" >&2
180+
rm -f "$archive"
181+
fi
182+
fi
183+
145184
if [[ ! -f "$archive" ]]; then
146185
download_tmp="$(mktemp "${postgres_root}/.postgresql-${pg_version}.download.XXXXXX")"
147186
if ! curl --fail --location --retry 3 --output "$download_tmp" "$archive_url"; then
148187
rm -f "$download_tmp"
149188
return 1
150189
fi
190+
actual_sha256="$(sha256_file "$download_tmp")"
191+
if [[ "$actual_sha256" != "$expected_sha256" ]]; then
192+
echo "SHA-256 mismatch for downloaded ${archive}: expected ${expected_sha256}, got ${actual_sha256}" >&2
193+
rm -f "$download_tmp"
194+
return 1
195+
fi
151196
mv "$download_tmp" "$archive"
152197
fi
153198

154-
actual_sha256="$(sha256_file "$archive")"
155-
if [[ "$actual_sha256" != "$expected_sha256" ]]; then
156-
echo "SHA-256 mismatch for ${archive}: expected ${expected_sha256}, got ${actual_sha256}" >&2
157-
exit 1
158-
fi
159-
160199
if [[ ! -d "$source_dir" ]]; then
161200
extract_tmp="$(mktemp -d "${postgres_root}/.postgresql-${pg_version}.extract.XXXXXX")"
162201
if ! tar -xjf "$archive" -C "$extract_tmp"; then

0 commit comments

Comments
 (0)