mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Client] Hand off single-instance payloads outside the readyRead signal Opening a cockatrice:// link from outside a running client showed the "Join game ...?" confirmation and then crashed on both answers. The join reached the server first, so the other players saw the user join and immediately disconnect. SingleInstanceManager::handleNewConnection() emitted filesReceived from inside the forwarding socket's readyRead lambda and only disconnected afterwards. A joingame link makes the handler show a modal QMessageBox, which runs a nested event loop. The sending instance exits as soon as it has the acknowledgment, so the socket was disconnected and deleteLater'd inside that loop: it died in the middle of its own readyRead signal, and both this lambda and Qt's signal dispatch kept using the freed memory. Disconnect right after the acknowledgment and emit filesReceived through a queued call, so no handler and no nested event loop ever runs inside the socket's signal. Guarding the disconnect call alone is not enough, since Qt still touches the socket after the lambda returns. Add a regression test that reproduces the nested-event-loop hand-off. * [Tests] Apply cmake-format to the single-instance test wiring * [Client] Defer the single-instance socket teardown past the readyRead frame The forwarded payload is emitted synchronously from inside the forwarding socket's readyRead, which is what lets the modal "Join game?" confirmation appear. A handler can spin a nested event loop, and the sending instance exits as soon as it has the ACK, so the socket is disconnected and deleteLater'd while that loop runs. Control then returns to this frame with the socket already destroyed, and the inline disconnectFromServer() touched freed memory. Queue the teardown instead and guard it with a QPointer taken before the handlers run -- building the guard afterwards is itself a use-after-free, since QPointer dereferences the pointer it is constructed from. The payload still reaches the handlers synchronously, so the confirmation is unchanged. The regression test covers both halves: the handlers must run while the socket is still connected, and the socket must not be torn down until the readyRead frame has returned. * Lint. * [Client] Only delete the single-instance socket outside the readyRead frame The auto-delete on `disconnected` was the remaining use-after-free. A handler may spin a nested event loop (the modal "Join game?" box), the sending instance exits as soon as it has the ACK, and so the peer closes while that loop runs. deleteLater() issued from inside such a loop is not deferred to the outer loop: Qt frees the object as soon as the nested loop unwinds, which is still inside this readyRead emission, so Qt's own signal dispatch and the handler lambda both keep using a freed socket. When the notification happens to arrive late the bug hides, which is why this only reproduced on some platforms. Drop the auto-delete and let the queued teardown own the socket's lifetime: it disconnects and then deletes, once the frame has unwound. The QPointer guard was necessary but not sufficient -- it stopped us dereferencing a dead pointer, not the socket being freed underneath the frame. The test waits on the socket's destroyed signal instead of polling, so the teardown assertion does not depend on platform event timing. * [Client] Do not queue the single-instance socket teardown The previous commit moved the deleteLater() off the disconnected signal and into a queued invocation, believing that "queued" meant "after the readyRead frame returns". It does not. A queued call is delivered by whichever event loop is running, and the modal "Join game?" box runs a nested one -- so the teardown was still executed inside the readyRead emission, which is exactly the use-after-free it claimed to prevent. Queueing was never necessary. Because nothing connects deleted/close to the socket's disconnected signal any more, no nested loop can free it while the handlers run, and emit filesReceived() is synchronous: once it returns, the frame has unwound and the socket can be closed and deleted inline. The QPointer stays, covering the one case that remains -- the primary shutting down while the confirmation box is open, since QLocalServer destroys its pending connections. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
185 lines
8.7 KiB
CMake
185 lines
8.7 KiB
CMake
# NOTE: Qt modules for tests are defined centrally in cmake/FindQtRuntime.cmake (the _TEST_NEEDED variable).
|
|
# If a new test needs additional Qt modules, add them there — not in individual test CMakeLists.txt files.
|
|
enable_testing()
|
|
|
|
add_test(NAME dummy_test COMMAND dummy_test)
|
|
add_test(NAME expression_test COMMAND expression_test)
|
|
add_test(NAME clamped_arithmetic_test COMMAND clamped_arithmetic_test)
|
|
add_test(NAME test_age_formatting COMMAND test_age_formatting)
|
|
add_test(NAME password_hash_test COMMAND password_hash_test)
|
|
add_test(NAME playmat_resolver_test COMMAND playmat_resolver_test)
|
|
add_test(NAME server_card_counter_test COMMAND server_card_counter_test)
|
|
add_test(NAME server_counter_test COMMAND server_counter_test)
|
|
add_test(NAME server_rate_limiter_test COMMAND server_rate_limiter_test)
|
|
add_test(NAME server_developer_role_test COMMAND server_developer_role_test)
|
|
add_test(NAME server_game_join_test COMMAND server_game_join_test)
|
|
add_test(NAME warning_categories_test COMMAND warning_categories_test)
|
|
add_test(NAME report_categories_test COMMAND report_categories_test)
|
|
add_test(NAME lag_monitor_test COMMAND lag_monitor_test)
|
|
add_test(NAME latency_tracker_test COMMAND latency_tracker_test)
|
|
add_test(NAME metrics_registry_test COMMAND metrics_registry_test)
|
|
add_test(NAME loader_local_matching_test COMMAND loader_local_matching_test)
|
|
|
|
add_test(NAME deck_hash_performance_test COMMAND deck_hash_performance_test)
|
|
set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 15)
|
|
|
|
# Find GTest
|
|
|
|
add_executable(dummy_test dummy_test.cpp)
|
|
add_executable(expression_test expression_test.cpp)
|
|
add_executable(clamped_arithmetic_test clamped_arithmetic_test.cpp)
|
|
add_executable(test_age_formatting test_age_formatting.cpp)
|
|
add_executable(password_hash_test password_hash_test.cpp)
|
|
add_executable(playmat_resolver_test playmat_resolver_test.cpp)
|
|
add_executable(deck_hash_performance_test deck_hash_performance_test.cpp)
|
|
add_executable(server_card_counter_test server_card_counter_test.cpp)
|
|
add_executable(server_counter_test server_counter_test.cpp)
|
|
add_executable(server_rate_limiter_test server_rate_limiter_test.cpp)
|
|
add_executable(server_developer_role_test server_developer_role_test.cpp)
|
|
add_executable(server_game_join_test server_game_join_test.cpp)
|
|
add_executable(warning_categories_test warning_categories_test.cpp)
|
|
add_executable(report_categories_test report_categories_test.cpp)
|
|
add_executable(lag_monitor_test ${CMAKE_SOURCE_DIR}/cockatrice/src/client/lag_monitor.cpp lag_monitor_test.cpp)
|
|
target_include_directories(lag_monitor_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
|
add_executable(latency_tracker_test latency_tracker_test.cpp)
|
|
add_executable(metrics_registry_test ../servatrice/src/metrics_registry.cpp metrics_registry_test.cpp)
|
|
add_executable(
|
|
loader_local_matching_test
|
|
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
|
|
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/cache_settings.cpp
|
|
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/card_counter_settings.cpp
|
|
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/shortcuts_settings.cpp
|
|
${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp
|
|
${VERSION_STRING_CPP}
|
|
loader_local_matching_test.cpp
|
|
)
|
|
target_include_directories(loader_local_matching_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
|
|
|
# Guard must match the condition for src/single_instance_manager.cpp in cockatrice/CMakeLists.txt.
|
|
if(WITH_CLIENT)
|
|
add_executable(
|
|
single_instance_manager_test ${CMAKE_SOURCE_DIR}/cockatrice/src/single_instance_manager.cpp
|
|
single_instance_manager_test.cpp
|
|
)
|
|
target_include_directories(single_instance_manager_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
|
|
|
add_test(NAME single_instance_manager_test COMMAND single_instance_manager_test)
|
|
endif()
|
|
|
|
find_package(GTest)
|
|
|
|
if(NOT GTEST_FOUND)
|
|
if(NOT EXISTS "${CMAKE_BINARY_DIR}/gtest-build")
|
|
message(STATUS "Downloading googletest")
|
|
configure_file(
|
|
"${CMAKE_SOURCE_DIR}/cmake/gtest-CMakeLists.txt.in" "${CMAKE_BINARY_DIR}/gtest-download/CMakeLists.txt"
|
|
)
|
|
execute_process(
|
|
COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" . WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/gtest-download
|
|
)
|
|
execute_process(COMMAND ${CMAKE_COMMAND} --build . WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/gtest-download)
|
|
else()
|
|
message(STATUS "GoogleTest directory exists")
|
|
endif()
|
|
|
|
# Add gtest directly to our build
|
|
add_subdirectory(${CMAKE_BINARY_DIR}/gtest-src ${CMAKE_BINARY_DIR}/gtest-build EXCLUDE_FROM_ALL)
|
|
|
|
# Add the gtest include directory, since gtest
|
|
# doesn't add that dependency to its gtest target
|
|
target_include_directories(gtest INTERFACE "$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/gtest-src/include>")
|
|
|
|
set(GTEST_INCLUDE_DIRS "${CMAKE_BINARY_DIR}/gtest-src/include")
|
|
set(GTEST_BOTH_LIBRARIES gtest)
|
|
add_dependencies(dummy_test gtest)
|
|
add_dependencies(expression_test gtest)
|
|
add_dependencies(clamped_arithmetic_test gtest)
|
|
add_dependencies(test_age_formatting gtest)
|
|
add_dependencies(password_hash_test gtest)
|
|
add_dependencies(playmat_resolver_test gtest)
|
|
add_dependencies(deck_hash_performance_test gtest)
|
|
add_dependencies(server_card_counter_test gtest)
|
|
add_dependencies(server_counter_test gtest)
|
|
add_dependencies(server_rate_limiter_test gtest)
|
|
add_dependencies(server_developer_role_test gtest)
|
|
add_dependencies(server_game_join_test gtest)
|
|
add_dependencies(warning_categories_test gtest)
|
|
add_dependencies(report_categories_test gtest)
|
|
add_dependencies(lag_monitor_test gtest)
|
|
add_dependencies(latency_tracker_test gtest)
|
|
add_dependencies(metrics_registry_test gtest)
|
|
add_dependencies(loader_local_matching_test gtest)
|
|
add_dependencies(single_instance_manager_test gtest)
|
|
endif()
|
|
|
|
include_directories(${GTEST_INCLUDE_DIRS})
|
|
target_link_libraries(dummy_test Threads::Threads ${GTEST_BOTH_LIBRARIES})
|
|
target_link_libraries(expression_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES})
|
|
target_link_libraries(
|
|
clamped_arithmetic_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
|
)
|
|
target_link_libraries(
|
|
test_age_formatting libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
|
)
|
|
target_link_libraries(
|
|
password_hash_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
|
)
|
|
target_link_libraries(
|
|
playmat_resolver_test libcockatrice_deck_list libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
|
${TEST_QT_MODULES}
|
|
)
|
|
target_link_libraries(
|
|
deck_hash_performance_test libcockatrice_deck_list libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
|
${TEST_QT_MODULES}
|
|
)
|
|
target_link_libraries(
|
|
server_card_counter_test libcockatrice_network Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
|
)
|
|
target_link_libraries(
|
|
server_counter_test libcockatrice_network Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
|
)
|
|
target_link_libraries(
|
|
server_rate_limiter_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
|
)
|
|
target_link_libraries(
|
|
server_developer_role_test libcockatrice_network libcockatrice_rng Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
|
${TEST_QT_MODULES}
|
|
)
|
|
target_link_libraries(
|
|
server_game_join_test libcockatrice_network_server_remote libcockatrice_rng Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
|
${TEST_QT_MODULES}
|
|
)
|
|
target_link_libraries(
|
|
warning_categories_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
|
)
|
|
target_link_libraries(
|
|
report_categories_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
|
)
|
|
target_link_libraries(lag_monitor_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES})
|
|
target_link_libraries(
|
|
latency_tracker_test libcockatrice_network Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
|
)
|
|
target_include_directories(metrics_registry_test PRIVATE ${CMAKE_SOURCE_DIR}/servatrice/src)
|
|
target_link_libraries(metrics_registry_test ${TEST_QT_MODULES} Threads::Threads ${GTEST_BOTH_LIBRARIES})
|
|
target_link_libraries(
|
|
loader_local_matching_test libcockatrice_settings Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
|
)
|
|
|
|
if(WITH_CLIENT)
|
|
target_link_libraries(single_instance_manager_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES})
|
|
endif()
|
|
|
|
add_subdirectory(card_zone_algorithms)
|
|
add_subdirectory(carddatabase)
|
|
add_subdirectory(deck_list_model)
|
|
add_subdirectory(deck_list_zones)
|
|
add_subdirectory(loading_from_clipboard)
|
|
add_subdirectory(movecard_tests)
|
|
add_subdirectory(oracle)
|
|
add_subdirectory(settings)
|
|
|
|
# picture_loader_benchmark links libcockatrice_settings, which only exists when
|
|
# a client/UI-capable target is being built.
|
|
if(WITH_ORACLE OR WITH_CLIENT)
|
|
add_subdirectory(picture_loader_benchmark)
|
|
endif()
|