Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/CICD.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
51 changes: 1 addition & 50 deletions util/check-safe-traversal.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ""
Expand Down
49 changes: 45 additions & 4 deletions util/check-toctou.sh
Original file line number Diff line number Diff line change
@@ -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.
#
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Loading