Repository navigation
Conversation
Move the audit atomic writer into a shared fs_util module and use it for the prebid bundle metadata patch, so the config keeps its permissions, is fsynced, and leaves no temp file behind on failure. Report TOML parse errors by byte offset instead of echoing the parser snippet, including in ts audit generate, through one shared toml_error_location helper. Closes #1202. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
prk-Jr
left a comment
There was a problem hiding this comment.
Summary
Moves the hardened atomic writer into a shared fs_util module, points ts prebid bundle at it, and stops echoing parser text in config parse errors. I ran the new permission and parse-error tests against main's implementation: both fail there and pass here, so they guard the regression from #1202. No blocking findings.
1 of the inline comments below carries a one-click GitHub
suggestion.
Non-blocking
🏕 camp site
- No test for the
patch_config_metadataparse-error redaction — see inline atcrates/trusted-server-cli/src/prebid_bundle.rs:1916
⛏ nitpick
patch_config_metadata_leaves_no_temp_filespasses onmaintoo — see inline atcrates/trusted-server-cli/src/prebid_bundle.rs:1883
👍 praise
- Shared writer and documented echo exception — see inline at
crates/trusted-server-cli/src/fs_util.rs:24
Cross-cutting / body-level findings
- 🏕 Two
ts audit generateparse sites still echo{error}—managed_creative_projection(commands/audit/generate/mod.rs:1161,"failed to parse config for dry-run diff: {error}") andensure_only_managed_fields_changed::unmanaged(commands/audit/generate/slot_toml.rs:705,"failed to validate updated config: {error}") both parse the operator's config and format the parser message. In practice neither is reachable with a malformed file, becausecreative_config(commands/audit/mod.rs:334) already parsed the same text with the sametomlparser. But the PR description says parse errors ints audit generate"now report only the path and byte offset". Routing both throughtoml_error_locationkeeps that true by construction, so a future reordering can't reintroduce the leak. Otherwise, narrow the claim in the description. - 🌱
.ts-write-*temp files are not gitignored — the new writer removes its temp file on any error path, so issue item 3 is fixed for ordinary failures. A SIGKILL or power loss between create and rename can still leave a partial.ts-write-XXXXXXcopy of the config next to it. It is now0600, butgit statuslists it as untracked in a checkout andgit add -Awould pick it up. A one-line.ts-write-*entry in.gitignore, next totrusted-server.toml, would close that.
CI Status
- integration tests: PASS
- browser integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- CodeQL: PASS
- cargo test (ts CLI, native) (macos-latest): PASS
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- cargo test (axum native): PASS
- cargo test (cross-adapter parity): PASS
- cargo test: PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- cargo fmt: PASS
- CLAUDE.md symlink guard: PASS
- Analyze (rust): PASS
- Analyze (python): PASS
- Analyze (javascript-typescript): PASS
- Analyze (actions): PASS
- vitest: PASS
- format-typescript: PASS
- format-docs: PASS
- prepare integration artifacts: PASS
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Approved. Sharing the hardened atomic writer and reporting TOML parse locations without config text addresses the permission-preservation and privacy concerns described in this PR.
aram356
left a comment
There was a problem hiding this comment.
Summary
Moves the hardened atomic writer into a shared fs_util module, points ts prebid client at it, and redacts TOML parse errors through one toml_error_location helper. One blocking test gap: the permission regression test can't detect the target's mode being dropped, which I proved by deleting the copy and watching every test pass. The other findings are non-blocking.
1 of the inline comments below carries a one-click GitHub
suggestion. Use Commit suggestion to apply it as a commit on the PR branch. The remaining comments describe the change in prose because it touches several call sites or is a follow-up.
Blocking
🔧 wrench
- Permission test can't detect a dropped mode copy: see inline at
crates/trusted-server-cli/src/prebid_bundle.rs:1864
Non-blocking
🤔 thinking
- Report line and column instead of a byte offset: see inline at
crates/trusted-server-cli/src/error.rs:45
🌱 seedling
- The shared writer replaces symlinks and carries over mode bits only: see inline at
crates/trusted-server-cli/src/fs_util.rs:38
Cross-cutting / body-level findings
-
📌
ts config validate/push/diffstill print the offending config line: these load through edgezero-core'sdeserialize_app_config_with_options, whoseAppConfigErrorDisplay isfailed to parse {path} as TOML: {source}. The CLI's ownapp_config::load_settings(ts config ad-templates *,ts audit ad-templates verify) wraps the same error asfailed to load {path}: {err}. Probe at this head,load_settingson a config whose third line is an unterminatedorigin_urlstring:failed to load <tmp>/trusted-server.toml: failed to parse <tmp>/trusted-server.toml as TOML: TOML parse error at line 3, column 58 | 3 | origin_url = "https://origin-private.example.com/internal | ^ invalid basic string, expected `"`ts config pushis the step most likely to run in CI, so the "lands in terminals and CI logs" scenario from the description is still open on that path. Out of scope for this PR; worth a follow-up issue covering edgezero's loader and the CLI wrapper. -
⛏ The title names the retired
ts prebid bundlespelling: the command ists prebid client, andprebid_rejects_retired_bundle_command(run.rs:211) pins thatts prebid bundleno longer parses. The squash commit inherits the PR title, soKeep config permissions and hide config text in ts prebid client errorskeepsgit logsearchable for the command operators actually run. The description'sts prebid bundlementions need the same change.
CI Status
- integration tests: PASS
- browser integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- CodeQL: PASS
- cargo test (ts CLI, native) (macos-latest): PASS
- cargo test (axum native): PASS
- CLAUDE.md symlink guard: PASS
- Analyze (rust): PASS
- Analyze (python): PASS
- cargo fmt: PASS (required)
- Analyze (javascript-typescript): PASS
- vitest: PASS
- format-typescript: PASS (required)
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- Analyze (javascript-typescript): PASS
- cargo test (cross-adapter parity): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- format-docs: PASS (required)
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- prepare integration artifacts: PASS
- cargo test: PASS (required)
- Analyze (actions): PASS
Map redacted TOML error spans to line and column, prove mode preservation with a 0640 config, cover the patch_config_metadata parse error, and narrow the atomic writer doc to the mode bits it actually keeps. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
|
Filed #1396 for |
Route the dry-run diff and managed-field validation parse errors through toml_error_location, add regression tests, and gitignore .ts-write-* temp files left by an interrupted config write. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
|
@prk-Jr both findings from your review are addressed in d61637a. Both |
| error.contains("at line 2, column "), | ||
| "should report the error location: {error}" | ||
| ); | ||
| } |
There was a problem hiding this comment.
⛏ nitpick — splice_creative_slots is the one redacted parse site without a no-echo test. Restoring : {error} to the message at line 630 passes all 352 prebid_bundle, fs_util, error and audit tests, and so does passing "" instead of existing to toml_error_location (the error then says line 1, column 1). Same shape as the test above:
| } | |
| } | |
| #[test] | |
| fn splice_creative_slots_parse_error_does_not_echo_config_source() { | |
| let existing = "[publisher]\norigin_url = \"https://origin-private.example.com/internal\n"; | |
| let error = splice_creative_slots(existing, &network_keys("222"), &header_rendered()) | |
| .expect_err("should reject invalid TOML"); | |
| assert!( | |
| !error.contains("origin-private.example.com"), | |
| "should not echo config source text: {error}" | |
| ); | |
| assert!( | |
| error.contains("at line 2, column "), | |
| "should report the error location: {error}" | |
| ); | |
| } |
Verified in a scratch worktree: passes at this head, fails under both mutations, and cargo fmt --check, cargo clippy-cli and the full trusted-server-cli suite pass with it applied.
| error.contains("at line "), | ||
| "should report the error location: {error}" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn patch_config_metadata_parse_error_does_not_echo_config_source() { | ||
| let (_temp, path) = write_config( | ||
| "[publisher]\norigin_url = \"https://origin-private.example.com/internal\n", | ||
| ); | ||
|
|
||
| let error = patch_config_metadata(&path, &"a".repeat(64), "sha384-abc") | ||
| .expect_err("should reject invalid TOML"); | ||
|
|
||
| assert!( | ||
| !error.contains("origin-private.example.com"), | ||
| "should not echo config source text: {error}" | ||
| ); | ||
| assert!( | ||
| error.contains("at line "), |
There was a problem hiding this comment.
⛏ nitpick — Both prebid parse tests only check for at line , so they pass when the helper gets the wrong text. Passing "" instead of &contents at line 488 or line 761 reports line 1, column 1, and every test still passes. The audit tests pin at line 2, column ; this does the same for both tests in one click (the lines in between are unchanged):
| error.contains("at line "), | |
| "should report the error location: {error}" | |
| ); | |
| } | |
| #[test] | |
| fn patch_config_metadata_parse_error_does_not_echo_config_source() { | |
| let (_temp, path) = write_config( | |
| "[publisher]\norigin_url = \"https://origin-private.example.com/internal\n", | |
| ); | |
| let error = patch_config_metadata(&path, &"a".repeat(64), "sha384-abc") | |
| .expect_err("should reject invalid TOML"); | |
| assert!( | |
| !error.contains("origin-private.example.com"), | |
| "should not echo config source text: {error}" | |
| ); | |
| assert!( | |
| error.contains("at line "), | |
| error.contains("at line 2, column "), | |
| "should report the error location: {error}" | |
| ); | |
| } | |
| #[test] | |
| fn patch_config_metadata_parse_error_does_not_echo_config_source() { | |
| let (_temp, path) = write_config( | |
| "[publisher]\norigin_url = \"https://origin-private.example.com/internal\n", | |
| ); | |
| let error = patch_config_metadata(&path, &"a".repeat(64), "sha384-abc") | |
| .expect_err("should reject invalid TOML"); | |
| assert!( | |
| !error.contains("origin-private.example.com"), | |
| "should not echo config source text: {error}" | |
| ); | |
| assert!( | |
| error.contains("at line 2, column "), |
Verified in a scratch worktree: passes at this head, fails under both mutations, and cargo fmt --check, cargo clippy-cli and the full trusted-server-cli suite pass with it applied.
| location, " at line 2, column 5", | ||
| "should map offset to line and column" | ||
| ); | ||
| } |
There was a problem hiding this comment.
⛏ nitpick — Nothing pins that the column counts characters. Swapping tail.chars().count() for tail.len() passes every test, but would put the x in a = "é" x at column 10. The current code is right: I compared this helper with toml's own line, column on malformed inputs with multibyte, emoji, tab, CRLF and BOM content, and it matched on all of them. This keeps it that way:
| } | |
| } | |
| #[test] | |
| fn toml_error_location_counts_columns_in_characters() { | |
| // "é" is two bytes, so counting bytes would report column 10. | |
| let source = "a = \"é\" x\n"; | |
| let location = toml_error_location(source, Some(9..10)); | |
| assert_eq!( | |
| location, " at line 1, column 9", | |
| "should count the column in characters, not bytes" | |
| ); | |
| } |
Verified in a scratch worktree: passes at this head, fails with the byte-count mutation, and cargo fmt --check, cargo clippy-cli and the full trusted-server-cli suite pass with it applied.
| prebid.insert("external_bundle_sri", value(sri)); | ||
|
|
||
| write_atomic(config_path, &document.to_string()) | ||
| write_file_atomically(config_path, &document.to_string()).map_err(|error| { |
There was a problem hiding this comment.
🏕 camp site — This is the operator-visible fix, but CHANGELOG.md has no entry for it. The fix keeps a config's current mode, so a 0600 config that an earlier ts prebid client already reset to 0644 stays 0644; operators only learn to restrict it again if the release notes tell them. Suggested entry under ### Security in [Unreleased]:
ts prebid clientnow keeps the config file's mode bits when it writesexternal_bundle_sha256andexternal_bundle_sri; previously a0600config was reset to the umask default (usually0644). Configs already rewritten by an earlier version keep the widened mode, so restrict them again (for examplechmod 600 trusted-server.toml). The rewrite is fsynced and leaves no temporary copy behind on failure. TOML parse errors fromts prebid clientandts audit generatenow name the file, line and column without quoting the offending config line (ts prebid bundle widens config file permissions and echoes config lines in parse errors #1202).
Apply manually: CHANGELOG.md isn't part of this diff, so this can't be a one-click suggestion.
Summary
ts prebid clientrewrote the operator's config with its own atomic writer, which reset a0600config to the umask default (usually0644), skipped fsync, and could leave a partial.tmp-{pid}copy of the config behind on a failed write. It now uses the hardened writer fromts audit generate, moved into a sharedfs_utilmodule.ts prebid clientandts audit generatenow report only the path plus line and column, through one sharedtoml_error_locationhelper.Changes
crates/trusted-server-cli/src/fs_util.rswrite_file_atomically(moved from the audit command), with a generic doc comment and a test that a failed rename leaves no temp filecrates/trusted-server-cli/src/lib.rsfs_utilcrates/trusted-server-cli/src/error.rstoml_error_locationhelper, which maps the parser span to a line and columncrates/trusted-server-cli/src/prebid_bundle.rswrite_atomicwithwrite_file_atomically; stop echoing parser text in both config parse errors; document why the bundle-section deserialization error is safe to echo; add regression tests for permissions, temp-file cleanup and error redactioncrates/trusted-server-cli/src/commands/audit/generate/mod.rscrates/trusted-server-cli/src/commands/audit/generate/slot_toml.rscrates/trusted-server-cli/src/commands/audit/mod.rs,crates/trusted-server-cli/src/commands/audit/generate/validate.rstoml_error_locationhelper instead of inline copiesCloses
Closes #1202
Test plan
cargo test-fastly && cargo test-axumcargo clippy-fastly && cargo clippy-axumcargo fmt --all -- --checkcd crates/trusted-server-js/lib && npx vitest runcd crates/trusted-server-js/lib && npm run formatcd docs && npm run formatcargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute servecargo test-cloudflare,cargo clippy-cloudflare,cargo clippy-cli, and./scripts/test-cli.sh(727 CLI tests pass, including the two failing tests from the issue)Checklist
unwrap()in production code — useexpect("should ...")tracingmacros (notprintln!)