microsoft/qdk
Publicmirrored fromhttps://github.com/microsoft/qdkAvailable
.github/workflows/fuzz.yml
224lines · modecode
| 1 | name: fuzz |
| 2 | run-name: Fuzz |
| 3 | env: |
| 4 | OWNER_RDPATH: ./source # Rel path to the dir that contains the fuzzing infra (contains "fuzz" dir). |
| 5 | DURATION_SEC: 7200 # Fuzzing run duration in seconds. |
| 6 | STDERR_LOG_FNAME: fuzz.stderr.log # File name to redirect the fuzzing run's stderr to. |
| 7 | TMIN_LOG_FNAME: fuzz.tmin.log # File name to redirect the fuzzing input minimization log to. |
| 8 | GH_ISSUE_TEMPLATE_RFPATH: .github/ISSUE_TEMPLATE/fuzz_bug_report.md |
| 9 | # GitHub issue template rel file path. |
| 10 | ARTIFACTS_RDPATH: fuzz/artifacts # Fuzzing artifacts rel dir path. |
| 11 | SEEDS_RDPATH: fuzz/seed_inputs # Fuzzing seed inputs rel dir path. |
| 12 | SEEDS_FNAME: list.txt # Fuzzing seed inputs list file name. |
| 13 | on: |
| 14 | workflow_dispatch: # Manual runs. |
| 15 | push: |
| 16 | branches: |
| 17 | - main # Development runs against main branch. |
| 18 | paths: |
| 19 | - 'source/compiler/**' # Run if the compiler was changed. |
| 20 | - 'source/fuzz/**' # Run if the fuzzing infra was changed. |
| 21 | - '.github/ISSUE_TEMPLATE/fuzz_bug_report.md' |
| 22 | # Run if the GitHub issue template was changed. |
| 23 | - '.github/workflows/fuzz.yml' # Run if the workflow itself was changed. |
| 24 | - '!source/compiler/qsc_eval/**' # Exclude the qsc_eval dir. |
| 25 | - '!source/compiler/qsc_codegen/**' # Exclude the qsc_codegen dir. |
| 26 | |
| 27 | jobs: |
| 28 | fuzz: |
| 29 | name: Fuzzing |
| 30 | strategy: |
| 31 | fail-fast: false |
| 32 | matrix: |
| 33 | os: [ubuntu-latest] # Fuzzing is not supported on Win. The macos is temporarily removed |
| 34 | # because of low availability. |
| 35 | target_name: [qsharp, qasm] |
| 36 | |
| 37 | runs-on: ${{ matrix.os }} |
| 38 | permissions: |
| 39 | issues: write |
| 40 | steps: |
| 41 | - name: Install and Configure Tools |
| 42 | run: | |
| 43 | rustup install nightly # Install nightly toolchain. |
| 44 | rustup default nightly # Make nightly toolchain default. |
| 45 | cargo install cargo-fuzz # Install cargo-fuzz (fuzzing tool). |
| 46 | |
| 47 | - name: Checkout the Repo |
| 48 | uses: actions/checkout@v3 |
| 49 | with: |
| 50 | submodules: "true" |
| 51 | |
| 52 | - name: Gather the Seed Inputs |
| 53 | if: matrix.target_name == 'qsharp' |
| 54 | working-directory: ${{ env.OWNER_RDPATH }} |
| 55 | run: | |
| 56 | # Clone the submodules of QDK: |
| 57 | REPOS="Quantum Quantum-NC QuantumKatas QuantumLibraries iqsharp qdk-python qsharp-compiler qsharp-runtime" |
| 58 | for REPO in $REPOS ; do |
| 59 | git clone --depth 1 --single-branch --no-tags --recurse-submodules --shallow-submodules --jobs 4 \ |
| 60 | https://github.com/microsoft/$REPO.git $SEEDS_RDPATH/${{ matrix.target_name }}/$REPO |
| 61 | done |
| 62 | |
| 63 | # Build a comma-separated list of all the .qs files in $SEEDS_FNAME file: |
| 64 | find $SEEDS_RDPATH/${{ matrix.target_name }} -name "*.qs" | tr "\n" "," > \ |
| 65 | $SEEDS_RDPATH/${{ matrix.target_name }}/$SEEDS_FNAME |
| 66 | |
| 67 | - name: Gather the Seed Inputs (qasm) |
| 68 | if: matrix.target_name == 'qasm' |
| 69 | working-directory: ${{ env.OWNER_RDPATH }} |
| 70 | run: | |
| 71 | # Clone openqasm repo for samples: |
| 72 | git clone --depth 1 --single-branch --no-tags --recurse-submodules --shallow-submodules --jobs 4 \ |
| 73 | https://github.com/openqasm/openqasm.git $SEEDS_RDPATH/${{ matrix.target_name }}/openqasm |
| 74 | |
| 75 | |
| 76 | # Build a comma-separated list of all the .qasm and .inc files in $SEEDS_FNAME file: |
| 77 | find $SEEDS_RDPATH/${{ matrix.target_name }} -name "*.qasm" | tr "\n" "," > \ |
| 78 | $SEEDS_RDPATH/${{ matrix.target_name }}/$SEEDS_FNAME |
| 79 | find $SEEDS_RDPATH/${{ matrix.target_name }} -name "*.inc" | tr "\n" "," > \ |
| 80 | $SEEDS_RDPATH/${{ matrix.target_name }}/$SEEDS_FNAME |
| 81 | |
| 82 | - name: Build and Run the Fuzz Target |
| 83 | working-directory: ${{ env.OWNER_RDPATH }} |
| 84 | run: | |
| 85 | cargo fuzz build --fuzz-dir ./fuzz --release --sanitizer=none --features do_fuzz ${{ matrix.target_name }} # Build the fuzz target. |
| 86 | |
| 87 | # Run fuzzing for specified number of seconds and redirect the `stderr` to a file |
| 88 | # whose name is specified by the STDERR_LOG_FNAME env var: |
| 89 | RUST_BACKTRACE=1 cargo fuzz run --fuzz-dir ./fuzz --release --sanitizer=none --features do_fuzz ${{ matrix.target_name }} -- \ |
| 90 | -seed_inputs=@$SEEDS_RDPATH/${{ matrix.target_name }}/$SEEDS_FNAME \ |
| 91 | -max_total_time=$DURATION_SEC \ |
| 92 | -rss_limit_mb=4096 \ |
| 93 | -max_len=20000 \ |
| 94 | 2>$STDERR_LOG_FNAME |
| 95 | # The `-rss_limit_mb` and `-max_len` work around running out of memory. |
| 96 | |
| 97 | - name: "If Fuzzing Failed: Collect Failure Info" |
| 98 | if: failure() |
| 99 | working-directory: ${{ env.OWNER_RDPATH }} |
| 100 | run: | |
| 101 | # Extract from stderr log the panic message: |
| 102 | PANIC_MESSAGE=`cat $STDERR_LOG_FNAME | |
| 103 | grep "panicked at" | sed "s|thread '<unnamed>' panicked at '\([^']*\).*|\1|"` |
| 104 | # Explanation: |
| 105 | # `cat $STDERR_LOG_FNAME |`: Display the contents of the stderr log file and pass the contents |
| 106 | # to the next command. |
| 107 | # `grep "panicked at" |`: Filter out (drop) all the lines except the ones containing "panicked at", |
| 108 | # the script expects that there is only one such line, pass that line to the next command. Line example: |
| 109 | # thread '<unnamed>' panicked at 'global item should have type', . . ./compiler/qsc_frontend/src/typeck/rules.rs:300:26 |
| 110 | # `sed "s|thread '<unnamed>' panicked at '\([^']*\).*|\1|"`: `sed` - stream editor. |
| 111 | # `s` after quote: search command. After `s` there are two sections, each between a pair of '|'. |
| 112 | # First section: |
| 113 | # In the incoming stream search for a sequence starting with "thread '<unnamed>' panicked at '" |
| 114 | # (sequence from the beginning of the line until after the apostrophe where the panic message starts), |
| 115 | # followed by zero or more ('*' after ']') non-apostrophe chars (`[^']`) |
| 116 | # and memorize ( `\(`, `\)` ) that sequence of non-apostrophe chars (between apostrophes - |
| 117 | # "global item should have type") as a memory item 1; |
| 118 | # followed by zero or more ('*' after '.') arbitrary chars ('.') till the end of the line. |
| 119 | # Second section (`\1`): |
| 120 | # If the sequence specified by the first section is found, then replace that sequence (the whole line) |
| 121 | # with the memory item 1 (`\1`), ending up in a panic message between the apostrophes. |
| 122 | # PANIC_MESSAGE=`. . .`: The output of the command(s) between the backticks ('`') is saved in the |
| 123 | # env var PANIC_MESSAGE. |
| 124 | # If the failure is not panic-based then extract any ERROR message(s): |
| 125 | if [ "$PANIC_MESSAGE" == "" ]; then |
| 126 | PANIC_MESSAGE=`cat $STDERR_LOG_FNAME | grep "ERROR"` |
| 127 | fi |
| 128 | echo "PANIC_MESSAGE: '$PANIC_MESSAGE'" # Output the PANIC_MESSAGE var value to the log |
| 129 | # (optional, for workflow failure analysis and sanity check). |
| 130 | echo "PANIC_MESSAGE=$PANIC_MESSAGE" >> "$GITHUB_ENV" # Save the PANIC_MESSAGE var in the env, will be used in |
| 131 | # the subsequent `run:` and `uses:` steps. |
| 132 | |
| 133 | # Determine the name of a file containing the input of interest (that triggers the panic/crash): |
| 134 | if [ -e $ARTIFACTS_RDPATH/${{ matrix.target_name }}/crash-* ]; then # Panic and Stack Overflow Cases. |
| 135 | TO_MINIMIZE_FNAME=crash-*; |
| 136 | elif [ -e $ARTIFACTS_RDPATH/${{ matrix.target_name }}/oom-* ]; then # Out-of-Memory Case. |
| 137 | TO_MINIMIZE_FNAME=oom-*; |
| 138 | else |
| 139 | echo -e "File to minimize not found.\nContents of artifacts dir \"$ARTIFACTS_RDPATH/${{ matrix.target_name }}/\":" |
| 140 | ls $ARTIFACTS_RDPATH/${{ matrix.target_name }}/ |
| 141 | fi |
| 142 | |
| 143 | if [ "$TO_MINIMIZE_FNAME" != "" ]; then |
| 144 | echo "TO_MINIMIZE_FNAME: $TO_MINIMIZE_FNAME" |
| 145 | |
| 146 | # Minimize the input: |
| 147 | ( cargo fuzz --fuzz-dir ./fuzz tmin --release --sanitizer=none --features do_fuzz -r 10000 ${{ matrix.target_name }} $ARTIFACTS_RDPATH/${{ matrix.target_name }}/$TO_MINIMIZE_FNAME 2>&1 ) > \ |
| 148 | $TMIN_LOG_FNAME || MINIMIZATION_FAILED=1 |
| 149 | |
| 150 | # Get the minimized input relative faile path: |
| 151 | if [ "$MINIMIZATION_FAILED" == "1" ]; then |
| 152 | # Minimization failed, get the latest successful minimized input relative faile path: |
| 153 | MINIMIZED_INPUT_RFPATH=` |
| 154 | cat $TMIN_LOG_FNAME | grep "CRASH_MIN: minimizing crash input: " | tail -n 1 | |
| 155 | sed "s|^.*\($ARTIFACTS_RDPATH/${{ matrix.target_name }}/[^\']*\).*|\1|"` |
| 156 | else |
| 157 | # Minimization Succeeded, get the reported minimized input relative faile path:: |
| 158 | MINIMIZED_INPUT_RFPATH=` |
| 159 | cat $TMIN_LOG_FNAME | grep "failed to minimize beyond" | |
| 160 | sed "s|.*\($ARTIFACTS_RDPATH/${{ matrix.target_name }}/[^ ]*\).*|\1|" ` |
| 161 | fi |
| 162 | echo "MINIMIZED_INPUT_RFPATH: $MINIMIZED_INPUT_RFPATH" |
| 163 | echo "MINIMIZED_INPUT_RFPATH=$MINIMIZED_INPUT_RFPATH" >> "$GITHUB_ENV" |
| 164 | |
| 165 | # Extract the minimized input: |
| 166 | MINIMIZED_INPUT=`cat $MINIMIZED_INPUT_RFPATH | tr "\n" "\r"` |
| 167 | # Display the contents of the minimized input file and replace all the occurrences of '\n' with '\r' |
| 168 | # so that the potentially multiline sequence can be "serialized" into the env var, |
| 169 | # while preserving the information about the line breaks. |
| 170 | else |
| 171 | MINIMIZED_INPUT="(Input minimization failed, see the workflow logs and artifacts)" |
| 172 | fi |
| 173 | echo "MINIMIZED_INPUT: '$MINIMIZED_INPUT'" |
| 174 | echo "MINIMIZED_INPUT=$MINIMIZED_INPUT" >> "$GITHUB_ENV" |
| 175 | |
| 176 | # Get the workflow agent system info: |
| 177 | WF_AGENT_SYS_INFO="`uname -a`" |
| 178 | echo "WF_AGENT_SYS_INFO: $WF_AGENT_SYS_INFO" |
| 179 | echo "WF_AGENT_SYS_INFO=$WF_AGENT_SYS_INFO" >> "$GITHUB_ENV" |
| 180 | echo "WF_AGENT_OS=${{ matrix.os }}" >> "$GITHUB_ENV" |
| 181 | |
| 182 | # Get the branch info: |
| 183 | BRANCH_INFO=`git branch | grep '*'` |
| 184 | echo "BRANCH_INFO: '$BRANCH_INFO'" |
| 185 | echo "BRANCH_INFO=$BRANCH_INFO" >> "$GITHUB_ENV" |
| 186 | |
| 187 | # Get the commit info: |
| 188 | COMMIT_INFO=`git log -1 | tr "\n" "\r"` |
| 189 | echo "COMMIT_INFO: '$COMMIT_INFO'" |
| 190 | echo "COMMIT_INFO=$COMMIT_INFO" >> "$GITHUB_ENV" |
| 191 | |
| 192 | # Get the last N bytes of the fuzzing stderr log into the env var |
| 193 | # (N is such that the subsequent GitHub issue reporting does not overflow): |
| 194 | STDERR_LOG=`tail -c 63488 $STDERR_LOG_FNAME | tr "\n" "\r"` |
| 195 | echo "STDERR_LOG: '$STDERR_LOG'" |
| 196 | echo "STDERR_LOG=$STDERR_LOG" >> "$GITHUB_ENV" |
| 197 | |
| 198 | - name: "If Fuzzing Failed: Upload Failure Artifacts" |
| 199 | if: failure() |
| 200 | uses: actions/upload-artifact@v4 |
| 201 | with: |
| 202 | name: ${{ matrix.target_name }}-fuzz-failure-artifacts |
| 203 | path: | |
| 204 | ${{ env.OWNER_RDPATH }}/${{ env.STDERR_LOG_FNAME }} |
| 205 | ${{ env.OWNER_RDPATH }}/${{ env.TMIN_LOG_FNAME }} |
| 206 | ${{ env.OWNER_RDPATH }}/${{ env.ARTIFACTS_RDPATH }}/${{ matrix.target_name }}/* |
| 207 | ${{ env.OWNER_RDPATH }}/${{ env.SEEDS_RDPATH }}/${{ matrix.target_name }}/${{ env.SEEDS_FNAME }} |
| 208 | if-no-files-found: error |
| 209 | |
| 210 | - name: "If Fuzzing Failed: Report GutHub Issue" |
| 211 | if: failure() |
| 212 | uses: JasonEtco/create-an-issue@v2 |
| 213 | env: |
| 214 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 215 | WORKFLOW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} |
| 216 | with: |
| 217 | filename: ${{ env.GH_ISSUE_TEMPLATE_RFPATH }} |
| 218 | # This issue template file uses a number of env vars collected above. |
| 219 | id: create-issue |
| 220 | |
| 221 | - name: "If Fuzzing Failed: Log Issue Info" |
| 222 | if: failure() |
| 223 | run: | |
| 224 | echo "Created issue #${{ steps.create-issue.outputs.number }} ${{ steps.create-issue.outputs.url }}" |
| 225 | |