Skip to content

Commit ca62d24

Browse files
Bound auxiliary artifact byte hashing
Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
1 parent 2f0ddc1 commit ca62d24

6 files changed

Lines changed: 84 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818

1919
- Added bounded parser/report defaults to `ordvec-manifest` verification for
2020
manifest JSON size, row-identity JSONL line length, row count,
21-
duplicate-tracking memory, auxiliary artifact declaration count, report issue
22-
count, and SQLite cached report size.
21+
duplicate-tracking memory, auxiliary artifact declaration count and bytes,
22+
report issue count, and SQLite cached report size.
2323

2424
### Added
2525

docs/INDEX_PROVENANCE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ The manifest verifier checks:
6868
whose `row_id` equals the zero-based line number and whose `db_id` is
6969
non-empty, NUL-free, and unique by default;
7070
- declared auxiliary artifacts, checking each caller-named sidecar's path,
71-
SHA-256 digest, and byte length under the same default path policy as the
72-
primary index artifact;
71+
SHA-256 digest, byte length, and configured byte ceiling under the same
72+
default path policy as the primary index artifact;
7373
- optional `calibration` profile references, checking profile identity,
7474
path/hash integrity, encoder identity, and ordinalization compatibility;
7575
- attestation **shape** only: predicate type, builder id when present, and at
@@ -81,8 +81,8 @@ secondary indexes, or stores that a caller intends to load together with the
8181
ordvec index. The verifier does not interpret those bytes; it only reports
8282
whether declared required members were verified, whether optional members were
8383
present or absent, and whether any declared member failed path, size, or digest
84-
checks. Callers should load sidecars only after the relevant declaration is
85-
verified.
84+
checks or exceeded the configured auxiliary artifact byte limit. Callers should
85+
load sidecars only after the relevant declaration is verified.
8686

8787
When present, `calibration` binds an index artifact to a hashed ordinal profile
8888
used to interpret overlap, bucket, sign, or rank evidence under a calibrated

ordvec-manifest/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ Stable limit codes are part of the contract:
3838
(`row_identity_duplicate_tracking_limit_exceeded`);
3939
- auxiliary artifact declarations: 1,024
4040
(`auxiliary_artifact_count_limit_exceeded`);
41+
- auxiliary artifact bytes per declared file: 64 MiB
42+
(`auxiliary_artifact_file_too_large`);
4143
- collected report issues: 1,024, after which a
4244
`verification_report_issue_limit_exceeded` issue is emitted;
4345
- SQLite cached report JSON: 4 MiB (`sqlite_cached_report_too_large`).
@@ -46,8 +48,9 @@ The CLI exposes matching override flags on `inspect`, `verify`, `create`,
4648
`sqlite verify`, and `sqlite activate`: `--max-manifest-bytes`,
4749
`--max-row-map-line-bytes`, `--max-row-map-rows`,
4850
`--max-row-map-tracked-id-bytes`, `--max-auxiliary-artifacts`,
49-
`--max-report-issues`, and `--max-cached-report-bytes`. Library callers can
50-
override the same ceilings via `VerifyOptions::limits`.
51+
`--max-auxiliary-artifact-bytes`, `--max-report-issues`, and
52+
`--max-cached-report-bytes`. Library callers can override the same ceilings via
53+
`VerifyOptions::limits`.
5154

5255
Stable limit codes:
5356

@@ -58,6 +61,7 @@ Stable limit codes:
5861
| row-identity JSONL rows | `row_identity_row_count_limit_exceeded` | `row_identity_row_count_limit_exceeded` |
5962
| row-identity duplicate-tracking `db_id` bytes | `row_identity_duplicate_tracking_limit_exceeded` | `row_identity_duplicate_tracking_limit_exceeded` |
6063
| auxiliary artifact declarations | `auxiliary_artifact_count_limit_exceeded` | n/a |
64+
| auxiliary artifact bytes per declared file | `auxiliary_artifact_file_too_large` | n/a |
6165
| collected verification report issues | `verification_report_issue_limit_exceeded` | n/a |
6266
| SQLite cached report JSON bytes | n/a | `sqlite_cached_report_too_large` |
6367

ordvec-manifest/src/lib.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub const DEFAULT_MAX_ROW_IDENTITY_JSONL_LINE_BYTES: usize = 64 * 1024;
1919
pub const DEFAULT_MAX_ROW_IDENTITY_ROWS: usize = 10_000_000;
2020
pub const DEFAULT_MAX_ROW_IDENTITY_TRACKED_DB_ID_BYTES: usize = 64 * 1024 * 1024;
2121
pub const DEFAULT_MAX_AUXILIARY_ARTIFACTS: usize = 1024;
22+
pub const DEFAULT_MAX_AUXILIARY_ARTIFACT_BYTES: u64 = 64 * 1024 * 1024;
2223
pub const DEFAULT_MAX_REPORT_ISSUES: usize = 1024;
2324
pub const DEFAULT_MAX_CACHED_REPORT_BYTES: u64 = 4 * 1024 * 1024;
2425

@@ -1238,7 +1239,12 @@ fn verify_auxiliary_artifacts(
12381239
match resolve_auxiliary_artifact_path(artifact, &document.base_dir, options, report) {
12391240
AuxiliaryPathResolution::Resolved(resolved) => {
12401241
entry.canonical_path = Some(path_to_display(&resolved.canonical_path));
1241-
match sha256_file(&resolved.resolved_path) {
1242+
match sha256_file_bounded(
1243+
&resolved.resolved_path,
1244+
options.limits.max_auxiliary_artifact_bytes,
1245+
"auxiliary_artifact_file_too_large",
1246+
"auxiliary artifact",
1247+
) {
12421248
Ok(hash) => {
12431249
entry.sha256 = Some(hash.sha256.clone());
12441250
entry.size_bytes = Some(hash.size_bytes);
@@ -1273,17 +1279,17 @@ fn verify_auxiliary_artifacts(
12731279
}
12741280
}
12751281
Err(err) => {
1276-
mark_auxiliary_artifact_failed(
1277-
&mut entry,
1278-
"auxiliary_artifact_hash_failed",
1279-
);
1280-
report.error(
1281-
"auxiliary_artifact_hash_failed",
1282+
let code = err.code().unwrap_or("auxiliary_artifact_hash_failed");
1283+
mark_auxiliary_artifact_failed(&mut entry, code);
1284+
let message = if err.code().is_some() {
1285+
err.to_string()
1286+
} else {
12821287
format!(
12831288
"failed to hash auxiliary artifact {:?}: {err}",
12841289
artifact.name
1285-
),
1286-
);
1290+
)
1291+
};
1292+
report.error(code, message);
12871293
}
12881294
}
12891295
}
@@ -1542,6 +1548,7 @@ pub struct ResourceLimits {
15421548
pub max_row_identity_rows: usize,
15431549
pub max_row_identity_tracked_db_id_bytes: usize,
15441550
pub max_auxiliary_artifacts: usize,
1551+
pub max_auxiliary_artifact_bytes: u64,
15451552
pub max_report_issues: usize,
15461553
pub max_cached_report_bytes: u64,
15471554
}
@@ -1554,6 +1561,7 @@ impl Default for ResourceLimits {
15541561
max_row_identity_rows: DEFAULT_MAX_ROW_IDENTITY_ROWS,
15551562
max_row_identity_tracked_db_id_bytes: DEFAULT_MAX_ROW_IDENTITY_TRACKED_DB_ID_BYTES,
15561563
max_auxiliary_artifacts: DEFAULT_MAX_AUXILIARY_ARTIFACTS,
1564+
max_auxiliary_artifact_bytes: DEFAULT_MAX_AUXILIARY_ARTIFACT_BYTES,
15571565
max_report_issues: DEFAULT_MAX_REPORT_ISSUES,
15581566
max_cached_report_bytes: DEFAULT_MAX_CACHED_REPORT_BYTES,
15591567
}

ordvec-manifest/src/main.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ struct LimitArgs {
133133
#[arg(long)]
134134
max_auxiliary_artifacts: Option<usize>,
135135
#[arg(long)]
136+
max_auxiliary_artifact_bytes: Option<u64>,
137+
#[arg(long)]
136138
max_report_issues: Option<usize>,
137139
#[arg(long)]
138140
max_cached_report_bytes: Option<u64>,
@@ -156,6 +158,9 @@ impl LimitArgs {
156158
if let Some(value) = self.max_auxiliary_artifacts {
157159
limits.max_auxiliary_artifacts = value;
158160
}
161+
if let Some(value) = self.max_auxiliary_artifact_bytes {
162+
limits.max_auxiliary_artifact_bytes = value;
163+
}
159164
if let Some(value) = self.max_report_issues {
160165
limits.max_report_issues = value;
161166
}

ordvec-manifest/tests/manifest.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,6 +1540,56 @@ fn auxiliary_artifact_count_limit_is_enforced_before_verification() {
15401540
assert!(report.auxiliary_artifacts.is_empty());
15411541
}
15421542

1543+
#[test]
1544+
fn auxiliary_artifact_byte_limit_is_enforced_before_hashing() {
1545+
let root = tempfile::tempdir().unwrap();
1546+
let (temp, mut manifest, _manifest_path) = identity_manifest(root.path());
1547+
let sidecar = temp.path().join("sidecar.bin");
1548+
fs::write(&sidecar, b"sidecar").unwrap();
1549+
let sidecar_hash = sha256_file(&sidecar).unwrap();
1550+
manifest.auxiliary_artifacts = vec![auxiliary_artifact(
1551+
"sidecar",
1552+
"sidecar.bin",
1553+
sidecar_hash.clone(),
1554+
true,
1555+
)];
1556+
1557+
let report = verify_manifest_with_base(
1558+
manifest.clone(),
1559+
temp.path(),
1560+
VerifyOptions {
1561+
limits: ResourceLimits {
1562+
max_auxiliary_artifact_bytes: sidecar_hash.size_bytes - 1,
1563+
..ResourceLimits::default()
1564+
},
1565+
..VerifyOptions::default()
1566+
},
1567+
);
1568+
assert!(error_codes(&report).contains(&"auxiliary_artifact_file_too_large"));
1569+
assert_eq!(report.auxiliary_artifacts[0].sha256, None);
1570+
assert_eq!(
1571+
report.auxiliary_artifacts[0].reason_code.as_deref(),
1572+
Some("auxiliary_artifact_file_too_large")
1573+
);
1574+
1575+
let report = verify_manifest_with_base(
1576+
manifest,
1577+
temp.path(),
1578+
VerifyOptions {
1579+
limits: ResourceLimits {
1580+
max_auxiliary_artifact_bytes: sidecar_hash.size_bytes,
1581+
..ResourceLimits::default()
1582+
},
1583+
..VerifyOptions::default()
1584+
},
1585+
);
1586+
assert!(report.ok, "{:?}", report.errors);
1587+
assert_eq!(
1588+
report.auxiliary_artifacts[0].sha256.as_deref(),
1589+
Some(sidecar_hash.sha256.as_str())
1590+
);
1591+
}
1592+
15431593
#[test]
15441594
fn verification_report_deserializes_missing_auxiliary_artifacts_field() {
15451595
let root = tempfile::tempdir().unwrap();

0 commit comments

Comments
 (0)