Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,19 @@ Notable changes to Chrono Mock, newest first. The format follows
fix below: a network library that measures its own timeout from the tick count, WinHTTP for one,
now counts that timeout in session time, while the network wait underneath stays real. At x60 a
60 second timeout runs out after one real second, so a server slower than that makes the request
fail. A session with timers sped up now says so whenever the application has the network stack
loaded, right under the line saying that waits on system objects stay on the real clock.
fail. A session with timers sped up now says so whenever the application opened a network
connection, right under the line saying that waits on system objects stay on the real clock.

### Fixed

- **The network caution missed most applications that go online.** The audit is meant to say when
an application opened a network connection, because it may then take the time from a server,
which no local substitution reaches. It watched one Windows function for that, and two of the
three ways to connect never call it - among them the one WinHTTP and WinINet use, and the ones
.NET, Node.js and Go connect through. Those applications got no caution and a clean result. The
audit now counts every connection attempt at the point all of them pass through, whichever
function made it, on both 32 and 64 bit. A datagram sent without a connection is still not
counted, because it is not one.
- **The .NET timing caution said `Environment.TickCount` follows the session speed.** It does only
when timers are sped up too (`--scale-duration`, or "Also speed up timers and countdowns inside
the application" in the window). With that off, it runs at real speed, so a tester reading the
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ column says which is which._
| .NET (Framework and modern) | experimental | measured on x64, x86 | Time calls go through Win32 exports and are covered, including the session time zone. Stopwatch stays on the real high-resolution counter unless you opt in with Scale QPC (`--scale-qpc`), which accelerates it too |
| Java (JVM) | experimental | measured on x64, x86 | Wall clock and elapsed time are covered. The session time zone is not reached - a known gap. nanoTime stays on the real high-resolution counter unless you opt in with Scale QPC (`--scale-qpc`) |
| Python (CPython, incl. PyInstaller) | experimental | measured by hand on x64, not by the suite | Wall clock (time.time, datetime) and the session time zone (time.localtime) are covered. perf_counter, and monotonic on Python 3.13+, are on the high-resolution counter - real by default, accelerated when you opt in with Scale QPC (`--scale-qpc`) |
| Applications reading time from the network | out of scope by definition | measured on x64, x86 | The audit detects it - connect observed, warned |
| Applications reading time from the network | out of scope by definition | measured on x64, x86 | The audit detects it - every connection attempt made through Windows' own socket layer is observed and warned about, whichever function made it (Winsock, WinHTTP, WinINet, and the .NET, Node.js, Go, Java and Python runtimes). A datagram sent without a connection is not a connection and is not counted. A third-party Winsock provider, rare on current Windows, is not watched |
| Embedded web engine inside a native app (WebView2, Qt WebEngine) | experimental | measured by hand on a WebView2 host and a Qt WebEngine host (x64), not by the suite | The native hook covers the application and the pages inside it are reached over the engine's debugging port, opened for the session through two environment variables the application inherits. The pages read the session clock at the session rate and follow a speed change and a jump. The engine's helper processes and the renderer's native reads stay on the real clock, so the verdict is PARTIAL and says why. The pages keep the machine's time zone. An elevated application is out of reach - the engine ignores the variables |
| Electron / Chromium | experimental (Chromium mode) | measured by hand on an Electron app (x64), not by the suite | A separate mechanism, not injection: the app is launched with a debug port and a clean isolated profile, and its own JS time APIs are put on the session clock over the DevTools protocol - reaching the sandboxed renderer and its Web Workers, where the timer often lives. The session zone follows the host zone (the instant is faked, not the local-time getters) |
| UWP / MSIX (Store apps) | not supported | declared (not exercised) | Packaging and launch model |
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ pub(crate) fn describe_warning(key: &str) -> String {
// Sits next to the object-wait line on purpose: that one says an I/O timeout is not shortened,
// and for a library that measures its own timeout from the tick count this one says otherwise.
"wait.network_timeouts_scaled" => {
"this application has the network stack loaded - a network library that measures its own \
"this application opened a network connection - a network library that measures its own \
timeout from the tick count (WinHTTP, for one) follows the session speed, so a server slower \
than that timeout divided by the speed makes a request fail, even though the waits \
underneath stay real"
Expand Down
6 changes: 4 additions & 2 deletions crates/cli/tests/hygiene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1371,8 +1371,10 @@ fn every_optional_module_channel_can_be_installed_late() {
let channels = optional_module_channels(&ctl_src);

// Canary, with a LITERAL rather than a count derived from the same list it checks: six channels
// live in optional modules today (timeGetTime, timeSetEvent, SetTimer, both message waits,
// connect). Fewer means the scan stopped matching the table's shape and went blind.
// live in optional modules today (timeGetTime, timeSetEvent, SetTimer, both message waits, the
// socket wait). Fewer means the scan stopped matching the table's shape and went blind. The
// connection observer left the list for ntdll on 2026-09-23, when the socket wait already made
// the count seven.
assert!(
channels.len() >= 6,
"found only {} channels in optional modules - the CHANNELS table changed shape and this \
Expand Down
45 changes: 34 additions & 11 deletions crates/cli/tests/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@
//! no equivalent of a Python audit hook here, so the static half stands alone.
//! * **Data can leave a machine without a socket** - a file written into a synced folder, a report
//! pasted into an issue. Nothing here looks at that.
//! * **The hooked `connect` is somebody else's traffic, not ours.** `chrono-hook` resolves
//! `ws2_32.dll` and intercepts `connect` so the audit can report that the target application
//! asked the network for something, which is a suspected server time source. Counting a call is
//! the opposite of making one, and the register says so where it grants that. The binary layer
//! says the half this one cannot: `chrono_hook.dll` LINKS no networking DLL at all, `ws2_32`
//! included, because it looks that module up only when the target has already loaded it.
//! * **The observed connections are somebody else's traffic, not ours.** `chrono-hook` counts the
//! target's connection attempts where they reach the socket driver, in ntdll, so the audit can
//! report that the target application asked the network for something, which is a suspected
//! server time source. It also resolves `ws2_32.dll` to observe the target's socket waits.
//! Counting a call is the opposite of making one, and the register says so where it grants that.
//! The binary layer says the half this one cannot: `chrono_hook.dll` LINKS no networking DLL at
//! all, `ws2_32` included, because it looks that module up only when the target has already
//! loaded it.
//!
//! So this is not a proof of silence. It is a lock on the surface: nobody adds a way out by
//! accident, and adding one on purpose means editing a register here and writing down why.
Expand Down Expand Up @@ -174,20 +176,41 @@ const ALLOWED: &[(&str, &str, &str)] = &[
session through the built binary, and reads the tick rate it wrote to a scratch file. \
Neither reaches past this machine",
),
(
"crates/cli/tests/network_observer.rs",
"spawn",
"runs this test binary's own ignored probe twice, once alone as the control and once under a \
session through the built binary. Neither reaches past this machine",
),
(
"crates/cli/tests/network_observer.rs",
"socket",
"binds a loopback listener on a port the system picks, and the probe connects to that port \
alone, which is the one thing the connection observer can be seen to count. Nothing is \
sent, and the listener is closed before the test ends",
),
(
"crates/cli/tests/network_observer.rs",
"winsock",
"declares the Winsock functions the probe connects through - WSAConnect and ConnectEx, the \
two ways to connect that never call the connect export - against the same loopback \
listener",
),
(
"crates/hook/src/lib.rs",
"winsock",
"the injected library resolves ws2_32 to INTERCEPT the target's own connect and count it. \
Counting somebody else's call is the opposite of making one, and the audit reports it as \
a suspected server time source",
"the injected library resolves ws2_32 to OBSERVE the target's own socket waits, and counts \
the target's own connection attempts in ntdll. Counting somebody else's call is the \
opposite of making one, and the audit reports a connection as a suspected server time \
source",
),
(
"gui/ChronoMock.Protocol.Tests/BinaryImportsTests.cs",
"winsock",
"the binary layer of this same guard, which names the module in order to REFUSE it. It reads \
the import table of every release binary and asserts that ws2_32 is linked by chrono.exe \
and by nothing else - least of all by chrono_hook.dll, which hooks connect without linking \
it. Naming a module in a register is the opposite of opening one",
and by nothing else - least of all by chrono_hook.dll, which observes connections without \
linking it. Naming a module in a register is the opposite of opening one",
),
(
"gui/ChronoMock.Protocol/CoreClient.cs",
Expand Down
Loading
Loading