From 2205b5ef1ae424ce24652e811c8737205dd88568 Mon Sep 17 00:00:00 2001 From: Chris Burr Date: Wed, 22 Apr 2026 15:53:37 +0200 Subject: [PATCH] [core] Use libuuid's uuid_generate_time in TUUID when available The existing TUUID v1 generator seeds its per-thread 14-bit clock sequence from `gSystem->Now() + GetPid()`. When many processes start within the same second on a single host, the seeds are nearly identical and all processes share the same MAC-derived node identifier, so UUIDs produced in the same 100ns tick collide (root-project/root#22015). libuuid's uuid_generate_time coordinates the clock sequence across processes via /var/lib/libuuid/clock.txt (or uuidd), eliminating these collisions while preserving the RFC 4122 v1 wire format. Link Core against uuid::uuid on non-Windows when libuuid is found and delegate generation to uuid_generate_time; fall back to the existing code path on Windows and on systems without libuuid. --- cmake/modules/SearchInstalledSoftware.cmake | 21 ++++++------ core/base/CMakeLists.txt | 8 +++++ core/base/src/TUUID.cxx | 36 +++++++++++++++------ 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/cmake/modules/SearchInstalledSoftware.cmake b/cmake/modules/SearchInstalledSoftware.cmake index 220f331595e3e..4180d636efb8c 100644 --- a/cmake/modules/SearchInstalledSoftware.cmake +++ b/cmake/modules/SearchInstalledSoftware.cmake @@ -937,6 +937,13 @@ if (uring) endif() endif() +#---Check for libuuid (used by TUUID for cross-process-unique UUIDv1 generation, +# and by the DAOS backend)--------------------------------------------------- +if(NOT WIN32) + message(STATUS "Looking for libuuid") + find_package(libuuid) +endif() + #---Check for DAOS---------------------------------------------------------------- if (daos AND daos_mock) message(FATAL_ERROR "Options `daos` and `daos_mock` are mutually exclusive; only one of them should be specified.") @@ -946,16 +953,12 @@ if (testing AND NOT daos AND NOT WIN32) endif() if (daos OR daos_mock) - message(STATUS "Looking for libuuid") - if(fail-on-missing) + if(fail-on-missing AND NOT libuuid_FOUND) find_package(libuuid REQUIRED) - else() - find_package(libuuid) - if(NOT libuuid_FOUND) - message(STATUS "libuuid not found. Disabling DAOS support") - set(daos OFF CACHE BOOL "Disabled (libuuid not found)" FORCE) - set(daos_mock OFF CACHE BOOL "Disabled (libuuid not found)" FORCE) - endif() + elseif(NOT libuuid_FOUND) + message(STATUS "libuuid not found. Disabling DAOS support") + set(daos OFF CACHE BOOL "Disabled (libuuid not found)" FORCE) + set(daos_mock OFF CACHE BOOL "Disabled (libuuid not found)" FORCE) endif() endif() if (daos) diff --git a/core/base/CMakeLists.txt b/core/base/CMakeLists.txt index 0ce55d1ff4a27..da9d725b0a828 100644 --- a/core/base/CMakeLists.txt +++ b/core/base/CMakeLists.txt @@ -227,6 +227,14 @@ if(ROOT_NEED_STDCXXFS) target_link_libraries(Core PRIVATE stdc++fs) endif() +# Use libuuid's uuid_generate_time() in TUUID when available so that concurrent +# processes on the same host coordinate their clock sequence and cannot collide +# within the same 100ns tick. See root-project/root#22015. +if(libuuid_FOUND) + target_link_libraries(Core PRIVATE uuid::uuid) + target_compile_definitions(Core PRIVATE R__HAS_LIBUUID) +endif() + # This code about the LIB_CORE_NAME define is important for TROOT::GetSharedLibDir() # On Linux, dl_iterate_phdr reports loaded libraries by their SONAME # (e.g. libCore.so.6.38), not their full filename (libCore.so.6.38.02). diff --git a/core/base/src/TUUID.cxx b/core/base/src/TUUID.cxx index 14bcfb648f6a5..5d0426a2becee 100644 --- a/core/base/src/TUUID.cxx +++ b/core/base/src/TUUID.cxx @@ -21,13 +21,12 @@ originally used in the Network Computing System (NCS) and later in the Open Software Foundation's (OSF) Distributed Computing Environment (DCE). -\note In the way this UUID is constructed, when used outside of -their original concept (NCS), they are actually not Globally unique -and indeed multiple distinct concurrent processes are actually likely -to generate the same UUID. Technically this is because the UUID is -constructed only from the node information and time information. -To make a globally unique number, this needs to be combined with -TProcessUUID. +\note When ROOT is built against `libuuid` (the default on Unix when +it is found at configure time), generation is delegated to +`uuid_generate_time()`, which coordinates the clock sequence across +processes on the same host. Without `libuuid` (e.g. on Windows) the +fallback generator only coordinates within a single process, so +concurrent processes on the same host can produce identical UUIDs. Structure of universal unique IDs (UUIDs). @@ -144,13 +143,31 @@ system clock catches up. #include #endif #include - +#ifdef R__HAS_LIBUUID +#include +#endif //////////////////////////////////////////////////////////////////////////////// -/// Create a UUID. +/// Create a UUID. See the class docs for the libuuid vs. fallback distinction. TUUID::TUUID() { +#ifdef R__HAS_LIBUUID + uuid_t buf; + uuid_generate_time(buf); + // libuuid writes RFC 4122 v1 UUIDs in network byte order: + // bytes 0-3 = time_low, 4-5 = time_mid, 6-7 = time_hi_and_version, + // 8 = clock_seq_hi_and_reserved, 9 = clock_seq_low, 10-15 = node. + fTimeLow = (UInt_t(buf[0]) << 24) | (UInt_t(buf[1]) << 16) | (UInt_t(buf[2]) << 8) | UInt_t(buf[3]); + fTimeMid = static_cast((UShort_t(buf[4]) << 8) | UShort_t(buf[5])); + fTimeHiAndVersion = static_cast((UShort_t(buf[6]) << 8) | UShort_t(buf[7])); + fClockSeqHiAndReserved = buf[8]; + fClockSeqLow = buf[9]; + for (int i = 0; i < 6; ++i) + fNode[i] = buf[10 + i]; + fUUIDIndex = 1 << 30; + return; +#else TTHREAD_TLS(uuid_time_t) time_last; TTHREAD_TLS(UShort_t) clockseq(0); TTHREAD_TLS(Bool_t) firstTime(kTRUE); @@ -189,6 +206,7 @@ TUUID::TUUID() time_last = timestamp; fUUIDIndex = 1<<30; +#endif } ////////////////////////////////////////////////////////////////////////////////