Skip to content

Commit 512edf2

Browse files
committed
v3: verify fallback retry source snapshots
1 parent e82f529 commit 512edf2

15 files changed

Lines changed: 349 additions & 85 deletions

File tree

cmd/v/macos_v3_dispatch.c.v

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ fn retry_macos_v3_with_old_compiler(caller_environment map[string]string, fallba
292292
return
293293
}
294294
export_macos_v3_report_content(report.kind, report.ccompiler, report.c_output,
295-
report.c_file, report.v_sources)
295+
report.c_file, report.v_sources, true)
296296
os.rmdir_all(c_error_dir) or {}
297297
if should_report {
298298
eprintln('V3 C compilation failed; retrying with `-old-compiler`.')
@@ -304,8 +304,16 @@ fn retry_macos_v3_with_old_compiler(caller_environment map[string]string, fallba
304304
// retry succeeds it prints the fallback notice and files the report. A directory
305305
// build (`v .`) or non-V input also remains metadata-only, but never silent.
306306
v_file, v_source := input_snapshot.current_report_source()
307+
// A post-parse V3 stage writes every exact parser digest into its owned staging
308+
// directory. If that manifest is unavailable (for example, an early parser
309+
// crash), the retry still runs but its report is not submitted because the
310+
// stable compiler cannot prove that it accepted the same complete input set.
311+
input_digests := read_macos_v3_source_digests(c_error_dir) or {
312+
map[string]string{}
313+
}
307314
export_macos_v3_bounded_report_content(macos_v3_compiler_error_fallback, 'v3',
308-
macos_v3_compiler_error_message(fallback_stage), v_file, v_source)
315+
macos_v3_compiler_error_message(fallback_stage), v_file, v_source, input_digests,
316+
input_digests.len > 0)
309317
os.rmdir_all(c_error_dir) or {}
310318
if should_report {
311319
eprintln('V3 compilation failed; retrying with `-old-compiler`.')
@@ -316,7 +324,7 @@ fn retry_macos_v3_with_old_compiler(caller_environment map[string]string, fallba
316324
// build succeeds the user sees the documented fallback notice (doc/docs.md) rather
317325
// than a silent switch that is indistinguishable from a direct V3 success.
318326
export_macos_v3_report_content(macos_v3_inline_asm_fallback, 'v3', '', '',
319-
map[string]string{})
327+
map[string]string{}, false)
320328
os.rmdir_all(c_error_dir) or {}
321329
if should_report {
322330
eprintln('V3 requested the compatibility compiler for inline assembly')
@@ -340,31 +348,36 @@ fn retry_macos_v3_with_old_compiler(caller_environment map[string]string, fallba
340348
fn take_macos_v3_report_content() ?MacosV3CErrorReport {
341349
report := builder.take_external_v3_report_from_env()?
342350
return MacosV3CErrorReport{
343-
kind: report.kind
344-
ccompiler: report.ccompiler
345-
c_output: report.c_output
346-
v_file: report.v_file
347-
v_source: report.v_source
351+
kind: report.kind
352+
ccompiler: report.ccompiler
353+
c_output: report.c_output
354+
v_file: report.v_file
355+
v_source: report.v_source
356+
input_digests: report.input_digests
357+
input_digests_complete: report.input_digests_complete
348358
}
349359
}
350360

351361
// export_macos_v3_report_content bounds the fallback source in THIS process — which staged
352362
// the report and therefore trusts `c_file` — and forwards only that content to the V1
353363
// retry through the environment.
354-
fn export_macos_v3_report_content(kind string, ccompiler string, c_output string, c_file string, v_sources map[string]string) {
364+
fn export_macos_v3_report_content(kind string, ccompiler string, c_output string, c_file string, v_sources map[string]string, input_digests_complete bool) {
355365
v_file, v_source := builder.bounded_v3_fallback_source(kind, c_output, c_file, v_sources)
356-
export_macos_v3_bounded_report_content(kind, ccompiler, c_output, v_file, v_source)
366+
export_macos_v3_bounded_report_content(kind, ccompiler, c_output, v_file, v_source, v_sources,
367+
input_digests_complete)
357368
}
358369

359-
fn export_macos_v3_bounded_report_content(kind string, ccompiler string, c_output string, v_file string, v_source string) {
370+
fn export_macos_v3_bounded_report_content(kind string, ccompiler string, c_output string, v_file string, v_source string, input_digests map[string]string, input_digests_complete bool) {
360371
builder.export_external_v3_report_to_env(builder.ExternalCErrorBugReport{
361-
kind: kind
362-
ccompiler: ccompiler
363-
c_output: c_output
364-
v_file: v_file
365-
v_source: v_source
366-
source_inline: true
367-
tag: 'V3'
372+
kind: kind
373+
ccompiler: ccompiler
374+
c_output: c_output
375+
v_file: v_file
376+
v_source: v_source
377+
source_inline: true
378+
input_digests: input_digests
379+
input_digests_complete: input_digests_complete
380+
tag: 'V3'
368381
})
369382
}
370383

@@ -461,6 +474,18 @@ fn read_macos_v3_c_error_report(report_dir string) ?MacosV3StagedReport {
461474
// compiler-error report may be notice-only (a directory / non-file build).
462475
return none
463476
}
477+
v_sources := read_macos_v3_source_digests(report_dir)?
478+
return MacosV3StagedReport{
479+
kind: kind
480+
ccompiler: ccompiler.trim_space()
481+
c_output: c_output
482+
c_file: c_file
483+
v_sources: v_sources
484+
report_dir: report_dir
485+
}
486+
}
487+
488+
fn read_macos_v3_source_digests(report_dir string) ?map[string]string {
464489
v_sources_text := os.read_file(os.join_path(report_dir, macos_v3_c_error_v_sources_file)) or {
465490
return none
466491
}
@@ -479,14 +504,7 @@ fn read_macos_v3_c_error_report(report_dir string) ?MacosV3StagedReport {
479504
}
480505
v_sources[path] = digest
481506
}
482-
return MacosV3StagedReport{
483-
kind: kind
484-
ccompiler: ccompiler.trim_space()
485-
c_output: c_output
486-
c_file: c_file
487-
v_sources: v_sources
488-
report_dir: report_dir
489-
}
507+
return v_sources
490508
}
491509

492510
fn macos_v3_child_environment(vexe string, fallback_file string, caller_environment map[string]string) map[string]string {

cmd/v/macos_v3_test.v

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1994,7 +1994,8 @@ fn test_macos_v3_inline_asm_trace_stays_off_generated_c_stdout() {
19941994
}
19951995

19961996
fn clear_macos_v3_report_env() {
1997-
for suffix in ['PRESENT', 'KIND', 'CCOMPILER', 'COUTPUT', 'TAG', 'VFILE', 'VSOURCE'] {
1997+
for suffix in ['PRESENT', 'KIND', 'CCOMPILER', 'COUTPUT', 'TAG', 'VFILE', 'VSOURCE',
1998+
'INPUT_DIGESTS', 'INPUT_DIGESTS_COMPLETE'] {
19981999
os.unsetenv('V_MACOS_V3_REPORT_${suffix}')
19992000
}
20002001
}
@@ -2019,7 +2020,7 @@ fn test_take_macos_v3_report_content_carries_no_path() {
20192020
// A forwarded content report round-trips as content only.
20202021
compiler_error := macos_v3_compiler_error_message('type specialization')
20212022
export_macos_v3_report_content(macos_v3_compiler_error_fallback, 'v3', compiler_error, '',
2022-
map[string]string{})
2023+
map[string]string{}, false)
20232024
report := take_macos_v3_report_content() or {
20242025
assert false, 'the forwarded content report must be returned'
20252026
return

cmd/v/v.v

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,10 @@ struct MacosV3CErrorReport {
7373
// Content only. The process that owned the staged report already bounded the source
7474
// and deleted the directory, so the retry never reads a path or deletes a directory
7575
// named by the (inheritable, forgeable) environment.
76-
v_file string // informational base filename (no directory)
77-
v_source string // already-bounded source snippet; never a whole file
76+
v_file string // informational base filename (no directory)
77+
v_source string // already-bounded source snippet; never a whole file
78+
input_digests map[string]string
79+
input_digests_complete bool
7880
}
7981

8082
@[unsafe]
@@ -407,13 +409,15 @@ fn rebuild(prefs &pref.Preferences, macos_v3_c_error_report ?MacosV3CErrorReport
407409
}
408410
if failed := macos_v3_c_error_report {
409411
builder.compile_with_external_c_error_report('build', prefs, cbuilder.compile_c, builder.ExternalCErrorBugReport{
410-
kind: failed.kind
411-
ccompiler: failed.ccompiler
412-
c_output: failed.c_output
413-
v_file: failed.v_file
414-
v_source: failed.v_source
415-
source_inline: true
416-
tag: 'V3'
412+
kind: failed.kind
413+
ccompiler: failed.ccompiler
414+
c_output: failed.c_output
415+
v_file: failed.v_file
416+
v_source: failed.v_source
417+
source_inline: true
418+
input_digests: failed.input_digests
419+
input_digests_complete: failed.input_digests_complete
420+
tag: 'V3'
417421
})
418422
} else {
419423
builder.compile('build', prefs, cbuilder.compile_c)
@@ -438,13 +442,15 @@ fn rebuild(prefs &pref.Preferences, macos_v3_c_error_report ?MacosV3CErrorReport
438442
// success without ever reading a path or deleting a directory named by the
439443
// (inheritable, forgeable) environment.
440444
builder.export_external_v3_report_to_env(builder.ExternalCErrorBugReport{
441-
kind: failed.kind
442-
ccompiler: failed.ccompiler
443-
c_output: failed.c_output
444-
v_file: failed.v_file
445-
v_source: failed.v_source
446-
source_inline: true
447-
tag: 'V3'
445+
kind: failed.kind
446+
ccompiler: failed.ccompiler
447+
c_output: failed.c_output
448+
v_file: failed.v_file
449+
v_source: failed.v_source
450+
source_inline: true
451+
input_digests: failed.input_digests
452+
input_digests_complete: failed.input_digests_complete
453+
tag: 'V3'
448454
})
449455
}
450456
util.launch_tool(prefs.is_verbose, 'builders/wasm_builder', os.args[1..])

doc/docs.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,11 @@ never uploaded.
119119
An internal V3 compiler error on a short program (and any directory build such as
120120
`v .`) submits metadata only. If the input selected for a source excerpt changes
121121
after V3 parses it, that report also submits metadata only rather than source V3
122-
did not parse. An unchanged input mapped from a generated-C error can still upload
123-
a strict-subset excerpt of that file plus a few lines of context around the failing
124-
line, even when the file is short. Inline-assembly
122+
did not parse. Before submitting any report, the stable compiler also verifies
123+
that it parsed the same bytes for every captured project input; if it did not, no
124+
fallback report is submitted. An unchanged input mapped from a generated-C error
125+
can still upload a strict-subset excerpt of that file plus a few lines of context
126+
around the failing line, even when the file is short. Inline-assembly
125127
fallbacks are notice-only and do not submit a report; reporting is also skipped
126128
for test compilations and to the default endpoint in GitHub CI. A custom fallback
127129
endpoint set with `V_C_ERROR_BUG_REPORT_URL` remains active in CI. The

vlib/v/ast/ast.v

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,13 +1181,17 @@ pub mut:
11811181
//
11821182
is_parse_text bool // true for files, produced by parse_text
11831183
is_template_text bool // true for files, produced by parse_comptime
1184+
// Set only for a V3->V1 retry. It is the SHA-256 digest of the exact scanner
1185+
// bytes, used to verify the retry before a fallback report is submitted.
1186+
source_digest string
11841187
}
11851188

11861189
@[unsafe]
11871190
pub fn (f &File) free() {
11881191
unsafe {
11891192
f.path.free()
11901193
f.path_base.free()
1194+
f.source_digest.free()
11911195
f.scope.free()
11921196
f.stmts.free()
11931197
f.imports.free()

vlib/v/builder/c_error_report.v

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module builder
33
import os
44
import strings
55
import crypto.sha256
6+
import encoding.base64
67
import v.pref
78
import v.gen.c as cgen
89
import v.util.version
@@ -26,6 +27,11 @@ const c_error_bug_report_truncation_notice = '\n... report truncated before uplo
2627
// compatibility fallback. The final upload is separately bounded by
2728
// c_error_bug_report_max_body_bytes.
2829
const c_error_bug_report_max_env_c_output_bytes = 64 * 1024
30+
// Keep the complete parsed-input manifest below Linux's per-environment-string
31+
// exec limit. If a very large project exceeds this bound, the retry still runs,
32+
// but its fallback report is conservatively suppressed because exact equivalence
33+
// cannot be proved.
34+
const v3_report_max_env_input_digests_bytes = 64 * 1024
2935

3036
struct CErrorReportLine {
3137
pub:
@@ -90,6 +96,11 @@ pub:
9096
v_file string // informational base filename of the failing source (no directory)
9197
v_source string // already-bounded source snippet; never a whole file
9298
source_inline bool // true: use v_file/v_source as-is and touch no filesystem path
99+
// Every path/digest pair below describes source bytes V3 actually parsed. The
100+
// stable parser compares these values only with its own trusted parsed-file
101+
// paths and scanner digests; it never opens a path supplied by this report.
102+
input_digests map[string]string
103+
input_digests_complete bool
93104
}
94105

95106
@[unsafe]
@@ -260,7 +271,49 @@ const v3_report_env_prefix = 'V_MACOS_V3_REPORT_'
260271
// and the take paths agree on exactly what to set and clear. Every variable carries
261272
// CONTENT only — never a filesystem path the receiver would read or a directory it
262273
// would delete.
263-
const v3_report_env_suffixes = ['PRESENT', 'KIND', 'CCOMPILER', 'COUTPUT', 'TAG', 'VFILE', 'VSOURCE']
274+
const v3_report_env_suffixes = ['PRESENT', 'KIND', 'CCOMPILER', 'COUTPUT', 'TAG', 'VFILE', 'VSOURCE',
275+
'INPUT_DIGESTS', 'INPUT_DIGESTS_COMPLETE']
276+
277+
// encode_v3_report_input_digests serializes arbitrary source paths without allowing a
278+
// newline or separator in a filename to corrupt the manifest. Paths are content used
279+
// only for equality checks against files the stable parser already opened itself.
280+
fn encode_v3_report_input_digests(input_digests map[string]string) ?string {
281+
mut paths := input_digests.keys()
282+
paths.sort()
283+
mut encoded := strings.new_builder(paths.len * 128)
284+
for path in paths {
285+
digest := input_digests[path]
286+
if digest.len != sha256.size * 2 {
287+
return none
288+
}
289+
encoded.write_string(base64.encode_str(path))
290+
encoded.write_u8(` `)
291+
encoded.write_string(digest)
292+
encoded.write_u8(`\n`)
293+
if encoded.len > v3_report_max_env_input_digests_bytes {
294+
return none
295+
}
296+
}
297+
return encoded.str()
298+
}
299+
300+
fn decode_v3_report_input_digests(encoded string) ?map[string]string {
301+
mut input_digests := map[string]string{}
302+
for line in encoded.split_into_lines() {
303+
separator := line.index_u8(` `)
304+
if separator <= 0 || separator + 1 >= line.len {
305+
return none
306+
}
307+
encoded_path := line[..separator]
308+
path := base64.decode_str(encoded_path)
309+
digest := line[separator + 1..]
310+
if path == '' || base64.encode_str(path) != encoded_path || digest.len != sha256.size * 2 {
311+
return none
312+
}
313+
input_digests[path] = digest
314+
}
315+
return input_digests
316+
}
264317

265318
// export_external_v3_report_to_env hands `report` to the next external builder (launched
266319
// via os.execvp) as self-contained content. That builder cannot authenticate anything
@@ -290,6 +343,17 @@ pub fn export_external_v3_report_to_env(report ExternalCErrorBugReport) {
290343
os.setenv('${v3_report_env_prefix}TAG', report.tag, true)
291344
os.setenv('${v3_report_env_prefix}VFILE', report.v_file, true)
292345
os.setenv('${v3_report_env_prefix}VSOURCE', report.v_source, true)
346+
if encoded := encode_v3_report_input_digests(report.input_digests) {
347+
os.setenv('${v3_report_env_prefix}INPUT_DIGESTS', encoded, true)
348+
os.setenv('${v3_report_env_prefix}INPUT_DIGESTS_COMPLETE', if report.input_digests_complete {
349+
'1'
350+
} else {
351+
'0'
352+
}, true)
353+
} else {
354+
os.setenv('${v3_report_env_prefix}INPUT_DIGESTS', '', true)
355+
os.setenv('${v3_report_env_prefix}INPUT_DIGESTS_COMPLETE', '0', true)
356+
}
293357
}
294358

295359
// bounded_v3_fallback_source extracts the bounded V source snippet to upload for a V3->V1
@@ -384,14 +448,20 @@ fn bounded_v_source_for_generated_c(c_output string, generated_c_file string, al
384448
// submission happen relative to the tool's own build outcome (only on success).
385449
pub fn take_external_v3_report_from_env() ?ExternalCErrorBugReport {
386450
present := os.getenv('${v3_report_env_prefix}PRESENT')
451+
input_digests := decode_v3_report_input_digests(os.getenv('${v3_report_env_prefix}INPUT_DIGESTS')) or {
452+
map[string]string{}
453+
}
387454
report := ExternalCErrorBugReport{
388-
kind: os.getenv('${v3_report_env_prefix}KIND')
389-
ccompiler: os.getenv('${v3_report_env_prefix}CCOMPILER')
390-
c_output: os.getenv('${v3_report_env_prefix}COUTPUT')
391-
tag: os.getenv('${v3_report_env_prefix}TAG')
392-
v_file: os.getenv('${v3_report_env_prefix}VFILE')
393-
v_source: os.getenv('${v3_report_env_prefix}VSOURCE')
394-
source_inline: true
455+
kind: os.getenv('${v3_report_env_prefix}KIND')
456+
ccompiler: os.getenv('${v3_report_env_prefix}CCOMPILER')
457+
c_output: os.getenv('${v3_report_env_prefix}COUTPUT')
458+
tag: os.getenv('${v3_report_env_prefix}TAG')
459+
v_file: os.getenv('${v3_report_env_prefix}VFILE')
460+
v_source: os.getenv('${v3_report_env_prefix}VSOURCE')
461+
source_inline: true
462+
input_digests: input_digests
463+
input_digests_complete: os.getenv('${v3_report_env_prefix}INPUT_DIGESTS_COMPLETE') == '1'
464+
&& input_digests.len > 0
395465
// c_file and cleanup_dir are intentionally left empty: the builder must not read
396466
// a file or delete a directory named by the environment.
397467
}
@@ -404,6 +474,35 @@ pub fn take_external_v3_report_from_env() ?ExternalCErrorBugReport {
404474
return report
405475
}
406476

477+
// matches_v3_fallback_inputs confirms that every source V3 recorded was parsed from the
478+
// same bytes by the stable compiler. Report paths are never opened: they are compared
479+
// only with canonical paths belonging to the stable parser's own AST files.
480+
fn (b &Builder) matches_v3_fallback_inputs(report ExternalCErrorBugReport) bool {
481+
if report.kind == external_v3_notice_only_kind {
482+
return true
483+
}
484+
if !report.input_digests_complete || report.input_digests.len == 0 {
485+
return false
486+
}
487+
mut stable_digests := map[string]string{}
488+
for file in b.parsed_files {
489+
if file.path == '' || file.source_digest == '' {
490+
continue
491+
}
492+
stable_digests[os.real_path(file.path)] = file.source_digest
493+
}
494+
for path, v3_digest in report.input_digests {
495+
if stable_digests[path] != v3_digest {
496+
return false
497+
}
498+
}
499+
return true
500+
}
501+
502+
fn discard_unverified_v3_fallback_report() {
503+
eprintln('note: source inputs changed before the stable compiler retry completed; the V3 fallback report was not submitted.')
504+
}
505+
407506
// submit_external_v3_compiler_error_bug_report reports metadata for a V3 internal
408507
// compiler error after the stable compiler has confirmed the program is buildable.
409508
// Source may only be supplied through the pre-V3 snapshot/content path, so this legacy

0 commit comments

Comments
 (0)