diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index ec6493e9ed0..0ca11b177b5 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -906,3 +906,21 @@ jobs: run: cargo build --profile=release-small -p uu_rm -p uu_chmod -p uu_chown -p uu_chgrp -p uu_mv -p uu_du - name: Run safe traversal verification run: ./util/check-safe-traversal.sh + + test_toctou: + name: TOCTOU Security Check + runs-on: ubuntu-latest + needs: [ min_version, deps ] + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Install strace + run: sudo apt-get update && sudo apt-get install -y strace + - name: Build utilities for TOCTOU checks + run: cargo build --profile=release-small -p uu_mkfifo -p uu_touch -p uu_head + - name: Run TOCTOU verification + run: ./util/check-toctou.sh diff --git a/util/check-safe-traversal.sh b/util/check-safe-traversal.sh index 79ef1b633ae..871abb0a9e9 100755 --- a/util/check-safe-traversal.sh +++ b/util/check-safe-traversal.sh @@ -191,7 +191,7 @@ if [ "$USE_MULTICALL" -eq 1 ]; then AVAILABLE_UTILS=$($COREUTILS_BIN --list) else AVAILABLE_UTILS="" - for util in rm chmod chown chgrp du mv cp touch head; do + for util in rm chmod chown chgrp du mv cp; do if [ -f "$PROJECT_ROOT/target/${PROFILE}/$util" ]; then AVAILABLE_UTILS="$AVAILABLE_UTILS $util" fi @@ -387,55 +387,6 @@ if echo "$AVAILABLE_UTILS" | grep -q "mv" && [ -d /dev/shm ]; then fi fi -# Test touch - creating a file must use O_CREAT but never O_TRUNC, so that a -# symlink planted in the metadata-check/open race window (#10019) is not -# truncated. This observes the flags directly, which integration tests cannot. -if echo "$AVAILABLE_UTILS" | grep -q "touch"; then - echo "" - echo "Testing touch (create_no_truncate)..." - if [ "$USE_MULTICALL" -eq 1 ]; then - touch_cmd="$COREUTILS_BIN touch" - else - touch_cmd="$PROJECT_ROOT/target/${PROFILE}/touch" - fi - strace -f -e trace=openat -o strace_touch_create.log $touch_cmd test_touch_new 2>/dev/null || true - cat strace_touch_create.log - if ! grep -q 'openat(.*test_touch_new.*O_CREAT' strace_touch_create.log; then - fail_immediately "touch did not create test_touch_new via openat(O_CREAT)" - fi - if grep 'test_touch_new' strace_touch_create.log | grep -q 'O_TRUNC'; then - fail_immediately "touch opened the target with O_TRUNC - vulnerable to truncating a symlink target (#10019)" - fi - echo "✓ touch creates with O_CREAT and without O_TRUNC" -fi - -# Test head - the is-a-directory check must derive from the already-open -# descriptor (fstat/statx on the fd), not from a separate path-based stat -# performed before the open. A path stat followed by an open is a TOCTOU -# window (#11972): the object named by the path can be swapped in between. -if echo "$AVAILABLE_UTILS" | grep -q "head"; then - echo "" - echo "Testing head (fstat_after_open)..." - if [ "$USE_MULTICALL" -eq 1 ]; then - head_cmd="$COREUTILS_BIN head" - else - head_cmd="$PROJECT_ROOT/target/${PROFILE}/head" - fi - echo "headtest" > test_head_file.txt - strace -f -e trace=openat,fstat,newfstatat,statx,stat,lstat \ - -o strace_head_metadata.log $head_cmd -c 4 test_head_file.txt 2>/dev/null || true - cat strace_head_metadata.log - if ! grep -q 'openat(AT_FDCWD, "test_head_file.txt"' strace_head_metadata.log; then - fail_immediately "head did not open test_head_file.txt via openat" - fi - # The filename should appear only in the openat; any stat-family call naming - # the path means head stat'd it before opening - the TOCTOU window. - if grep '"test_head_file.txt"' strace_head_metadata.log | grep -qv 'openat('; then - fail_immediately "head stat'd the path before opening it - TOCTOU window (#11972); metadata must come from the open descriptor" - fi - echo "✓ head reads metadata from the open descriptor, not a path stat" -fi - echo "" echo "✓ Basic safe traversal verification completed" echo "" diff --git a/util/check-toctou.sh b/util/check-toctou.sh index 6fc2094ad45..975bdcf92a0 100755 --- a/util/check-toctou.sh +++ b/util/check-toctou.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# spell-checker:ignore mknod mknodat fchmod fchmodat mkfifoat strace +# spell-checker:ignore mknod mknodat fchmod fchmodat mkfifoat strace newfstatat statx lstat CREAT FDCWD headtest # # TOCTOU (time-of-check / time-of-use) verification. # @@ -61,9 +61,7 @@ if [ "$USE_MULTICALL" -eq 1 ]; then AVAILABLE_UTILS=$($COREUTILS_BIN --list) else AVAILABLE_UTILS="" - # The list intentionally holds a single util today; more will be added. - # shellcheck disable=SC2043 - for util in mkfifo; do + for util in mkfifo touch head; do if [ -f "$PROJECT_ROOT/target/${PROFILE}/$util" ]; then AVAILABLE_UTILS="$AVAILABLE_UTILS $util" fi @@ -105,5 +103,48 @@ if echo "$AVAILABLE_UTILS" | grep -q "mkfifo"; then rm -f test_fifo fi +# Test touch - creating a file must use O_CREAT but never O_TRUNC, so that a +# symlink planted in the metadata-check/open race window (#10019) is not +# truncated. This observes the flags directly, which integration tests cannot. +if echo "$AVAILABLE_UTILS" | grep -q "touch"; then + echo "" + echo "Testing touch (create_no_truncate)..." + touch_cmd=$(util_cmd touch) + strace -f -e trace=openat -o strace_touch_create.log $touch_cmd test_touch_new 2>/dev/null || true + cat strace_touch_create.log + if ! grep -q 'openat(.*test_touch_new.*O_CREAT' strace_touch_create.log; then + fail_immediately "touch did not create test_touch_new via openat(O_CREAT)" + fi + if grep 'test_touch_new' strace_touch_create.log | grep -q 'O_TRUNC'; then + fail_immediately "touch opened the target with O_TRUNC - vulnerable to truncating a symlink target (#10019)" + fi + echo "✓ touch creates with O_CREAT and without O_TRUNC" + rm -f test_touch_new +fi + +# Test head - the is-a-directory check must derive from the already-open +# descriptor (fstat/statx on the fd), not from a separate path-based stat +# performed before the open. A path stat followed by an open is a TOCTOU +# window (#11972): the object named by the path can be swapped in between. +if echo "$AVAILABLE_UTILS" | grep -q "head"; then + echo "" + echo "Testing head (fstat_after_open)..." + head_cmd=$(util_cmd head) + echo "headtest" > test_head_file.txt + strace -f -e trace=openat,fstat,newfstatat,statx,stat,lstat \ + -o strace_head_metadata.log $head_cmd -c 4 test_head_file.txt 2>/dev/null || true + cat strace_head_metadata.log + if ! grep -q 'openat(AT_FDCWD, "test_head_file.txt"' strace_head_metadata.log; then + fail_immediately "head did not open test_head_file.txt via openat" + fi + # The filename should appear only in the openat; any stat-family call naming + # the path means head stat'd it before opening - the TOCTOU window. + if grep '"test_head_file.txt"' strace_head_metadata.log | grep -qv 'openat('; then + fail_immediately "head stat'd the path before opening it - TOCTOU window (#11972); metadata must come from the open descriptor" + fi + echo "✓ head reads metadata from the open descriptor, not a path stat" + rm -f test_head_file.txt +fi + echo "" echo "✓ TOCTOU verification completed"