blob: d55fb73ce080a3b1402771d4054f46f641d5c7d1 [file] [log] [blame]
#!/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