From ce44d5fc4c6c1774671cfbf3e2b5e7941345a32e Mon Sep 17 00:00:00 2001 From: Charles Moyes Date: Fri, 13 Mar 2026 20:42:16 +0000 Subject: [PATCH 1/5] respect tmpdir --- src/test_suite/sanitizer_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test_suite/sanitizer_utils.py b/src/test_suite/sanitizer_utils.py index b1b551d..9406082 100644 --- a/src/test_suite/sanitizer_utils.py +++ b/src/test_suite/sanitizer_utils.py @@ -112,7 +112,10 @@ def locate_sancov_stub() -> Optional[str]: pass # Build a stub library in a temp location - stub_dir = Path(tempfile.gettempdir()) / "solana_conformance_sancov_stub" + stub_dir = ( + Path(os.environ.get("TMPDIR", tempfile.gettempdir())) + / "solana_conformance_sancov_stub" + ) stub_so = stub_dir / "libsancov_stub.so" stub_c = stub_dir / "sancov_stub.c" From 39fd0a2dde48c75384c30c6d8935064c3f0206aa Mon Sep 17 00:00:00 2001 From: Charles Moyes Date: Fri, 13 Mar 2026 22:31:20 +0000 Subject: [PATCH 2/5] raw bytes support --- src/test_suite/fuzz_context.py | 1 + src/test_suite/fuzz_interface.py | 3 + src/test_suite/multiprocessing_utils.py | 22 ++++--- src/test_suite/test_suite.py | 79 ++++++++++++++++++++++++ tests/test_scripts.py | 80 +++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 7 deletions(-) diff --git a/src/test_suite/fuzz_context.py b/src/test_suite/fuzz_context.py index 81b3d20..b7c25d1 100644 --- a/src/test_suite/fuzz_context.py +++ b/src/test_suite/fuzz_context.py @@ -98,6 +98,7 @@ context_human_encode_fn=gossip_codec.encode_input, context_human_decode_fn=gossip_codec.decode_input, effects_human_encode_fn=gossip_codec.encode_output, + raw_binary_io=True, ) diff --git a/src/test_suite/fuzz_interface.py b/src/test_suite/fuzz_interface.py index 6ae31ef..5c9c075 100644 --- a/src/test_suite/fuzz_interface.py +++ b/src/test_suite/fuzz_interface.py @@ -84,6 +84,9 @@ class HarnessCtx: effects_human_decode_fn: Callable[[EffectsType], None] = generic_human_decode regenerate_transformation_fn: Callable[[FixtureType], None] = generic_transform supports_flatbuffers: bool = False + raw_binary_io: bool = ( + False # context.data passed as raw bytes; output is a raw single-byte bool + ) fixture_type: Type[FixtureType] = field(init=False) context_type: Type[ContextType] = field(init=False) effects_type: Type[EffectsType] = field(init=False) diff --git a/src/test_suite/multiprocessing_utils.py b/src/test_suite/multiprocessing_utils.py index ac3c83e..728cbe9 100644 --- a/src/test_suite/multiprocessing_utils.py +++ b/src/test_suite/multiprocessing_utils.py @@ -176,12 +176,13 @@ def process_target( - invoke_pb.InstrEffects | None: Result of instruction execution. """ - serialized_instruction_context = context.SerializeToString(deterministic=True) - if serialized_instruction_context is None: - return None - - # Prepare input data and output buffers - in_data = serialized_instruction_context + if harness_ctx.raw_binary_io: + in_data = context.data + else: + serialized_instruction_context = context.SerializeToString(deterministic=True) + if serialized_instruction_context is None: + return None + in_data = serialized_instruction_context in_ptr = (ctypes.c_uint8 * len(in_data))(*in_data) in_sz = len(in_data) out_sz = ctypes.c_uint64(OUTPUT_BUFFER_SIZE) @@ -209,7 +210,14 @@ def process_target( # Process the output output_data = bytearray(globals.output_buffer_pointer[: out_sz.value]) output_object = harness_ctx.effects_type() - output_object.ParseFromString(output_data) + + if harness_ctx.raw_binary_io and len(output_data) == 1: + for field_desc in output_object.DESCRIPTOR.fields: + if field_desc.type == field_desc.TYPE_BOOL: + setattr(output_object, field_desc.name, output_data[0] != 0) + break + else: + output_object.ParseFromString(output_data) return output_object diff --git a/src/test_suite/test_suite.py b/src/test_suite/test_suite.py index 28e1707..76c1a3b 100644 --- a/src/test_suite/test_suite.py +++ b/src/test_suite/test_suite.py @@ -1539,6 +1539,18 @@ def fetch_repros(client): if num_duplicates > 0: print(f"Removed {num_duplicates} duplicate(s)") + for section_name in section_names_list: + harness = _infer_raw_binary_harness(section_name) + if harness is not None: + converted = _convert_raw_crashes_to_contexts(globals.inputs_dir, harness) + if converted > 0: + default_harness_ctx = next( + name for name, obj in HARNESS_MAP.items() if obj is harness + ) + print( + f"Converted {converted} raw crash file(s) to {harness.context_extension} context(s)" + ) + create_fixtures_dir = globals.output_dir / "create_fixtures" if create_fixtures_dir.exists(): shutil.rmtree(create_fixtures_dir) @@ -1587,6 +1599,55 @@ def fetch_repros(client): ) +def _infer_raw_binary_harness(lineage: str) -> "HarnessCtx | None": + """If *lineage* maps to a raw_binary_io harness, return it. + Returns None otherwise (including for normal protobuf harnesses).""" + for entrypoint, harness in ENTRYPOINT_HARNESS_MAP.items(): + if not harness.raw_binary_io: + continue + core = ( + entrypoint.removeprefix("sol_compat_") + .removesuffix("_v1") + .removesuffix("_v2") + ) + if core and core in lineage: + return harness + return None + + +def _convert_raw_crashes_to_contexts(inputs_dir: Path, harness: "HarnessCtx") -> int: + """Convert raw (non-protobuf) .fix files in *inputs_dir* into context + protobuf files that a raw_binary_io harness can consume. Returns + the number of files converted.""" + from test_suite.multiprocessing_utils import _MetadataOnlyFixture + + converted = 0 + for fix_file in list(inputs_dir.rglob(f"*{FIXTURE_EXTENSION}")): + try: + with open(fix_file, "rb") as f: + raw = f.read() + meta = _MetadataOnlyFixture() + meta.ParseFromString(raw) + if meta.HasField("metadata") and meta.metadata.fn_entrypoint: + continue + except Exception: + pass + + try: + with open(fix_file, "rb") as f: + crash_data = f.read() + ctx = harness.context_type() + ctx.data = crash_data + ctx_path = fix_file.with_suffix(harness.context_extension) + with open(ctx_path, "wb") as f: + f.write(ctx.SerializeToString(deterministic=True)) + fix_file.unlink() + converted += 1 + except Exception as e: + print(f" Warning: failed to convert {fix_file.name}: {e}") + return converted + + @app.command(help="Debug a single repro by hash.") def debug_mismatch( repro_hash: str = typer.Argument( @@ -1679,6 +1740,24 @@ def debug_mismatch( raise typer.Exit(code=1) print(f"{result}\n") + # Convert raw crash files to context protobufs if needed. + # Targets like gossip (raw_binary_io) use raw binary inputs, not + # protobuf fixtures. + harness_ctx_for_lineage = _infer_raw_binary_harness(lineage) + if harness_ctx_for_lineage is not None: + converted = _convert_raw_crashes_to_contexts( + globals.inputs_dir, harness_ctx_for_lineage + ) + if converted > 0: + default_harness_ctx = next( + name + for name, obj in HARNESS_MAP.items() + if obj is harness_ctx_for_lineage + ) + print( + f"Converted {converted} raw crash file(s) to {harness_ctx_for_lineage.context_extension} context(s)" + ) + # Deduplicate print("Deduplicating fixtures...") num_duplicates = deduplicate_fixtures_by_hash(globals.inputs_dir) diff --git a/tests/test_scripts.py b/tests/test_scripts.py index 7120a18..2ef8354 100644 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -123,6 +123,86 @@ def test_buf_works(self, buf_path): assert result.returncode == 0 +class TestRawBinaryIO: + """Tests for the raw_binary_io harness flag (gossip support).""" + + def test_gossip_harness_has_flag(self): + """GossipHarness must have raw_binary_io=True.""" + from test_suite.fuzz_context import GossipHarness + + assert GossipHarness.raw_binary_io is True + + def test_other_harnesses_no_flag(self): + """All harnesses except GossipHarness must have raw_binary_io=False.""" + from test_suite.fuzz_context import HARNESS_MAP, GossipHarness + + for name, harness in HARNESS_MAP.items(): + if harness is GossipHarness: + continue + assert ( + harness.raw_binary_io is False + ), f"{name} unexpectedly has raw_binary_io=True" + + def test_infer_raw_binary_harness_gossip(self): + """_infer_raw_binary_harness finds GossipHarness for gossip lineages.""" + from test_suite.test_suite import _infer_raw_binary_harness + from test_suite.fuzz_context import GossipHarness + + assert ( + _infer_raw_binary_harness("sol_gossip_message_deserialize_diff") + is GossipHarness + ) + assert ( + _infer_raw_binary_harness("sol_gossip_message_deserialize_diff_hf") + is GossipHarness + ) + + def test_infer_raw_binary_harness_non_gossip(self): + """_infer_raw_binary_harness returns None for non-raw-binary lineages.""" + from test_suite.test_suite import _infer_raw_binary_harness + + assert _infer_raw_binary_harness("sol_vm_syscall_cpi_rust_diff") is None + assert _infer_raw_binary_harness("sol_compat_instr_execute") is None + + def test_convert_raw_crash_to_context(self, tmp_path): + """Raw .fix crash files are converted to .gossipctx context files.""" + from test_suite.test_suite import _convert_raw_crashes_to_contexts + from test_suite.fuzz_context import GossipHarness, FIXTURE_EXTENSION + import test_suite.protos.gossip_pb2 as gossip_pb + + crash_data = b"\x01\x00\x00\x00" + b"\x00" * 156 + fix_path = tmp_path / f"test_crash{FIXTURE_EXTENSION}" + fix_path.write_bytes(crash_data) + + converted = _convert_raw_crashes_to_contexts(tmp_path, GossipHarness) + assert converted == 1 + assert not fix_path.exists(), ".fix file should have been removed" + + ctx_path = tmp_path / "test_crash.gossipctx" + assert ctx_path.exists(), ".gossipctx file should have been created" + + ctx = gossip_pb.GossipMessageBinary() + ctx.ParseFromString(ctx_path.read_bytes()) + assert ctx.data == crash_data + + def test_convert_skips_valid_fixtures(self, tmp_path): + """Valid protobuf fixtures are not converted.""" + from test_suite.test_suite import _convert_raw_crashes_to_contexts + from test_suite.fuzz_context import GossipHarness, FIXTURE_EXTENSION + import test_suite.protos.gossip_pb2 as gossip_pb + import test_suite.protos.metadata_pb2 as metadata_pb + + fixture = gossip_pb.GossipMessageFixture() + fixture.metadata.fn_entrypoint = "sol_compat_gossip_message_deserialize_v1" + fixture.input.data = b"\x04\x00\x00\x00" + fix_path = tmp_path / f"valid{FIXTURE_EXTENSION}" + fix_path.write_bytes(fixture.SerializeToString(deterministic=True)) + + converted = _convert_raw_crashes_to_contexts(tmp_path, GossipHarness) + assert converted == 0 + assert fix_path.exists(), "valid .fix file should NOT have been removed" + + class TestGeneratedCode: """Tests for generated code (when available).""" From fdb914a9c27ba73ebd1fdb6f4c28198d6fc73195 Mon Sep 17 00:00:00 2001 From: Charles Moyes Date: Fri, 13 Mar 2026 22:34:23 +0000 Subject: [PATCH 3/5] remove useless tests --- tests/test_scripts.py | 40 +--------------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/tests/test_scripts.py b/tests/test_scripts.py index 2ef8354..cdb8fae 100644 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -124,45 +124,7 @@ def test_buf_works(self, buf_path): class TestRawBinaryIO: - """Tests for the raw_binary_io harness flag (gossip support).""" - - def test_gossip_harness_has_flag(self): - """GossipHarness must have raw_binary_io=True.""" - from test_suite.fuzz_context import GossipHarness - - assert GossipHarness.raw_binary_io is True - - def test_other_harnesses_no_flag(self): - """All harnesses except GossipHarness must have raw_binary_io=False.""" - from test_suite.fuzz_context import HARNESS_MAP, GossipHarness - - for name, harness in HARNESS_MAP.items(): - if harness is GossipHarness: - continue - assert ( - harness.raw_binary_io is False - ), f"{name} unexpectedly has raw_binary_io=True" - - def test_infer_raw_binary_harness_gossip(self): - """_infer_raw_binary_harness finds GossipHarness for gossip lineages.""" - from test_suite.test_suite import _infer_raw_binary_harness - from test_suite.fuzz_context import GossipHarness - - assert ( - _infer_raw_binary_harness("sol_gossip_message_deserialize_diff") - is GossipHarness - ) - assert ( - _infer_raw_binary_harness("sol_gossip_message_deserialize_diff_hf") - is GossipHarness - ) - - def test_infer_raw_binary_harness_non_gossip(self): - """_infer_raw_binary_harness returns None for non-raw-binary lineages.""" - from test_suite.test_suite import _infer_raw_binary_harness - - assert _infer_raw_binary_harness("sol_vm_syscall_cpi_rust_diff") is None - assert _infer_raw_binary_harness("sol_compat_instr_execute") is None + """Tests for the raw_binary_io harness flag.""" def test_convert_raw_crash_to_context(self, tmp_path): """Raw .fix crash files are converted to .gossipctx context files.""" From 0569c5da44dff4ed44df8a2c9031ed26a6e6fd2b Mon Sep 17 00:00:00 2001 From: Charles Moyes Date: Fri, 13 Mar 2026 22:43:19 +0000 Subject: [PATCH 4/5] PR comments --- src/test_suite/test_suite.py | 9 ++++----- tests/test_scripts.py | 1 - 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/test_suite/test_suite.py b/src/test_suite/test_suite.py index 76c1a3b..d8121d5 100644 --- a/src/test_suite/test_suite.py +++ b/src/test_suite/test_suite.py @@ -1623,9 +1623,10 @@ def _convert_raw_crashes_to_contexts(inputs_dir: Path, harness: "HarnessCtx") -> converted = 0 for fix_file in list(inputs_dir.rglob(f"*{FIXTURE_EXTENSION}")): + with open(fix_file, "rb") as f: + raw = f.read() + try: - with open(fix_file, "rb") as f: - raw = f.read() meta = _MetadataOnlyFixture() meta.ParseFromString(raw) if meta.HasField("metadata") and meta.metadata.fn_entrypoint: @@ -1634,10 +1635,8 @@ def _convert_raw_crashes_to_contexts(inputs_dir: Path, harness: "HarnessCtx") -> pass try: - with open(fix_file, "rb") as f: - crash_data = f.read() ctx = harness.context_type() - ctx.data = crash_data + ctx.data = raw ctx_path = fix_file.with_suffix(harness.context_extension) with open(ctx_path, "wb") as f: f.write(ctx.SerializeToString(deterministic=True)) diff --git a/tests/test_scripts.py b/tests/test_scripts.py index cdb8fae..c612614 100644 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -152,7 +152,6 @@ def test_convert_skips_valid_fixtures(self, tmp_path): from test_suite.test_suite import _convert_raw_crashes_to_contexts from test_suite.fuzz_context import GossipHarness, FIXTURE_EXTENSION import test_suite.protos.gossip_pb2 as gossip_pb - import test_suite.protos.metadata_pb2 as metadata_pb fixture = gossip_pb.GossipMessageFixture() fixture.metadata.fn_entrypoint = "sol_compat_gossip_message_deserialize_v1" From a3967b656ab3a4a69a9f5519380dbae79cd51bf5 Mon Sep 17 00:00:00 2001 From: Charles Moyes Date: Fri, 20 Mar 2026 21:16:56 +0000 Subject: [PATCH 5/5] make GCS credentials optional --- README.md | 54 +++++------------ src/test_suite/multiprocessing_utils.py | 13 ++-- src/test_suite/octane_api_client.py | 80 +++++++++++++++---------- 3 files changed, 70 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 9e53fce..478f6c5 100644 --- a/README.md +++ b/README.md @@ -216,26 +216,20 @@ Fixtures and crash inputs produced by the fuzzing infrastructure can be download ### Setup -1. **Install dependencies:** - ```sh - pip install -e ".[octane]" - ``` - -2. **GCS credentials** (for downloading artifacts from Google Cloud Storage): - - Credentials are auto-detected in this order: - 1. `GOOGLE_APPLICATION_CREDENTIALS` environment variable - 2. gcloud legacy credentials (`~/.config/gcloud/legacy_credentials//adc.json`) - 3. GCE metadata service (when running on Google Compute Engine) - - For manual setup: - ```sh - # Option 1: Use gcloud CLI (recommended) - gcloud auth application-default login - - # Option 2: Set credentials path explicitly - export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json - ``` +Artifact downloads are proxied through the API server by default — no cloud +credentials are required. + +For **optional** direct GCS/S3 downloads (faster, avoids the API proxy hop), +install the extra dependencies and configure credentials: + +```sh +pip install -e ".[octane]" +``` + +Credentials are auto-detected in this order: +1. `GOOGLE_APPLICATION_CREDENTIALS` environment variable +2. gcloud legacy credentials (`~/.config/gcloud/legacy_credentials//adc.json`) +3. GCE metadata service (when running on Google Compute Engine) ### Debugging workflow @@ -261,26 +255,6 @@ solana-conformance debug-mismatches -n sol_elf_loader_diff -l 5 \ -s $SOLFUZZ_TARGET -t $FIREDANCER_TARGET -o debug_output/ ``` -### GCS Authentication Troubleshooting - -GCS credentials are auto-detected from gcloud legacy credentials, so most users won't need manual setup. If you still see authentication errors: - -```sh -# Check if gcloud is authenticated -gcloud auth list - -# Check for legacy credentials (auto-detected) -ls ~/.config/gcloud/legacy_credentials/*/adc.json - -# Re-authenticate if needed -gcloud auth application-default login - -# Or set credentials explicitly -export GOOGLE_APPLICATION_CREDENTIALS=~/.config/gcloud/legacy_credentials//adc.json -``` - -**Note:** On GCE VMs without a service account attached, the metadata service won't work. The tool will automatically fall back to gcloud legacy credentials if available. - ## Setting up Environment To setup the `solana-conformance` environment, run the following command and you will be all set: ``` diff --git a/src/test_suite/multiprocessing_utils.py b/src/test_suite/multiprocessing_utils.py index 728cbe9..bb2c4c0 100644 --- a/src/test_suite/multiprocessing_utils.py +++ b/src/test_suite/multiprocessing_utils.py @@ -1025,9 +1025,9 @@ def download_and_process(source): api_origin=api_origin, http2=True, ) as client: - # If we have cached metadata with a BugRecord, download - # directly from the GCS/S3 URLs to avoid a redundant - # /api/bugs/ round-trip. + # If we have cached metadata with a BugRecord, use its + # URLs directly to avoid a redundant /api/bugs/ + # round-trip. bug_record = ( getattr(repro_metadata, "bug_record", None) if repro_metadata @@ -1120,10 +1120,9 @@ def download_single_crash(source): api_origin=api_origin, http2=True, ) as client: - # If we have cached metadata with a BugRecord, download directly - # from the GCS/S3 URLs to avoid a redundant /api/bugs/ - # round-trip (which can 404 due to bundle_id scoping in - # standalone mode). + # If we have cached metadata with a BugRecord, use its URLs + # directly to avoid a redundant /api/bugs/ round-trip + # (which can 404 due to bundle_id scoping in standalone mode). cached_meta = None if hasattr(globals, "repro_metadata_cache") and crash_hash in getattr( globals, "repro_metadata_cache", {} diff --git a/src/test_suite/octane_api_client.py b/src/test_suite/octane_api_client.py index 756a488..5e73974 100644 --- a/src/test_suite/octane_api_client.py +++ b/src/test_suite/octane_api_client.py @@ -8,11 +8,12 @@ - /api/bugs - List all bugs with full metadata (supports filters: lineages, hashes, statuses, run_id, bundle_id) - /api/bugs/ - Get single bug by hash - /api/bugs//artifact - Get artifact download URLs +- /api/artifact/download - Proxy endpoint for downloading artifacts - /api/health - Health check Key features: - Server-side filtering: lineages, hashes, statuses, run_id all combined with AND logic -- Direct GCS/S3 downloads: artifacts are downloaded directly from cloud storage +- Artifact downloads: proxied through the API server by default, with optional direct GCS/S3 fallback - Reproducible bugs: use statuses=REPRO_BUG_STATUSES or get_reproducible_bugs() Default API endpoint: gusc1b-fdfuzz-orchestrator1.jumpisolated.com:5000 @@ -425,12 +426,10 @@ class OctaneAPIClient: API client for the native Octane orchestrator API. This client uses the native Octane API endpoints (/api/bugs, etc.) - IMPORTANT: All artifact downloads are performed directly from cloud storage - (GCS/S3). The Octane API only provides metadata and download URLs, it never - proxies artifact bytes. + Artifact downloads use direct GCS/S3 access when the SDK and credentials + are available, and fall back to proxying through the API server otherwise. - Requires google-cloud-storage and/or boto3 for direct cloud downloads. - Install with: pip install "solana-conformance[octane]" + For direct downloads, install optional deps: pip install "solana-conformance[octane]" """ def __init__( @@ -688,10 +687,7 @@ def download_bug_artifact_native( progress_callback: Optional[Callable[[int, int], None]] = None, ) -> bytes: """ - Download bug artifact by first getting URLs from API, then downloading directly from GCS/S3. - - This is the preferred method as it downloads directly from cloud storage - without proxying through the Octane server. + Download bug artifact by first getting URLs from API, then downloading. Args: bug_hash: The bug fingerprint hash. @@ -969,32 +965,56 @@ def _download_from_s3( return data + def _download_via_http( + self, + url: str, + ) -> bytes: + """Download from an HTTP/HTTPS URL.""" + response = self.client.get(url) + response.raise_for_status() + data = response.content + + # Update shared progress bar + try: + import test_suite.globals as globals + + if globals.download_progress_bar is not None: + globals.download_progress_bar.update(len(data)) + except: + pass + + return data + + def _proxy_url(self, cloud_url: str) -> str: + """Convert a gs:// or s3:// URL to an API-proxied HTTP URL.""" + return f"{self.api_origin}/api/artifact/download?url={urllib.parse.quote(cloud_url, safe='')}" + def _download_from_url( self, url: str, progress_callback: Optional[Callable[[int, int], None]] = None, ) -> bytes: - """Download from a URL (GCS, S3, or HTTP).""" - if url.startswith("gs://"): - return self._download_from_gcs(url, progress_callback) - elif url.startswith("s3://"): - return self._download_from_s3(url, progress_callback) - elif url.startswith("http://") or url.startswith("https://"): - # HTTP download - response = self.client.get(url) - response.raise_for_status() - data = response.content + """Download from a URL (GCS, S3, or HTTP). - # Update shared progress bar + For gs:// and s3:// URLs, attempts direct download first if the + appropriate SDK is installed (google-cloud-storage or boto3). + Falls back to proxying through the API server if the SDK is missing + or direct download fails (e.g. missing credentials). + """ + if url.startswith("gs://") or url.startswith("s3://"): + # Try direct download if SDK is available try: - import test_suite.globals as globals - - if globals.download_progress_bar is not None: - globals.download_progress_bar.update(len(data)) - except: + if url.startswith("gs://"): + return self._download_from_gcs(url, progress_callback) + else: + return self._download_from_s3(url, progress_callback) + except (ImportError, Exception): pass - return data + # Fall back to API proxy (no SDK/credentials needed) + return self._download_via_http(self._proxy_url(url)) + elif url.startswith("http://") or url.startswith("https://"): + return self._download_via_http(url) else: raise ValueError(f"Unsupported URL scheme: {url}") @@ -1151,7 +1171,7 @@ def download_repro_data( Download repro data by hash: .fix first, then .fuzz as fallback. Use this for download-repro / download-repros commands. - Downloads directly from cloud storage (GCS/S3), never proxied through Octane. + Uses direct GCS/S3 download if available, otherwise proxied through the API server. Args: repro_hash: Hash of the repro to download. @@ -1180,7 +1200,7 @@ def download_fixture_data( Download fixture data (.fix file) ONLY by hash - no fallback to .fuzz. Use this for download-fixture / download-fixtures commands. - Downloads directly from cloud storage (GCS/S3), never proxied through Octane. + Uses direct GCS/S3 download if available, otherwise proxied through the API server. Args: artifact_hash: Hash of the artifact (bug hash) to download. @@ -1212,7 +1232,7 @@ def download_crash_data( Download crash data (.fuzz file) ONLY by hash - no fallback to .fix. Use this for download-crash / download-crashes commands. - Downloads directly from cloud storage (GCS/S3), never proxied through Octane. + Uses direct GCS/S3 download if available, otherwise proxied through the API server. Args: crash_hash: Hash of the crash (bug hash) to download.