Skip to content

y2038: unify to uint64 GetTickCount64() to properly manage milliseconds - #702

Merged
mrjimenez merged 2 commits into
amule-project:masterfrom
danim7:y2038-ready-squash
May 24, 2026
Merged

y2038: unify to uint64 GetTickCount64() to properly manage milliseconds#702
mrjimenez merged 2 commits into
amule-project:masterfrom
danim7:y2038-ready-squash

Conversation

@danim7

@danim7 danim7 commented May 24, 2026

Copy link
Copy Markdown
Contributor

Get aMule ready for Y2038

Fixes #602

Introduction

This pull request aims to fix several issues concerning time management in aMule that arised when analyzing Y2038 compatibility of aMule.
There are several points:

  1. To be Y2038 ready, wxWidgets version needs to include the upstream fix reference from aMule is not ready for year 2038 problem --> unresponsive, 100% CPU and memory spiking #602. This has already been merged in their master version, so we may expect to see this included in the versions following the current releases 3.2.10 and 3.3.2.
  2. There are a few usages of sint32 datatypes to store value, replace with 64 bits data types.
  3. I did a full rewrite of GetTickCount.{h,cpp}, which contained several potential issues. Detailed analysis follow:

GetTickCount.{h,cpp} files analysis

These files define the function uint32 GetTickCount() used everywhere in the code to manage timeouts. For non-WINDOWS builds, this function calls GetTickCountFullRes() and returns the number of milliseconds from Jan 1 1970. However, the return type is uint32. If it were seconds, uint32 will last 136 years, but for milliseconds, it can hold only 2^32 milliseconds = 50 days aprox. This means this function has been overflowing every ~50 days after Jan 1 1970.

I wrote a small program to tell me the previous and the next "50-days timeout reset" event:

int main () {
unsigned long msecs;
struct timeval aika;
gettimeofday(&aika,NULL);

//convert to milliseconds
msecs = aika.tv_sec * 1000;
uint32_t msecs_truncated = msecs;

//last time millisecons overflowed uint32
uint64_t t1 = (msecs >> 32) << 32;
//next time it will overflow
uint64_t t2 = t1 + ((uint64_t)1<<32);

//convert back to seconds to display the date
time_t ttn = (time_t) msecs/1000;
time_t tt1 = (time_t) t1/1000;
time_t tt2 = (time_t) t2/1000;
cout << "now:      " << put_time(gmtime(&ttn), "%c") << ", full: " << msecs << ", truncated: " << msecs_truncated << endl;
cout << "previous: " << put_time(gmtime(&tt1), "%c") << ", " << tt1 << endl;
cout << "next:     " << put_time(gmtime(&tt2), "%c") << ", " << tt2 << endl;
}

Output:

now:      Sat May 23 22:02:05 2026, full: 1779573725000, truncated: 1457264456
previous: Thu May  7 01:14:20 2026, 1778116460
next:     Thu Jun 25 18:17:07 2026, 1782411427

When running aMule on that moment, we can check on the Verbose Debug log that multiple errors are triggered. Also, aMule freezes for some seconds, probably from other timeouts triggering events like writing files, drawing windows, etc... Apparently no big deal, but not pretty and error-prone, and this uint32 storing milliseconds could hide other problems. It is worth fixing. Example of log output when simulating that date:

2026-06-25 18:17:14: BaseClient.cpp(1385): ED2k Client: --- Deleted client D:3 U:8 "Client Unknown on IP:Port X.X.X.X:YYYY using Unknown Unknown "; Reason was OnError: Unknown client (IP:X.X.X.X) caused an error: 111. Disconnecting client!
.2026-06-25 18:17:14: BaseClient.cpp(1385): ED2k Client: --- Deleted client D:3 U:8 "Client Unknown on IP:Port X.X.X.X:YYYY using Unknown Unknown "; Reason was OnError: Unknown client (IP:X.X.X.X) caused an error: 111. Disconnecting client!
.2026-06-25 18:17:15: BaseClient.cpp(1385): ED2k Client: --- Deleted client D:3 U:8 "Client Unknown on IP:Port X.X.X.X:YYYY using Unknown Unknown "; Reason was OnError: Unknown client (IP:X.X.X.X) caused an error: 113. Disconnecting client!
.2026-06-25 18:17:17: BaseClient.cpp(1385): ED2k Client: --- Disconnected client D:1 U:8 "Client ZZZZ on IP:Port X.X.X.X:YYYY using eMule v0.60c "; Reason was Timeout
.2026-06-25 18:17:17: BaseClient.cpp(1385): ED2k Client: --- Deleted client D:13 U:8 "Client ZZZZ on IP:Port X.X.X.X:YYYY using eMule v0.50a "; Reason was Timeout
.2026-06-25 18:17:17: BaseClient.cpp(1385): ED2k Client: --- Disconnected client D:1 U:8 "Client ZZZZ on IP:Port X.X.X.X:YYYY using eMule v0.60d "; Reason was Timeout
.2026-06-25 18:17:17: BaseClient.cpp(1385): ED2k Client: --- Deleted client D:3 U:8 "Client Unknown on IP:Port X.X.X.X:25147 using Unknown Unknown "; Reason was Timeout
.2026-06-25 18:17:17: BaseClient.cpp(1385): ED2k Client: --- Disconnected client D:1 U:8 "Client ZZZZ on IP:Port X.X.X.X:YYYY using eMule v0.70a "; Reason was Timeout
.2026-06-25 18:17:17: BaseClient.cpp(1385): ED2k Client: --- Disconnected client D:1 U:8 "Client ZZZZ on IP:Port X.X.X.X:YYYY using eMule v0.50a - AdunanzA 3.18 AdunanzA 3.18"; Reason was Timeout

Also, this function uses the deprecated gettimeofday() to retrieve time, which shall be replaced by clock_gettime(). There is also a GetTickCount64() function which returns uint64, but is much less used and also calls gettimeofday().

Additionally, the file contains an unused MyTimer class, which has been removed.

Ironically, the WINDOWS function GetTickCount_64() seems to be the only already free of issues for aMule.

The fix consists in leaving only one function, uint64 GetTickCount64(), using clock_gettime(). Then, replace all calls to the removed functions with this one, and use uint64 datatypes in all code calling it.

On a final note concerning GetTickCount.{h,cpp}, it defines a global variable uint32 TheTime. Since it counts seconds from application start-up, 32 bits are enough.

As a bonus, I removed a couple of GetTickCount used to initialize variables that were never used (example: m_nCreationTime in BaseClient.cpp).
And also improved consecutive >=2 GetTickCount() calls in multiple places. Just do one call and store the return value. Example: change this

if ( ::GetTickCount() - m_lastDiskCheck < DISKSPACERECHECKTIME ) {
		return;
	}
m_lastDiskCheck = ::GetTickCount();

to this

const uint64 curTick = ::GetTickCount64();
if ( curTick - m_lastDiskCheck < DISKSPACERECHECKTIME ) {
	return;
}
m_lastDiskCheck = curTick;

Fix

  • replace deprecated gettimeofday() with clock_gettime()
  • adapt consumers to 64 bits data types
  • revisit GetTickCount files, leave only one 64 bits function used, removed dead and error-prone 32 bits code
  • minor improvements removing unused variables and repeated consecutive calls
  • wxDateTime::Now().GetTicks() is not safe post Y2038 according to the doc, replace it too

Out of scope

  • Uint32 overflowing in year 2106, most notably those stored in files. This allows to keep the on-disk format unchanged for aMule files.
  • time(NULL) calls and time_t types, these are unsinged integers storing seconds, and 64bits on most architectures
  • Analyze the milliseconds timeout that could actually be managed with seconds. Just keep the existing units (milliseconds) in appropiate-size data types.

Testing

I have been running this version on a VM for several days, with the time set to 2038-01-19, performed multiple searches, downloads, restart to read/write conf files, etc...

Opening this pull request as a Draft to collect some reviews / extra testing.

Addresses #602
Keep only uint64 GetTickCount64() as a source of ticks in
milliseconds. Update all calls to use this function and to
store values in uint64 datatypes.
GetTickCount64() will use internally clock_gettime() instead of
the deprecated gettimeofday().
Additionally, reduce calls by removing unused variables and too
consecutive calls that can be stored.
@got3nks

got3nks commented May 24, 2026

Copy link
Copy Markdown
Contributor

Two notes on the GetTickCount rewrite — first is a bigger ask than nits, but I think worth doing while we're here:

1. Use CLOCK_MONOTONIC for GetTickCount64(), route wall-clock callers through time(NULL) directly.

You stayed on CLOCK_REALTIME, matching the old gettimeofday() behavior exactly. That preserves a pre-existing bug: every "now − previous_tick" delta in timeout code goes negative or jumps when NTP or the user adjusts the wall clock. Since we're rewriting the function anyway, the right semantics for a tick-count API is CLOCK_MONOTONIC — never goes backward, immune to clock skew, exactly what the timeout/rate-limit callers want.

Looking at your diff, the callers split cleanly. Almost all of them are the if (GetTickCount64() - last_x > timeout) shape, which is MONOTONIC territory. The only true wall-clock callers are the ones that store seconds-since-epoch on disk:

  • CClientCredits::SetLastSeen (ClientCredits.cpp:166)
  • PartFile source-seeds serialization (file.WriteUInt32(...))
  • PartFile source-seeds 120-minute validity check
  • wxCas WxCasCte::ABSOLUTE_MAX_DL_DATE_KEY default

Those four sites should use time(NULL) directly — it's the idiomatic one-liner for "Unix timestamp now". Then GetTickCount64() itself can flip to CLOCK_MONOTONIC without breaking anything that depends on it being interpretable as wall-clock.

Concrete change:

 // GetTickCount.cpp
 uint64 GetTickCount64(void) {
     struct timespec ts;
-    // Fetch time (Y2038-safe)
-    clock_gettime(CLOCK_REALTIME, &ts);
+    // CLOCK_MONOTONIC: tick count is for timeouts / deltas, must not
+    // jump when wall clock is adjusted. Callers that need a Unix
+    // timestamp (CClientCredits::SetLastSeen, partfile source-seeds
+    // serialization, wxCas defaults) should use time(NULL) instead.
+    clock_gettime(CLOCK_MONOTONIC, &ts);
     msecs = (uint64) ts.tv_sec * 1000;
     msecs += ts.tv_nsec / 1000000;
     return msecs;
 }
 // ClientCredits.cpp
-    m_pCredits->nLastSeen = GetTickCount64()/1000;
+    m_pCredits->nLastSeen = time(NULL);
 // PartFile.cpp source-seeds write
-    file.WriteUInt32((uint32)(GetTickCount64()/1000));
+    file.WriteUInt32((uint32) time(NULL));
 // PartFile.cpp source-seeds 120-min validity check
-    if ((time + MIN2S(120)) >= GetTickCount64()/1000) {
+    if ((time + MIN2S(120)) >= (uint32) time(NULL)) {
 // wxCas/wxcasframe.cpp
-    ( long ) ( ts.tv_sec ) ) ) ); // Stored in Ticks
+    ( long ) ( time(NULL) ) ) ) );

Five sites total, all single-line. Original PR keeps its Y2038 win and gains the clock-skew immunity for free.

2. Cosmetic only: SetLastSeen() revert above is the same change I'd have suggested for stylistic reasons alone — time(NULL) reads more directly than the round-trip through GetTickCount64()/1000. Subsumed by (1).

@got3nks

got3nks commented May 24, 2026

Copy link
Copy Markdown
Contributor

Small clarification on the wxCas hunk — with the time(NULL) substitution, the struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); pair you added at the top of WxCasFrame::WxCasFrame becomes dead (its only consumer was the ts.tv_sec it fed the default). Just remove those two lines along with the suggested swap, so the hunk is:

@@ WxCasFrame::WxCasFrame ( const wxString & title ) :
 
 	m_maxLineCount = 0;
 
-	struct timespec ts;
-	clock_gettime(CLOCK_REALTIME, &ts);
-
 	// Check if we have a previous DL max hit
 	double absoluteMaxDL = ( double ) ( prefs->Read ( WxCasCte::ABSOLUTE_MAX_DL_KEY, 0L ) ) / 1024.0; // Stored in bytes
 	wxDateTime absoluteMaxDlDate( ( time_t ) ( prefs->Read ( WxCasCte::ABSOLUTE_MAX_DL_DATE_KEY,
-	                              ( long ) ( ts.tv_sec ) ) ) ); // Stored in Ticks
+	                              ( long ) ( time(NULL) ) ) ) );

integrated suggestion by @got3nks to use CLOCK_MONOTONIC instead
of CLOCK_REALTIME, to be safe in case of negative jump because of
NTP or user adjusted wall-clock going backwards
@danim7

danim7 commented May 24, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for your review @got3nks

I integrated your suggestions and pushed a new commit. Indeed, most of the calls are MONOTONIC territory, let's make the full fix since we are here. I just needed to slightly adjust the second PartFile.cpp diff, since there was a variable called "time" that I needed to rename before calling time(NULL).

Compiled and running on my side, I will let it run a couple of hours and propose to merge if it goes fine.

@danim7
danim7 marked this pull request as ready for review May 24, 2026 21:20
@mrjimenez
mrjimenez merged commit 56a369e into amule-project:master May 24, 2026
12 checks passed
@danim7
danim7 deleted the y2038-ready-squash branch June 9, 2026 16:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

aMule is not ready for year 2038 problem --> unresponsive, 100% CPU and memory spiking

3 participants