Skip to content

Commit

Permalink
Add a CI build that checks for unpermitted libc functions.
Browse files Browse the repository at this point in the history
After some discussion with Ray, we settled on certain libc functions that we're
ok using in note-c. Everything else from libc should be excluded. This commit
adds a CMake option to build the note-c library without libc. It also tells the
linker to generate errors for undefined references. The result is that we can
run this build to see what libc functions we're using in note-c. A new Bash
script, check_libc_dependencies.sh, processes the output of this build. If any
non-permitted functions are found in the undefined reference errors, the script
fails. This protects us against the introduction of non-permitted libc
functions. A new job in ci.yml runs this script on every PR and push to master.
  • Loading branch information
haydenroche5 committed Dec 11, 2023
1 parent deed538 commit 42f86cb
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 0 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,25 @@ jobs:
run: |
docker run --rm --volume $(pwd):/note-c/ --workdir /note-c/ --entrypoint ./scripts/build_docs.sh ghcr.io/blues/note_c_ci:latest
check_libc_dependencies:
runs-on: ubuntu-latest
if: ${{ always() }}
needs: [build_ci_docker_image]

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Load CI Docker image
# Only load the Docker image artifact if build_ci_docker_image actually
# ran (e.g. it wasn't skipped and was successful).
if: ${{ needs.build_ci_docker_image.result == 'success' }}
uses: ./.github/actions/load-ci-image

- name: Check note-c's libc dependencies
run: |
docker run --rm --volume $(pwd):/note-c/ --workdir /note-c/ --entrypoint ./scripts/check_libc_dependencies.sh ghcr.io/blues/note_c_ci:latest
run_unit_tests:
runs-on: ubuntu-latest
if: ${{ always() }}
Expand Down
11 changes: 11 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ option(NOTE_C_BUILD_TESTS "Build tests." OFF)
option(NOTE_C_COVERAGE "Compile for test NOTE_C_COVERAGE reporting." OFF)
option(NOTE_C_MEM_CHECK "Run tests with Valgrind." OFF)
option(NOTE_C_BUILD_DOCS "Build docs." OFF)
option(NOTE_C_NO_LIBC "Build the library without linking against libc, generating errors for any undefined symbols." OFF)

set(NOTE_C_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR})
add_library(note_c SHARED)
Expand Down Expand Up @@ -56,6 +57,16 @@ target_include_directories(
PUBLIC ${NOTE_C_SRC_DIR}
)

if(NOTE_C_NO_LIBC)
target_link_options(
note_c
PRIVATE
-nostdlib
-nodefaultlibs
LINKER:--no-undefined
)
endif()

if(NOTE_C_BUILD_TESTS)
# Including CTest here rather than in test/CMakeLists.txt allows us to run
# ctest from the root build directory (e.g. build/ instead of build/test/).
Expand Down
96 changes: 96 additions & 0 deletions scripts/check_libc_dependencies.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/bin/bash

SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
ROOT_SRC_DIR="$SCRIPT_DIR/.."

if [[ ! -f "$ROOT_SRC_DIR/CMakeLists.txt" ]]; then
echo "Failed to find note-c root directory. (did the location of check_libc_dependencies.sh change?)"
exit 1
fi

pushd $ROOT_SRC_DIR $@ > /dev/null

CMAKE_OPTIONS="-DNOTE_C_NO_LIBC=1"

cmake -B build/ $CMAKE_OPTIONS
if [[ $? -ne 0 ]]; then
echo "Failed to run CMake."
popd $@ > /dev/null
exit 1
fi

PERMITTED_FNS=(
# The mem* functions are ok.
"memchr"
"memcmp"
"memcpy"
"memmove"
"memset"
# These string functions are ok.
"strchr"
"strcmp"
"strlen"
"strncmp"
"strstr"
# TODO: The only explicit usage of strtod is in the cJSON code if
# CJSON_NO_CLIB is NOT true. It's true by default, so we need to figure out
# why strtod is still being brought in.
"strtod"
# strtol comes from us using atoi in NoteGetEnvInt.
"strtol"
# Used by NotePrintf.
"vsnprintf"
)

# Function to check if an element is in an array.
element_in_array() {
local element="$1"
local array=("${@:2}")

for item in "${array[@]}"; do
if [ "$item" == "$element" ]; then
return 0 # Element found in array
fi
done
return 1 # Element not found in array
}

BUILD_OUTPUT=$(cmake --build build/ -j 2>&1)
if [[ $? -ne 0 ]]; then
# Iterate over the lines from the build output to get all the undefined
# references.
UNDEF_REFS=()
while IFS= read -r LINE; do
PATTERN="undefined reference to \`(.*)'"
if [[ $LINE =~ $PATTERN ]]; then
UNDEF_REFS+=("${BASH_REMATCH[1]}")
fi
done <<< "$BUILD_OUTPUT"

# Remove duplicates
UNDEF_REFS=($(printf "%s\n" "${UNDEF_REFS[@]}" | sort -u))

# Check if each function that caused an undefined reference error is
# permitted.
FAIL=0
for UNDEF_REF in "${UNDEF_REFS[@]}"; do
if element_in_array "$UNDEF_REF" "${PERMITTED_FNS[@]}"; then
echo "$UNDEF_REF is permitted."
else
echo "$UNDEF_REF is NOT permitted"
FAIL=1
fi
done

if [ "$FAIL" -eq 1 ]; then
echo "Unpermitted libc functions found."
popd $@ > /dev/null
exit 1
fi
else
echo "Build unexpectedly succeeded. The build should fail because certain permitted libc functions shouldn't be found when linking."
popd $@ > /dev/null
exit 1
fi

popd $@ > /dev/null

0 comments on commit 42f86cb

Please sign in to comment.