Skip to content

Crash exec_result_code can be overwritten by the normal sync page_not_found path before host samling, causing real crashes to be reported as regular #345

Description

@hyunjungg

Hi,

I observed a real crash misclassification in kafl.qemu that appears to come from the upstream result handoff logic.

This is not only a source-level concern: I instrumented the QEMU/Nyx paths locally and directly observed a real rc_crash result being overwritten with rc_success before the host fuzzer sampled it.

Observed behavior

A target reliably triggered a real panic/crash path, but the input was sometimes reported to the fuzzer as regular instead of crash.

After adding tracing around the aux-buffer result flow, I observed this sequence:

  1. panic path entered
  2. exec_result_code set to rc_crash
  3. reload completed
  4. before the next host-visible sync point, exec_result_code was overwritten from rc_crash to rc_success
  5. the Python worker then sampled success and classified the input as regular

Directly observed overwrite path

The overwrite I observed was:

  • terminal result written in the panic/crash path
  • then overwritten in the next normal sync path by page_not_found handling

In other words, this was not a hypothetical overwrite candidate.
I directly observed:

  • crash -> success
  • at the synchronization_lock()/page_not_found path
  • before host sampling

Why this happens

1. Host sampling is delayed

In kafl.fuzzer, the host does not continuously observe the aux buffer.
It only reads the result when QEMU reaches the next control-socket sync boundary.

Relevant logic is in:

  • kafl_fuzzer/worker/qemu.py
  • run_qemu()
  • send_payload()

So once a terminal result is written, it must remain stable until the next host-visible sync point.

2. Crash path writes the result before host sampling

In upstream kafl.qemu:

  • handle_hypercall_kafl_panic()
  • synchronization_lock_crash_found()

The panic path writes rc_crash, performs reload, and returns.
The host does not sample the aux buffer at that exact point.

3. The aux result uses a single mutable exec_result_code slot

Multiple paths write directly into the same exec_result_code byte, including:

  • set_crash_auxiliary_result_buffer()
  • set_timeout_auxiliary_result_buffer()
  • set_asan_auxiliary_result_buffer()
  • set_success_auxiliary_result_buffer()

So the crash result is not latched.

4. The next normal sync path can overwrite it

In the run I traced, the overwrite happened in:

  • synchronization_lock()
  • specifically the synchronization_check_page_not_found() / page_not_found handling path

That path wrote success before the host sampled the earlier crash result.

Why I think this is an upstream bug

The implementation relies on the assumption that once a terminal result is written, it will still be present when the host reads it later.

But in the current structure:

  • terminal result is written first
  • host sampling happens later
  • normal sync code can still write into the same result slot

That makes the crash-report handoff non-atomic with respect to host observation.

Expected behavior

If the current execution has already been classified as crash, the host should not later observe that same execution as regular.

Once exec_result_code becomes terminal for an execution, later non-terminal sync logic should not downgrade it before host sampling.

Suggested fix direction

A robust fix would be:

  1. give terminal results (crash, timeout, asan, etc.) a dedicated handoff path,
  2. record exec_done / runtime / dirty-page metadata there,
  3. perform the host PING/wait handshake immediately for that terminal result,
  4. only reset the result slot after host ACK,
  5. prevent normal sync paths from overwriting an already-terminal exec_result_code.

Practical impact

This can cause real crashes to be misclassified as regular, depending on the post-reload sync flow.

If useful, I can also provide a minimal patch that fixes this by preserving terminal results until host handoff and guarding the normal sync overwrite path.

Thank you for reading ! :)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions