Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions src/test_suite/fuzz_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)


Expand Down
3 changes: 3 additions & 0 deletions src/test_suite/fuzz_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 15 additions & 7 deletions src/test_suite/multiprocessing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Comment on lines +179 to +183
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)
Expand Down Expand Up @@ -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
Comment on lines +214 to +218
else:
output_object.ParseFromString(output_data)

return output_object

Expand Down
78 changes: 78 additions & 0 deletions src/test_suite/test_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Comment on lines +1545 to +1549
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)
Expand Down Expand Up @@ -1587,6 +1599,54 @@ 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}")):
with open(fix_file, "rb") as f:
raw = f.read()

try:
meta = _MetadataOnlyFixture()
meta.ParseFromString(raw)
if meta.HasField("metadata") and meta.metadata.fn_entrypoint:
continue
except Exception:
pass

try:
ctx = harness.context_type()
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))
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(
Expand Down Expand Up @@ -1679,6 +1739,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
)
Comment on lines +1747 to +1755
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)
Expand Down
41 changes: 41 additions & 0 deletions tests/test_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,47 @@ def test_buf_works(self, buf_path):
assert result.returncode == 0


class TestRawBinaryIO:
"""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 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

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)."""

Expand Down
Loading