Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions meld-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ enum Commands {
#[arg(long)]
no_attestation: bool,

/// Emit a byte-reproducible artifact (#325): derive the attestation
/// id from the output content and take the timestamp from
/// `SOURCE_DATE_EPOCH` (default epoch 0) instead of a random UUID +
/// wall clock, so identical input yields an identical sha256.
#[arg(long)]
reproducible: bool,

/// Disable the `component-provenance` custom section (#192).
/// The section maps each fused-module function index back to
/// its originating component + function index; downstream
Expand Down Expand Up @@ -163,6 +170,7 @@ fn main() -> Result<()> {
address_rebase,
stats,
no_attestation,
reproducible,
no_component_provenance,
dwarf,
preserve_names,
Expand All @@ -178,6 +186,7 @@ fn main() -> Result<()> {
address_rebase,
stats,
no_attestation,
reproducible,
no_component_provenance,
dwarf,
preserve_names,
Expand Down Expand Up @@ -235,6 +244,7 @@ fn fuse_command(
address_rebase: bool,
show_stats: bool,
no_attestation: bool,
reproducible: bool,
no_component_provenance: bool,
dwarf: String,
preserve_names: bool,
Expand Down Expand Up @@ -333,6 +343,7 @@ fn fuse_command(
let config = FuserConfig {
memory_strategy,
attestation: !no_attestation,
reproducible,
component_provenance: !no_component_provenance,
address_rebasing: address_rebase,
preserve_names,
Expand Down
1 change: 1 addition & 0 deletions meld-core/benches/fusion_benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ fn bench_config() -> FuserConfig {
FuserConfig {
memory_strategy: MemoryStrategy::MultiMemory,
attestation: false,
reproducible: false,
component_provenance: false,
address_rebasing: false,
preserve_names: false,
Expand Down
51 changes: 49 additions & 2 deletions meld-core/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ pub struct FusionAttestationBuilder {
tool_version: String,
tool_hash: Option<String>,
memory_strategy: String,
reproducible: bool,
}

impl FusionAttestationBuilder {
Expand All @@ -134,9 +135,18 @@ impl FusionAttestationBuilder {
tool_version: tool_version.into(),
tool_hash: None,
memory_strategy: "shared".to_string(),
reproducible: false,
}
}

/// Emit a byte-reproducible attestation (#325): derive the id from the
/// output content and take the timestamp from `SOURCE_DATE_EPOCH`
/// (default epoch 0) instead of a random UUID + wall clock.
pub fn reproducible(mut self, reproducible: bool) -> Self {
self.reproducible = reproducible;
self
}

/// Set the tool hash for reproducibility
pub fn tool_hash(mut self, hash: impl Into<String>) -> Self {
self.tool_hash = Some(hash.into());
Expand Down Expand Up @@ -211,8 +221,16 @@ impl FusionAttestationBuilder {
/// Build the attestation
pub fn build(self, output_bytes: &[u8], stats: &FusionStats) -> FusionAttestation {
let output_hash = compute_sha256(output_bytes);
let timestamp = chrono_timestamp();
let attestation_id = generate_uuid();
// #325: reproducible mode derives the id from the output content and
// the timestamp from SOURCE_DATE_EPOCH so the artifact is byte-stable.
let (timestamp, attestation_id) = if self.reproducible {
(
chrono_timestamp_from(source_date_epoch()),
generate_uuid_from(entropy_from_hex(&output_hash)),
)
} else {
(chrono_timestamp(), generate_uuid())
};

let size_reduction = if stats.input_size > 0 {
let diff = stats.input_size as i128 - stats.output_size as i128;
Expand Down Expand Up @@ -328,6 +346,35 @@ pub(crate) fn generate_uuid_from(entropy: u128) -> String {
)
}

/// Seconds-since-epoch for a reproducible attestation timestamp (#325).
///
/// Honors the ecosystem-standard `SOURCE_DATE_EPOCH` environment variable
/// (a decimal seconds-since-Unix-epoch string); when it is unset or
/// unparseable, falls back to `0` (`1970-01-01T00:00:00Z`) so a reproducible
/// build is byte-stable regardless of the wall clock. Only consulted on the
/// `--reproducible` path; normal builds keep the real `chrono_timestamp()`.
pub(crate) fn source_date_epoch() -> u64 {
std::env::var("SOURCE_DATE_EPOCH")
.ok()
.and_then(|s| s.trim().parse::<u64>().ok())
.unwrap_or(0)
}

/// Derive deterministic UUID entropy from a content hash (#325).
///
/// Takes the fused output's SHA-256 hex string and folds its first 32 hex
/// digits into a `u128`, so the attestation id is a stable function of the
/// artifact content rather than the wall clock. Short/odd input degrades
/// gracefully to whatever parses (never panics).
pub(crate) fn entropy_from_hex(hash_hex: &str) -> u128 {
let head: String = hash_hex
.chars()
.filter(|c| c.is_ascii_hexdigit())
.take(32)
.collect();
u128::from_str_radix(&head, 16).unwrap_or(0)
}

/// Get current timestamp in ISO 8601 format using the system clock.
///
/// Thin wrapper over [`chrono_timestamp_from`] sourcing seconds-since-epoch
Expand Down
113 changes: 110 additions & 3 deletions meld-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ pub struct FuserConfig {
/// Whether to generate attestation data
pub attestation: bool,

/// Whether to emit a byte-reproducible attestation (#325): derive the
/// attestation id from the output content and source the timestamp from
/// `SOURCE_DATE_EPOCH` (default epoch 0) instead of a random UUID + the
/// wall clock, so identical input yields an identical fused artifact.
pub reproducible: bool,

/// Whether to emit the `component-provenance` custom section
/// (issue #192). When enabled (the default), every defined
/// function in the fused module gets a back-pointer entry
Expand Down Expand Up @@ -144,6 +150,7 @@ impl Default for FuserConfig {
Self {
memory_strategy: MemoryStrategy::Auto,
attestation: true,
reproducible: false,
component_provenance: true,
address_rebasing: false,
preserve_names: false,
Expand Down Expand Up @@ -1832,7 +1839,8 @@ impl Fuser {
stats: &FusionStats,
) -> attestation::FusionAttestation {
let mut builder = FusionAttestationBuilder::new("meld", env!("CARGO_PKG_VERSION"))
.memory_strategy(self.memory_strategy_label());
.memory_strategy(self.memory_strategy_label())
.reproducible(self.config.reproducible);

for (index, component) in self.components.iter().enumerate() {
let name = component
Expand Down Expand Up @@ -1863,8 +1871,20 @@ impl Fuser {
};

let output_hash = attestation::compute_sha256(output_bytes);
let timestamp = attestation::chrono_timestamp();
let attestation_id = attestation::generate_uuid();
// #325: in reproducible mode, derive the id from the output content and
// the timestamp from SOURCE_DATE_EPOCH so the fused artifact is
// byte-stable across runs; otherwise keep the wall-clock/random id.
let (timestamp, attestation_id) = if self.config.reproducible {
(
attestation::chrono_timestamp_from(attestation::source_date_epoch()),
attestation::generate_uuid_from(attestation::entropy_from_hex(&output_hash)),
)
} else {
(
attestation::chrono_timestamp(),
attestation::generate_uuid(),
)
};

let mut inputs = Vec::new();
for (index, component) in self.components.iter().enumerate() {
Expand Down Expand Up @@ -2822,6 +2842,93 @@ mod tests {
}
}

/// #325: with `reproducible: true`, the fused artifact is byte-identical
/// across runs — the attestation id is derived from the output content and
/// the timestamp from `SOURCE_DATE_EPOCH` (default epoch 0), so the
/// random-UUID + wall-clock non-determinism is removed. The control
/// (reproducible off) must differ, proving the flag is what fixes it.
///
/// Scoped to the default build (the shipped configuration). Under the
/// optional `attestation` (wsc) feature the emitted section additionally
/// carries `tool_parameters` / `metadata` as `wsc_attestation` `HashMap`
/// fields, whose serialization order meld cannot control from this side —
/// full byte-reproducibility on that path needs an upstream fix (sorted /
/// BTreeMap serialization). The default `FusionAttestationBuilder` path has
/// no such maps and is fully reproducible.
#[cfg(not(feature = "attestation"))]
#[test]
fn test_reproducible_attestation_is_byte_stable() {
use wasm_encoder::{
CodeSection, Component, ExportKind, ExportSection, Function, FunctionSection,
Instruction, MemorySection, MemoryType, Module as EncoderModule, ModuleSection,
TypeSection, ValType,
};
fn build_minimal_component() -> Vec<u8> {
let mut types = TypeSection::new();
types.ty().function([], [ValType::I32]);
let mut functions = FunctionSection::new();
functions.function(0);
let mut memory = MemorySection::new();
memory.memory(MemoryType {
minimum: 1,
maximum: None,
memory64: false,
shared: false,
page_size_log2: None,
});
let mut exports = ExportSection::new();
exports.export("run", ExportKind::Func, 0);
exports.export("memory", ExportKind::Memory, 0);
let mut code = CodeSection::new();
let mut func = Function::new([]);
func.instruction(&Instruction::I32Const(42));
func.instruction(&Instruction::End);
code.function(&func);
let mut module = EncoderModule::new();
module
.section(&types)
.section(&functions)
.section(&memory)
.section(&exports)
.section(&code);
let mut component = Component::new();
component.section(&ModuleSection(&module));
component.finish()
}
let component_bytes = build_minimal_component();

let fuse_once = |reproducible: bool| -> Vec<u8> {
let config = FuserConfig {
attestation: true,
reproducible,
..FuserConfig::default()
};
let mut fuser = Fuser::new(config);
fuser
.add_component(&component_bytes)
.expect("add_component failed");
fuser.fuse().expect("fuse failed")
};

// Reproducible: identical bytes across independent runs.
let a = fuse_once(true);
let b = fuse_once(true);
assert_eq!(
a, b,
"#325: reproducible fusion must be byte-identical across runs"
);

// Control: without reproducible, the attestation's random UUID makes
// the output differ — otherwise the flag would be a no-op.
let c = fuse_once(false);
let d = fuse_once(false);
assert_ne!(
c, d,
"#325: non-reproducible fusion is expected to differ (random attestation id); \
if this ever holds, the reproducible flag is testing nothing"
);
}

/// SR-20 / SC-8: Fail-fast when a core module (not a component) is passed
/// to `add_component()`.
///
Expand Down
1 change: 1 addition & 0 deletions meld-core/tests/adapter_dwarf_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ fn fuse_remap() -> Vec<u8> {
memory_strategy: MemoryStrategy::MultiMemory,
dwarf_handling: DwarfHandling::Remap,
attestation: false,
reproducible: false,
..Default::default()
});
fuser.add_component_named(&a, Some("a")).unwrap();
Expand Down
13 changes: 13 additions & 0 deletions meld-core/tests/adapter_safety.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ fn test_sr12_adapter_generation_for_string_param() {
let config = FuserConfig {
memory_strategy: MemoryStrategy::MultiMemory,
attestation: false,
reproducible: false,
component_provenance: false,
address_rebasing: false,
preserve_names: false,
Expand Down Expand Up @@ -819,6 +820,7 @@ fn test_sr13_cabi_realloc_targets_correct_memory() {
let config = FuserConfig {
memory_strategy: MemoryStrategy::MultiMemory,
attestation: false,
reproducible: false,
component_provenance: false,
address_rebasing: false,
preserve_names: false,
Expand Down Expand Up @@ -1235,6 +1237,7 @@ fn test_sr15_list_copy_length() {
let config = FuserConfig {
memory_strategy: MemoryStrategy::MultiMemory,
attestation: false,
reproducible: false,
component_provenance: false,
address_rebasing: false,
preserve_names: false,
Expand Down Expand Up @@ -1731,6 +1734,7 @@ fn test_sr16_inner_pointer_fixup_list_string() {
let config = FuserConfig {
memory_strategy: MemoryStrategy::MultiMemory,
attestation: false,
reproducible: false,
component_provenance: false,
address_rebasing: false,
preserve_names: false,
Expand Down Expand Up @@ -2031,6 +2035,7 @@ fn test_sr17_utf8_to_utf16_string_transcoding() {
let config = FuserConfig {
memory_strategy: MemoryStrategy::MultiMemory,
attestation: false,
reproducible: false,
component_provenance: false,
address_rebasing: false,
preserve_names: false,
Expand Down Expand Up @@ -2120,6 +2125,7 @@ fn test_sr17_utf8_to_utf16_supplementary_plane_transcoding() {
let config = FuserConfig {
memory_strategy: MemoryStrategy::MultiMemory,
attestation: false,
reproducible: false,
component_provenance: false,
address_rebasing: false,
preserve_names: false,
Expand Down Expand Up @@ -2645,6 +2651,7 @@ fn test_sr17_utf16_to_utf8_supplementary_plane_transcoding() {
let config = FuserConfig {
memory_strategy: MemoryStrategy::MultiMemory,
attestation: false,
reproducible: false,
component_provenance: false,
address_rebasing: false,
preserve_names: false,
Expand Down Expand Up @@ -2719,6 +2726,7 @@ fn test_sr17_utf16_to_utf8_lone_high_surrogate_replacement() {
let config = FuserConfig {
memory_strategy: MemoryStrategy::MultiMemory,
attestation: false,
reproducible: false,
component_provenance: false,
address_rebasing: false,
preserve_names: false,
Expand Down Expand Up @@ -2783,6 +2791,7 @@ fn test_sr17_utf16_to_utf8_midstring_lone_surrogate_replacement() {
let config = FuserConfig {
memory_strategy: MemoryStrategy::MultiMemory,
attestation: false,
reproducible: false,
component_provenance: false,
address_rebasing: false,
preserve_names: false,
Expand Down Expand Up @@ -2872,6 +2881,7 @@ fn test_sr17_utf16_to_utf8_malformed_surrogate_matrix() {
let config = FuserConfig {
memory_strategy: MemoryStrategy::MultiMemory,
attestation: false,
reproducible: false,
component_provenance: false,
address_rebasing: false,
preserve_names: false,
Expand Down Expand Up @@ -3007,6 +3017,7 @@ fn ls_p_20_test_sr17_utf8_to_utf16_malformed_matrix() {
let config = FuserConfig {
memory_strategy: MemoryStrategy::MultiMemory,
attestation: false,
reproducible: false,
component_provenance: false,
address_rebasing: false,
preserve_names: false,
Expand Down Expand Up @@ -3082,6 +3093,7 @@ fn test_sr17_latin1_to_utf16_transcoding() {
let config = FuserConfig {
memory_strategy: MemoryStrategy::MultiMemory,
attestation: false,
reproducible: false,
component_provenance: false,
address_rebasing: false,
preserve_names: false,
Expand Down Expand Up @@ -3565,6 +3577,7 @@ fn fuse_run_i32(caller: &[u8], callee: &[u8], label: &str) -> i32 {
let config = FuserConfig {
memory_strategy: MemoryStrategy::MultiMemory,
attestation: false,
reproducible: false,
component_provenance: false,
address_rebasing: false,
preserve_names: false,
Expand Down
Loading
Loading