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