| #!/bin/bash |
| # SPDX-License-Identifier: GPL-2.0 |
| # |
| # Copyright 2024 Google LLC |
| # |
| # Author: Lee Jones <lee@kernel.org> |
| # |
| # Usage |
| # cve_review [ v6.7.1..v6.7.2 | filename ] |
| # |
| # * Highlights key words often used to make sane judgements |
| # * Able to accept a Stable Git range or a file in `git log --oneline` format |
| # * Provides an on-going progress report in the form "x of y (z%)" |
| # * Presents commit on a clean terminal without clipping scroll-back |
| # * Tracks progress of reviews and will skip commits already processed |
| # - Progress is tracked in <VULNS>/tmp/cve-review (which Git ignores) |
| # * Clips commits to a little less than the size of your terminal |
| # - This ensures that commit message is always visible |
| # - The remainder of the commit message can be seen by pressing 'M' |
| # |
| # Requires: |
| # * Remember to change the user-specific variables a few lines down |
| # * Expected to be executed from inside a kernel Git repository |
| |
| # set -x # Uncomment to enable debugging |
| set -e # Exit on any error |
| |
| # ------- ACTION REQUIRED ------- |
| # Change these to suit your own setup |
| NAME=lee |
| STABLEREMOTE=stable # Whatever you called your Stable remote |
| |
| if [[ "$(whoami)" == "gregkh" ]]; then |
| NAME=greg |
| fi |
| |
| if [[ "$(whoami)" == "sasha" ]]; then |
| NAME=sasha |
| fi |
| |
| function usage() |
| { |
| echo "Usage: $(basename ${0}) {rangebottom..rangetop}" |
| exit 1 |
| } |
| |
| print_red() |
| { |
| echo -e "\e[01;31m$@\e[0m" |
| } |
| |
| print_blue() |
| { |
| echo -e "\e[01;34m$@\e[0m" |
| } |
| |
| shopt -s extglob |
| while [ $# -gt 0 ]; do |
| case $1 in |
| *+([0-9])\.\.*) |
| RANGE=${1} |
| ;; |
| *+([0-9])\.*+([0-9])\.\.) |
| RANGE=${1} |
| ;; |
| --annotate|-a) |
| ANNOTATE=true |
| ;; |
| --skip-reviewed|-s) |
| SKIPREVIEWED=true |
| ;; |
| *) |
| if [ -s "${1}" ]; then |
| FILE=${1}; |
| shift |
| continue |
| fi |
| |
| if git cat-file -t ${1} > /dev/null 2>&1; then |
| SHAS+=(${1}) |
| shift |
| continue |
| fi |
| |
| print_red "Unrecognised argument: ${1}" |
| usage |
| ;; |
| esac |
| shift |
| done |
| shopt -u extglob |
| |
| if [[ "${ANNOTATE}" == "true" && "${FILE}" == "" ]]; then |
| print_red "When annotating, a file containing commits in --oneline format must be provided" |
| exit 1 |
| fi |
| |
| if [ "${RANGE}" == "" ]; then |
| if [ "${ANNOTATE}" == "true" ]; then |
| print_red "A version must be provided e.g. v6.7.2" |
| exit 1 |
| elif [[ "${FILE}" == "" && "${SHAS[@]}" == "" ]]; then |
| print_red "A range must be provided e.g. v6.7.2..v6.7.3" |
| exit 1 |
| fi |
| fi |
| |
| if [ ! -e .git ] || [ ! -f MAINTAINERS ]; then |
| print_red "Not in a kernel directory" |
| exit 1 |
| fi |
| |
| print_blue "Fetching from ${STABLEREMOTE}" |
| git fetch ${STABLEREMOTE} || print_blue "Unable to fetch - continuing anyway\n" |
| |
| TAG=${RANGE#*..} |
| if [ -s "${FILE}" ]; then |
| |
| if [ "${ANNOTATE}" == "true" ]; then |
| TAG=${TAG}-annotated |
| else |
| TAG="$(basename ${FILE})-fromfile" |
| fi |
| |
| while read line; do |
| # Skip annotations (TODO: Collect these and present them during review) |
| if echo ${line} | grep -Eq "^\s*-"; then |
| continue |
| fi |
| |
| SHAS+=($(echo ${line} | grep -oE "^\s*[a-f0-9]{7,}")) |
| done < ${FILE} |
| elif [ "${RANGE}" != "" ]; then |
| H=$(git log --reverse --format=%h ${RANGE}) |
| for h in ${H}; do |
| SHAS+=($h) |
| done |
| else |
| TAG=${SHAS[0]} |
| fi |
| |
| NOSHAS=$(echo ${SHAS[@]} | wc -w) |
| if [ ${#SHAS[@]} -le 0 ]; then |
| print_red "No commits to review" |
| exit 0 |
| fi |
| |
| SCRIPTDIR=$(dirname ${0}) |
| FINALDIR=${SCRIPTDIR}/../cve/review/proposed |
| PUBLISHEDDIR=${SCRIPTDIR}/../cve/published |
| WORKDIR=${SCRIPTDIR}/../tmp/cve-review |
| PROCESSEDDIR=${WORKDIR}/processed |
| RESULTSDIR=${WORKDIR}/results |
| PROCESSEDFILE=${PROCESSEDDIR}/${TAG} |
| CVEMEFILE=${TAG}-${NAME} |
| CVEME=${RESULTSDIR}/${CVEMEFILE} |
| UPDATEFINALDIR="" |
| |
| mkdir -p ${PROCESSEDDIR} ${RESULTSDIR} |
| |
| print_blue "Reviewing ${NOSHAS} commits" |
| count=0 |
| |
| for h in ${SHAS[@]}; do |
| clipcommitto=$(($(tput lines) - 9)) |
| oneline=$(git --no-pager log ${h} -n1 --format="%h %s") |
| subject=$(echo ${oneline} | cut -d' ' -f 2-) |
| count=$((count + 1)) |
| percentage=$(echo "scale=4; (${count}/${NOSHAS})*100" | bc | awk '{printf "%.2f\n", $0}') |
| alreadyreviewed="" |
| |
| if grep -q -s -F "${oneline}" ${PROCESSEDFILE}; then |
| print_blue "Skipping already processed commit: ${oneline}" |
| continue |
| fi |
| |
| # Shift the screen up without loosing scroll-back |
| for l in $(seq 1 $(tput lines)); do |
| printf '\n'; |
| done |
| clear |
| |
| print_blue "Processing ${TAG} fix: ${count} of ${NOSHAS} (%${percentage})" |
| |
| BADMATCHES="\ |
| attack|\ |
| call[-\s_\n]*trace|\ |
| dead[-\s_\n]*lock|lock[-\s_\n]*up|\ |
| nul[l]*[-\s_\n]*p[a-z]*[-\s_\n]*deref[a-z]*|nul[l]*[-\s_\n]*p[a-z]*|null|deref[a-z]*|\ |
| div[-\s_\na-z]*by[-\s_\n]*zero|divi[-\s_\na-z]*by[-\s_\n]*0|\ |
| double[-\s_\n]*free|\ |
| kernel[-\s_\n]*bug|\ |
| buffer[-\s_\n]*overflow|over[-\s_\n]*run|over[-\s_\n]*flow|\ |
| out[-\s_\n]*of[-\s_\n]*bound[s]*|bound[s]*|\ |
| use[-\s_\n]*after[-\s_\n]*free|use[-\s_\n]*after|after[-\s_\n]*free|\ |
| circular|\ |
| crash[a-z]*|\ |
| denial[-\s_\n]*of[-\s_\n]*service|denial\ |
| dos|\ |
| exploit|\ |
| [-\s_\n]fault|\ |
| kernel[-\s_\n]hang|system[-\s_\n]hang|[-\s_\n]hang[a-z]*|hung|\ |
| info[-\s_\na-z].*leak|leak|\ |
| malicious[a-z]*|\ |
| kernel[-\s_\n]*memory|memory|\ |
| oob|\ |
| oops|\ |
| panic|\ |
| permission|\ |
| possible recursive locking detected|\ |
| reboot|\ |
| refcount|\ |
| system|\ |
| syzkaller|\ |
| syzbot|\ |
| uaf|\ |
| underflow|\ |
| uninitial[a-z]*|\ |
| vuln[a-z]*|\ |
| BUG:|\ |
| KSPP|\ |
| WARN[A-Z]*\ |
| " |
| GOODMATCHES="\ |
| Alex Hung|\ |
| bad unlock balance detected!|\ |
| bogus|\ |
| false[-\s_\n]*alarm|false[-\s_\n]*positive|\ |
| integer|\ |
| locking dependency detected|\ |
| Nested lock was not taken|\ |
| theory[a-z]*|theoretical[a-z]*|\ |
| tools/.* \| .*|\ |
| selftests|\ |
| unmet direct dependencies detected|\ |
| " |
| |
| mainlinesha=$(git --no-pager log -n1 ${h} | grep -i upstream ${f} | grep -oE "[a-f0-9]{40,}") || true |
| commitmsgfile=$(mktemp /tmp/cve-review-XXXXX) |
| commitfile0=$(mktemp /tmp/cve-review-XXXXX) |
| commitfile=$(mktemp /tmp/cve-review-XXXXX) |
| |
| git --no-pager log --stat --color -n1 ${h} > ${commitmsgfile} |
| |
| cat ${commitmsgfile} | GREP_COLORS="ms=01;32" grep -C99999 --color=always -Piz "${GOODMATCHES}" > ${commitfile0} || true |
| if [ ! -s ${commitfile0} ]; then |
| cat ${commitmsgfile} > ${commitfile0} |
| fi |
| |
| cat ${commitfile0} | grep -C99999 --color=always -Piz "${BADMATCHES}" > ${commitfile} || true |
| if [ ! -s ${commitfile} ]; then |
| cat ${commitfile0} > ${commitfile} |
| fi |
| rm -f ${commitmsgfile} |
| |
| if [ "${mainlinesha}" != "" ]; then |
| pubpath=$(grep -rlF "${subject}" ${PUBLISHEDDIR} | grep json | xargs grep -l ${mainlinesha}) || true |
| fi |
| |
| if [ "${pubpath}" != "" ]; then |
| pubfile=$(echo $(basename ${pubpath}) | sed 's/.json//') |
| print_red "\nCVE already published as ${pubfile} -- skipping" |
| |
| sleep 1 |
| echo "${oneline}" >> ${PROCESSEDFILE} |
| |
| echo "$(git --no-pager log ${mainlinesha} -n1 --format="%h %s") [auto: cve already created]" >> ${CVEME} |
| continue |
| fi |
| |
| if grep -rqF "${subject}" ${PROCESSEDDIR}; then |
| filename=$(basename $(grep -lrF "${subject}" ${PROCESSEDDIR} | head -n1)) |
| sha=$(grep -rhF "${subject}" ${PROCESSEDDIR} | cut -d' ' -f1) |
| |
| print_red "\nPotentially already reviewed in\n " |
| |
| echo -n " ${filename}: " |
| echo "$(grep -hF "${subject}" ${PROCESSEDDIR}/*)" |
| |
| if [ "${SKIPREVIEWED}" == "true" ]; then |
| newpatchid=$(git show ${h} | git patch-id | cut -d' ' -f1) |
| oldpatchid=$(git show ${sha} | git patch-id | cut -d' ' -f1) |
| |
| if [ "${newpatchid}" == "${oldpatchid}" ]; then |
| print_blue "\nConfirmed as already reviewed - SKIPPING\n" |
| sleep 1 |
| rm -f ${commitfile} |
| echo "${oneline}" >> ${PROCESSEDFILE} |
| continue |
| else |
| print_blue "\nPatch ID doesn't match - please review for similarity manually" |
| fi |
| fi |
| |
| clipcommitto=$((clipcommitto - 5)) |
| fi |
| echo |
| |
| hits=$(grep -F "${subject}" ${FINALDIR}/* | wc -l) |
| if [ ${hits} -gt 0 ]; then |
| print_red "Positively voted for in:\n" |
| grep -Fl "${subject}" ${FINALDIR}/* | sed 's!'"${FINALDIR}/"'! !' |
| clipcommitto=$((clipcommitto- ${hits} - 3)) |
| echo |
| fi |
| |
| print_blue "Summary:\n" |
| |
| echo >> ${commitfile} |
| git --no-pager log -p --format="" --color=always -n1 ${h} >> ${commitfile} |
| commitlen=$(cat ${commitfile} | wc -l) |
| |
| if [ "${NAME}" == "greg" ]; then |
| bat --paging=always ${commitfile} |
| elif [ ${commitlen} -gt ${clipcommitto} ]; then |
| head -n ${clipcommitto} ${commitfile} |
| print_blue "\nCommit has been clipped, press M to see the remainder" |
| else |
| cat ${commitfile} |
| print_blue "\nCommit not clipped" |
| fi |
| |
| if [ "${ANNOTATE}" != "true" ]; then |
| print_blue "\nShould this commit be assigned a CVE <y/N/q>?" |
| else |
| print_blue "\nPlease annotate <description/q>" |
| fi |
| echo -n "> " |
| read CHOICE |
| |
| if [[ "${CHOICE}" == "m" || "${CHOICE}" == "M" ]]; then |
| echo |
| tail -n $((commitlen - ${clipcommitto})) ${commitfile} |
| |
| if [ "${ANNOTATE}" != "true" ]; then |
| print_blue "\nShould this commit be assigned a CVE <y/N/q>?" |
| else |
| print_blue "\nPlease annotate <description/q>:" |
| fi |
| echo -n "> " |
| read CHOICE |
| fi |
| rm -f ${commitfile} ${commitfile0} |
| |
| if [[ "${CHOICE}" == "q" || "${CHOICE}" == "Q" ]]; then |
| print_blue "\nExiting" |
| exit 0 |
| fi |
| |
| if [[ "${CHOICE}" == "y" || "${CHOICE}" == "Y" || "${CHOICE}" == "]" || "${ANNOTATE}" == "true" ]]; then |
| # If the commit does not contain a Mainline SHA, we'll assume it *is* a Mainline SHA |
| if [ "${mainlinesha}" == "" ]; then |
| mainlinesha="${h}" |
| fi |
| |
| git --no-pager log ${mainlinesha} -n1 --format="%h %s" >> ${CVEME} |
| |
| if [ "${ANNOTATE}" == "true" ]; then |
| echo "- [${NAME}] ${CHOICE}" >> ${CVEME} |
| fi |
| fi |
| |
| echo "${oneline}" >> ${PROCESSEDFILE} |
| done |
| |
| if [ -s ${CVEME} ]; then |
| cat ${CVEME} >> ${FINALDIR}/${CVEMEFILE} |
| fi |