name: fuzz run-name: Fuzz env: OWNER_RDPATH: ./source # Rel path to the dir that contains the fuzzing infra (contains "fuzz" dir). DURATION_SEC: 7200 # Fuzzing run duration in seconds. STDERR_LOG_FNAME: fuzz.stderr.log # File name to redirect the fuzzing run's stderr to. TMIN_LOG_FNAME: fuzz.tmin.log # File name to redirect the fuzzing input minimization log to. ARTIFACTS_RDPATH: fuzz/artifacts # Fuzzing artifacts rel dir path. SEEDS_RDPATH: fuzz/seed_inputs # Fuzzing seed inputs rel dir path. SEEDS_FNAME: list.txt # Fuzzing seed inputs list file name. PYTHON_VERSION: "3.11" on: workflow_dispatch: # Manual runs. push: branches: - main # Development runs against main branch. paths: - "source/compiler/**" # Run if the compiler was changed. - "source/fuzz/**" # Run if the fuzzing infra was changed. - ".github/workflows/fuzz.yml" # Run if the workflow itself was changed. - "!source/compiler/qsc_eval/**" # Exclude the qsc_eval dir. - "!source/compiler/qsc_codegen/**" # Exclude the qsc_codegen dir. jobs: fuzz: name: Fuzzing strategy: fail-fast: false matrix: os: [ubuntu-latest] # Fuzzing is not supported on Win. The macos is temporarily removed # because of low availability. target_name: [qsharp, qasm] runs-on: ${{ matrix.os }} permissions: issues: write steps: - name: Install and Configure Tools run: | rustup install nightly # Install nightly toolchain. rustup default nightly # Make nightly toolchain default. cargo install cargo-fuzz # Install cargo-fuzz (fuzzing tool). - name: Checkout the Repo uses: actions/checkout@v6 with: submodules: "true" - uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - name: Gather the Seed Inputs if: matrix.target_name == 'qsharp' working-directory: ${{ env.OWNER_RDPATH }} run: | # Clone the submodules of QDK: REPOS="Quantum Quantum-NC QuantumKatas QuantumLibraries iqsharp qdk-python qsharp-compiler qsharp-runtime" for REPO in $REPOS ; do git clone --depth 1 --single-branch --no-tags --recurse-submodules --shallow-submodules --jobs 4 \ https://github.com/microsoft/$REPO.git $SEEDS_RDPATH/${{ matrix.target_name }}/$REPO done # Build a comma-separated list of all the .qs files in $SEEDS_FNAME file: find $SEEDS_RDPATH/${{ matrix.target_name }} -name "*.qs" | tr "\n" "," > \ $SEEDS_RDPATH/${{ matrix.target_name }}/$SEEDS_FNAME - name: Gather the Seed Inputs (qasm) if: matrix.target_name == 'qasm' working-directory: ${{ env.OWNER_RDPATH }} run: | # Clone openqasm repo for samples: git clone --depth 1 --single-branch --no-tags --recurse-submodules --shallow-submodules --jobs 4 \ https://github.com/openqasm/openqasm.git $SEEDS_RDPATH/${{ matrix.target_name }}/openqasm # Build a comma-separated list of all the .qasm and .inc files in $SEEDS_FNAME file: find $SEEDS_RDPATH/${{ matrix.target_name }} -name "*.qasm" | tr "\n" "," > \ $SEEDS_RDPATH/${{ matrix.target_name }}/$SEEDS_FNAME find $SEEDS_RDPATH/${{ matrix.target_name }} -name "*.inc" | tr "\n" "," > \ $SEEDS_RDPATH/${{ matrix.target_name }}/$SEEDS_FNAME - name: Build and Run the Fuzz Target working-directory: ${{ env.OWNER_RDPATH }} run: | cargo fuzz build --fuzz-dir ./fuzz --release --sanitizer=none --features do_fuzz ${{ matrix.target_name }} # Build the fuzz target. # Run fuzzing for specified number of seconds and redirect the `stderr` to a file # whose name is specified by the STDERR_LOG_FNAME env var: RUST_BACKTRACE=1 cargo fuzz run --fuzz-dir ./fuzz --release --sanitizer=none --features do_fuzz ${{ matrix.target_name }} -- \ -seed_inputs=@$SEEDS_RDPATH/${{ matrix.target_name }}/$SEEDS_FNAME \ -max_total_time=$DURATION_SEC \ -rss_limit_mb=4096 \ -max_len=20000 \ 2>$STDERR_LOG_FNAME # The `-rss_limit_mb` and `-max_len` work around running out of memory. - name: "If Fuzzing Failed: Collect Failure Info" if: failure() working-directory: ${{ env.OWNER_RDPATH }} run: | # Extract from stderr log the panic message: PANIC_MESSAGE=`cat $STDERR_LOG_FNAME | grep "panicked at" | sed "s|thread '' panicked at '\([^']*\).*|\1|"` # Explanation: # `cat $STDERR_LOG_FNAME |`: Display the contents of the stderr log file and pass the contents # to the next command. # `grep "panicked at" |`: Filter out (drop) all the lines except the ones containing "panicked at", # the script expects that there is only one such line, pass that line to the next command. Line example: # thread '' panicked at 'global item should have type', . . ./compiler/qsc_frontend/src/typeck/rules.rs:300:26 # `sed "s|thread '' panicked at '\([^']*\).*|\1|"`: `sed` - stream editor. # `s` after quote: search command. After `s` there are two sections, each between a pair of '|'. # First section: # In the incoming stream search for a sequence starting with "thread '' panicked at '" # (sequence from the beginning of the line until after the apostrophe where the panic message starts), # followed by zero or more ('*' after ']') non-apostrophe chars (`[^']`) # and memorize ( `\(`, `\)` ) that sequence of non-apostrophe chars (between apostrophes - # "global item should have type") as a memory item 1; # followed by zero or more ('*' after '.') arbitrary chars ('.') till the end of the line. # Second section (`\1`): # If the sequence specified by the first section is found, then replace that sequence (the whole line) # with the memory item 1 (`\1`), ending up in a panic message between the apostrophes. # PANIC_MESSAGE=`. . .`: The output of the command(s) between the backticks ('`') is saved in the # env var PANIC_MESSAGE. # If the failure is not panic-based then extract any ERROR message(s): if [ "$PANIC_MESSAGE" == "" ]; then PANIC_MESSAGE=`cat $STDERR_LOG_FNAME | grep "ERROR"` fi echo "PANIC_MESSAGE: '$PANIC_MESSAGE'" # Output the PANIC_MESSAGE var value to the log # (optional, for workflow failure analysis and sanity check). echo "PANIC_MESSAGE=$PANIC_MESSAGE" >> "$GITHUB_ENV" # Save the PANIC_MESSAGE var in the env, will be used in # the subsequent `run:` and `uses:` steps. # Determine the name of a file containing the input of interest (that triggers the panic/crash): if [ -e $ARTIFACTS_RDPATH/${{ matrix.target_name }}/crash-* ]; then # Panic and Stack Overflow Cases. TO_MINIMIZE_FNAME=crash-*; elif [ -e $ARTIFACTS_RDPATH/${{ matrix.target_name }}/oom-* ]; then # Out-of-Memory Case. TO_MINIMIZE_FNAME=oom-*; else echo -e "File to minimize not found.\nContents of artifacts dir \"$ARTIFACTS_RDPATH/${{ matrix.target_name }}/\":" ls $ARTIFACTS_RDPATH/${{ matrix.target_name }}/ fi if [ "$TO_MINIMIZE_FNAME" != "" ]; then echo "TO_MINIMIZE_FNAME: $TO_MINIMIZE_FNAME" # Minimize the input: ( cargo fuzz tmin --fuzz-dir ./fuzz --release --sanitizer=none --features do_fuzz -r 10000 ${{ matrix.target_name }} $ARTIFACTS_RDPATH/${{ matrix.target_name }}/$TO_MINIMIZE_FNAME 2>&1 ) > \ $TMIN_LOG_FNAME || MINIMIZATION_FAILED=1 # Get the minimized input relative faile path: if [ "$MINIMIZATION_FAILED" == "1" ]; then # Minimization failed, get the latest successful minimized input relative faile path: MINIMIZED_INPUT_RFPATH=` cat $TMIN_LOG_FNAME | grep "CRASH_MIN: minimizing crash input: " | tail -n 1 | sed "s|^.*\($ARTIFACTS_RDPATH/${{ matrix.target_name }}/[^\']*\).*|\1|"` else # Minimization Succeeded, get the reported minimized input relative faile path:: MINIMIZED_INPUT_RFPATH=` cat $TMIN_LOG_FNAME | grep "failed to minimize beyond" | sed "s|.*\($ARTIFACTS_RDPATH/${{ matrix.target_name }}/[^ ]*\).*|\1|" ` fi echo "MINIMIZED_INPUT_RFPATH: $MINIMIZED_INPUT_RFPATH" echo "MINIMIZED_INPUT_RFPATH=$MINIMIZED_INPUT_RFPATH" >> "$GITHUB_ENV" # Extract the minimized input: MINIMIZED_INPUT=`cat $MINIMIZED_INPUT_RFPATH | tr "\n" "\r"` # Display the contents of the minimized input file and replace all the occurrences of '\n' with '\r' # so that the potentially multiline sequence can be "serialized" into the env var, # while preserving the information about the line breaks. else MINIMIZED_INPUT="(Input minimization failed, see the workflow logs and artifacts)" fi echo "MINIMIZED_INPUT: '$MINIMIZED_INPUT'" echo "MINIMIZED_INPUT=$MINIMIZED_INPUT" >> "$GITHUB_ENV" # Get the workflow agent system info: WF_AGENT_SYS_INFO="`uname -a`" echo "WF_AGENT_SYS_INFO: $WF_AGENT_SYS_INFO" echo "WF_AGENT_SYS_INFO=$WF_AGENT_SYS_INFO" >> "$GITHUB_ENV" echo "WF_AGENT_OS=${{ matrix.os }}" >> "$GITHUB_ENV" # Get the branch info: BRANCH_INFO=`git branch | grep '*'` echo "BRANCH_INFO: '$BRANCH_INFO'" echo "BRANCH_INFO=$BRANCH_INFO" >> "$GITHUB_ENV" # Get the commit info: COMMIT_INFO=`git log -1 | tr "\n" "\r"` echo "COMMIT_INFO: '$COMMIT_INFO'" echo "COMMIT_INFO=$COMMIT_INFO" >> "$GITHUB_ENV" # Get the last N bytes of the fuzzing stderr log into the env var # (N is such that the subsequent GitHub issue reporting does not overflow): STDERR_LOG=`tail -c 63488 $STDERR_LOG_FNAME | tr "\n" "\r"` echo "STDERR_LOG: '$STDERR_LOG'" echo "STDERR_LOG=$STDERR_LOG" >> "$GITHUB_ENV" - name: "If Fuzzing Failed: Upload Failure Artifacts" if: failure() uses: actions/upload-artifact@v6 with: name: ${{ matrix.target_name }}-fuzz-failure-artifacts path: | ${{ env.OWNER_RDPATH }}/${{ env.STDERR_LOG_FNAME }} ${{ env.OWNER_RDPATH }}/${{ env.TMIN_LOG_FNAME }} ${{ env.OWNER_RDPATH }}/${{ env.ARTIFACTS_RDPATH }}/${{ matrix.target_name }}/* ${{ env.OWNER_RDPATH }}/${{ env.SEEDS_RDPATH }}/${{ matrix.target_name }}/${{ env.SEEDS_FNAME }} if-no-files-found: error - name: "If Fuzzing Failed: Report GutHub Issue" if: failure() id: create-issue env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} WORKFLOW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | python - <<'PY' import pathlib body_text = """ The fuzz-testing workflow has detected a bug.
Auto-Minimized Fuzzing Input That Triggers the Bug: Click this line. Note: If the input is multi-line then the end-of-line characters '\n' (0x0A) and '\r' (0x0D) may affect the reproducibility of the bug. If you fail to repro the bug with the input shown below then you may want to go to the workflow that reported this GitHub bug, download the artifact, and extract the file with the exact minimized input. ```qs ${{ env.MINIMIZED_INPUT }} ```
The branch/commit the bug has been found in: Click. If the developers fail to repro the bug in the latest main then the branch/commit info below can help them to make sure that they are using the correct way to repro. If the bug is reproducible in the branch/commit below, but not in latest main, then the bug is likely fixed already or is not in the main branch. ```log Branch: ${{ env.BRANCH_INFO }} ${{ env.COMMIT_INFO }} ```
**Other Info** - [**Workflow**](${{ env.WORKFLOW_RUN_URL }}) (contains the run artifacts). - **Workflow Agent System Info:** `${{ env.WF_AGENT_OS }}: ${{ env.WF_AGENT_SYS_INFO }}`. """ pathlib.Path("/tmp/fuzz_issue.md").write_text(body_text) PY url="$(gh issue create \ --title "Fuzz: \"${{ env.PANIC_MESSAGE }}\" (${{ env.WF_AGENT_OS }})" \ --body-file /tmp/fuzz_issue.md \ --label bug \ --assignee swernli)" number="$(gh issue view "$url" --json number --jq .number)" echo "url=$url" >> "$GITHUB_OUTPUT" echo "number=$number" >> "$GITHUB_OUTPUT" - name: "If Fuzzing Failed: Log Issue Info" if: failure() run: | echo "Created issue #${{ steps.create-issue.outputs.number }} ${{ steps.create-issue.outputs.url }}"