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.
This commit is contained in:
Leslie P. Polzer
2025-12-16 07:33:38 +00:00
committed by Brad King
parent 86a2d3e9a5
commit 8ada1bcf8c
4 changed files with 44 additions and 1 deletions

View File

@@ -3017,7 +3017,7 @@ bool HandleLockCommand(std::vector<std::string> 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,

View File

@@ -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

View File

@@ -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")

View File

@@ -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)