commit 665e83ecd46ad0bd3d827a8ba8d3edc99c6f0a6f
parent 56755e162db1b89015e3320757a37fcfb8ddbfeb
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 28 May 2026 18:43:23 -0700
Tighten package verification checks
Diffstat:
5 files changed, 47 insertions(+), 1 deletion(-)
diff --git a/doc/DISTRIBUTE.md b/doc/DISTRIBUTE.md
@@ -118,6 +118,9 @@ Top-level fields:
| `blake2b` | no | dependency package id |
| `key` | no | expected signer key id |
+Artifact paths are always relative unpack paths. Absolute paths, empty path
+components, `.`, `..`, backslashes, and drive-style `:` are rejected.
+
## Merkle tree
Artifacts are split into fixed 64 KiB raw chunks. The final chunk may be
diff --git a/driver/dist/manifest.c b/driver/dist/manifest.c
@@ -113,6 +113,23 @@ static int kind_valid(const char* k) {
strcmp(k, "source") == 0;
}
+int dist_manifest_path_valid(const char* p) {
+ size_t start = 0, i;
+ if (!p[0] || p[0] == '/') return 0;
+ for (i = 0;; ++i) {
+ char c = p[i];
+ if (c == '\\' || c == ':') return 0;
+ if (c == '/' || c == '\0') {
+ size_t n = i - start;
+ if (n == 0) return 0;
+ if (n == 1 && p[start] == '.') return 0;
+ if (n == 2 && p[start] == '.' && p[start + 1] == '.') return 0;
+ if (c == '\0') return 1;
+ start = i + 1u;
+ }
+ }
+}
+
static int parse_u64(const char* s, uint64_t* out) {
char* end = NULL;
unsigned long long v;
@@ -225,6 +242,8 @@ int dist_manifest_parse(const uint8_t* data, size_t len, DistManifest* m,
return set_err(err, errcap, "bad artifact id");
seen |= F_ID;
} else if (strcmp(key, "path") == 0) {
+ if (!dist_manifest_path_valid(val))
+ return set_err(err, errcap, "unsafe artifact path");
if (copy_field(art->path, sizeof art->path, val, err, errcap))
return DIST_ERR;
seen |= F_PATH;
diff --git a/driver/dist/manifest.h b/driver/dist/manifest.h
@@ -46,6 +46,8 @@ typedef struct DistManifest {
int dist_manifest_emit(const DistManifest* m, CfreeWriter* out);
+int dist_manifest_path_valid(const char* path);
+
int dist_manifest_parse(const uint8_t* data, size_t len, DistManifest* m,
char* err, size_t errcap);
diff --git a/driver/pkg.c b/driver/pkg.c
@@ -161,6 +161,10 @@ static int pkg_load_inputs(const CfreeContext* ctx, const char** files,
for (i = 0; i < n_files; ++i) {
in[i].src = files[i];
in[i].path = driver_basename(files[i]);
+ if (!dist_manifest_path_valid(in[i].path)) {
+ driver_errf(PKG_TOOL, "create: unsafe artifact path: %s", in[i].path);
+ return DIST_ERR;
+ }
if (pkg_read_file(ctx, files[i], &in[i].fd) != DIST_OK) {
driver_errf(PKG_TOOL, "create: cannot read file: %s", files[i]);
return DIST_ERR;
@@ -971,6 +975,7 @@ static int pkg_verify_native(DriverEnv* env, const CfreeContext* ctx,
if (memcmp(d.package_id, v.package_id, DIST_BLAKE2B_LEN) != 0 ||
d.index_offset != h.index_offset || d.index_size != h.index_size ||
d.content_offset != h.content_offset || d.content_size != h.content_size ||
+ d.chunk_size != h.chunk_size || d.alignment != h.alignment ||
d.chunk_size != DIST_CFPKG_CHUNK_SIZE_DEFAULT ||
d.alignment != DIST_CFPKG_ALIGNMENT) {
driver_errf(PKG_TOOL, "encoding descriptor does not match package layout");
diff --git a/test/pkg/run.sh b/test/pkg/run.sh
@@ -18,7 +18,8 @@ trap 'rm -rf "$work"' EXIT
HOME="$work/home"
CFREE_TRUSTED_KEYS="$work/trusted_keys"
-export HOME CFREE_TRUSTED_KEYS
+SOURCE_DATE_EPOCH=1
+export HOME CFREE_TRUSTED_KEYS SOURCE_DATE_EPOCH
mkdir -p "$HOME" "$work/in" "$work/pkg" "$work/unpack"
pass=0
@@ -433,6 +434,8 @@ if have_cmd perl; then
descriptor_off=$(cfpkg_field_offset "$work/pkg/matrix-none.cfpkg" 4)
index_off=$(cfpkg_field_offset "$work/pkg/matrix-none.cfpkg" 10)
content_off=$(cfpkg_field_offset "$work/pkg/matrix-none.cfpkg" 12)
+ alignment_off=$((16 + 8 * 14))
+ chunk_size_off=$((16 + 8 * 15))
cp "$work/pkg/matrix-none.cfpkg" "$work/pkg/bad-native-descriptor.cfpkg"
flip_byte "$work/pkg/bad-native-descriptor.cfpkg" "$descriptor_off"
@@ -448,6 +451,16 @@ if have_cmd perl; then
flip_byte "$work/pkg/bad-native-content.cfpkg" "$content_off"
run_fail "native-mutated-content-fails" "$CFREE" pkg verify -p "$work/key.pub" \
"$work/pkg/bad-native-content.cfpkg"
+
+ cp "$work/pkg/matrix-none.cfpkg" "$work/pkg/bad-native-alignment.cfpkg"
+ flip_byte "$work/pkg/bad-native-alignment.cfpkg" "$alignment_off"
+ run_fail "native-mutated-header-alignment-fails" "$CFREE" pkg verify -p "$work/key.pub" \
+ "$work/pkg/bad-native-alignment.cfpkg"
+
+ cp "$work/pkg/matrix-none.cfpkg" "$work/pkg/bad-native-chunk-size.cfpkg"
+ flip_byte "$work/pkg/bad-native-chunk-size.cfpkg" "$chunk_size_off"
+ run_fail "native-mutated-header-chunk-size-fails" "$CFREE" pkg verify -p "$work/key.pub" \
+ "$work/pkg/bad-native-chunk-size.cfpkg"
else
skip_test "native-region-offset-corruption-tests"
fi
@@ -466,6 +479,10 @@ printf two > "$work/dup/b/same.dat"
run_fail "pkg-create-duplicate-artifact-path-fails" "$CFREE" pkg create \
--name matrix-test --version 1.0.0 --format tar.gz -s "$work/key.key" \
-o "$work/pkg/duplicate.tar.gz" "$work/dup/a/same.dat" "$work/dup/b/same.dat"
+printf unsafe > "$work/in/bad\\path.dat"
+run_fail "pkg-create-unsafe-artifact-path-fails" "$CFREE" pkg create \
+ --name matrix-test --version 1.0.0 --format tar.gz -s "$work/key.key" \
+ -o "$work/pkg/unsafe-path.tar.gz" "$work/in/bad\\path.dat"
run_fail "pkg-verify-missing-file-fails" "$CFREE" pkg verify -p "$work/key.pub" \
"$work/pkg/does-not-exist.tar.gz"
printf not-a-package > "$work/not-a-package.cfpkg"