From 8ada1bcf8cc7ecf407aa70ec9fe10b11d3034d56 Mon Sep 17 00:00:00 2001 From: "Leslie P. Polzer" Date: Tue, 16 Dec 2025 07:33:38 +0000 Subject: [PATCH] file(LOCK): Avoid truncating existing files Previously the command opened the lock file using fopen with "w" mode, which truncates the file to zero length. This is unsafe because: 1. If the lock file path is a symlink, the target file gets truncated 2. Race conditions between path resolution and file opening can be exploited to truncate arbitrary files An attacker can exploit this by creating a symlink at a predictable lock file location pointing to a critical file (e.g., source files, configuration, or system files). When cmake runs file(LOCK), it follows the symlink and destroys the target file's contents. Fix by changing the file mode from "w" (write/truncate) to "a" (append). This creates the file if it doesn't exist but preserves existing content, preventing data destruction attacks. --- Source/cmFileCommand.cxx | 2 +- .../file/LOCK-symlink-no-truncate-stdout.txt | 4 ++ .../file/LOCK-symlink-no-truncate.cmake | 37 +++++++++++++++++++ Tests/RunCMake/file/RunCMakeTest.cmake | 2 + 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 Tests/RunCMake/file/LOCK-symlink-no-truncate-stdout.txt create mode 100644 Tests/RunCMake/file/LOCK-symlink-no-truncate.cmake diff --git a/Source/cmFileCommand.cxx b/Source/cmFileCommand.cxx index fb53e0e273..65e675f7a1 100644 --- a/Source/cmFileCommand.cxx +++ b/Source/cmFileCommand.cxx @@ -3017,7 +3017,7 @@ bool HandleLockCommand(std::vector const& args, cmSystemTools::SetFatalErrorOccurred(); return false; } - FILE* file = cmsys::SystemTools::Fopen(path, "w"); + FILE* file = cmsys::SystemTools::Fopen(path, "a"); if (!file) { status.GetMakefile().IssueMessage( MessageType::FATAL_ERROR, diff --git a/Tests/RunCMake/file/LOCK-symlink-no-truncate-stdout.txt b/Tests/RunCMake/file/LOCK-symlink-no-truncate-stdout.txt new file mode 100644 index 0000000000..93605d2ee0 --- /dev/null +++ b/Tests/RunCMake/file/LOCK-symlink-no-truncate-stdout.txt @@ -0,0 +1,4 @@ +-- Original content: IMPORTANT DATA THAT MUST NOT BE DESTROYED +-- Lock result: 0 +-- Final content: IMPORTANT DATA THAT MUST NOT BE DESTROYED +-- PASS: Symlink target was not truncated diff --git a/Tests/RunCMake/file/LOCK-symlink-no-truncate.cmake b/Tests/RunCMake/file/LOCK-symlink-no-truncate.cmake new file mode 100644 index 0000000000..10aa428a2b --- /dev/null +++ b/Tests/RunCMake/file/LOCK-symlink-no-truncate.cmake @@ -0,0 +1,37 @@ +# Test that file(LOCK) does not truncate existing files when the lock path +# is a symlink pointing to another file. This is a regression test for a +# data destruction vulnerability (CWE-59). + +set(target_file "${CMAKE_CURRENT_BINARY_DIR}/target_file.txt") +set(lock_symlink "${CMAKE_CURRENT_BINARY_DIR}/lock_symlink") + +# Create a target file with known content +file(WRITE "${target_file}" "IMPORTANT DATA THAT MUST NOT BE DESTROYED") + +# Read original content for comparison +file(READ "${target_file}" original_content) +message(STATUS "Original content: ${original_content}") + +# Create a symlink pointing to the target file +file(CREATE_LINK "${target_file}" "${lock_symlink}" SYMBOLIC) + +# Attempt to lock the symlink - this should NOT truncate the target +file(LOCK "${lock_symlink}" RESULT_VARIABLE lock_result) +message(STATUS "Lock result: ${lock_result}") + +# Release the lock +file(LOCK "${lock_symlink}" RELEASE) + +# Verify the target file still has its content +file(READ "${target_file}" final_content) +message(STATUS "Final content: ${final_content}") + +if(NOT final_content STREQUAL original_content) + message(FATAL_ERROR + "VULNERABILITY: file(LOCK) truncated the symlink target!\n" + "Original: '${original_content}'\n" + "Final: '${final_content}'" + ) +endif() + +message(STATUS "PASS: Symlink target was not truncated") diff --git a/Tests/RunCMake/file/RunCMakeTest.cmake b/Tests/RunCMake/file/RunCMakeTest.cmake index 57c191bf53..7220278266 100644 --- a/Tests/RunCMake/file/RunCMakeTest.cmake +++ b/Tests/RunCMake/file/RunCMakeTest.cmake @@ -91,6 +91,8 @@ if(NOT WIN32 if(NOT CYGWIN) run_cmake(INSTALL-FOLLOW_SYMLINK_CHAIN) endif() + # Test that file(LOCK) doesn't truncate symlink targets (CVE regression test) + run_cmake(LOCK-symlink-no-truncate) endif() run_cmake(REAL_PATH-non-existing)