alosafuzz
ICS/OT protocol security research — fuzzing, scanning, and coordinated disclosure.

Fuzzing the time: coverage-guided testing of PTP, gPTP, and NTS across four implementations

August 9, 2026 · Project: timefuzzer

Almost everything that has to agree on when leans on a small set of network time protocols. NTP is the famous one, and it has been fuzzed to death — Project Zero’s AFL campaign, AmpFuzz, and continuous OSS-Fuzz coverage on ntpsec. So I went looking for the parts of the time ecosystem that haven’t had that attention: the Precision Time Protocol (PTP / IEEE 1588) that underpins telecom, finance, and industrial timing; its automotive/TSN cousin gPTP (802.1AS); and Network Time Security (NTS, RFC 8915), the TLS-authenticated layer that finally makes NTP trustworthy.

This is a writeup of a structured fuzzing campaign against those four implementations — linuxptp and ptpd for PTP, chrony and ntpsec for NTS. The headline result is not a string of CVEs. It’s the opposite, and I think that’s worth saying plainly: across roughly 840 million executions, under AddressSanitizer and UndefinedBehaviorSanitizer, against every part that I could reach, I found no memory-safety or undefined-behavior defects. These are mature, careful codebases, and they held up. A robust result, rigorously arrived at, is a legitimate outcome — and the interesting story is in how you make a “nothing broke” claim trustworthy.

The method, briefly

I fuzzed these targets in-process and coverage-guided (libFuzzer + ASan/UBSan), not black-box over the wire. Every target here is open source, so I compiled it with instrumentation and drove its real parsing and handling code directly. A few techniques did the heavy lifting:

Coverage matrix

The point of a “nothing broke” result is the reach behind it. Rows where the target simply held up are collapsed; the numbers are function/region coverage of the reachable attack surface (whole-file percentages are lower and misleading — they include transmit/build, servo, and timer code a receive-path fuzzer structurally cannot reach).

Attack Surface Implementation What was fuzzed Reach Executions Result
PTP message parser linuxptp v4.4 msg_post_recv + all TLV sub-parsers, incl. structure-aware mutator passes 100% of reachable 251M+ held up
PTP header decode (differential) linuxptp vs ptpd 6 common-header fields — 72.7M 0 divergences
PTP handlers / state machine linuxptp v4.4 management, sync, delay, signaling, unicast management 90%, sync 85%, unicast-service 47% ~90M held up
gPTP / 802.1AS (L2) linuxptp v4.4 has_prp_trailer (L2 frame parse) 95% 60.2M held up
NTS-KE record parser chrony 4.8 framing + client + server dispatch framing 100%, server 100%, client 86% 131.6M held up
NTS NTP-path EF parser (request) chrony 4.8 NEF_ParseField (extension-field structural parse) 100% 54.6M held up
NTS-KE inner (crypto-gated) chrony 4.8 decrypted request EFs 100% 26.8M held up
NTS-KE (real 2nd impl) ntpsec nts_ke_process_receive under ASan 94.7% 101.4M held up
NTS client response (hostile server) chrony 4.8 decrypted response EFs 87% 27.5M held up
NTS-KE framing (differential) chrony vs ntpsec record type/length/critical + bounds — 24.7M 0 divergences

Two independent PTP implementations agree on header decoding; two independent NTS implementations agree on record framing. Nothing crashed.

A few things worth reading

Most of the campaign is “held up fine,” so here are only the parts I found genuinely interesting.

When 100% coverage isn’t reachable — and that’s the answer. linuxptp’s management-message parser sat stubbornly at 98% of lines, with a handful of if (data_len < 2) goto bad_length guards I couldn’t hit. It turns out they’re provably unreachable: the field is a 2-byte id, the only caller invokes the parser solely when the TLV length exceeds 2, and odd lengths are rejected upstream — so data_len is always ≥ 2. The guards are dead code by construction: good defensive programming that a caller invariant makes impossible to trigger. Proving a branch unreachable is as valuable as covering it.

gPTP is mostly the code you already fuzzed. I expected 802.1AS to be a distinct target. It isn’t: in linuxptp, gPTP reuses the same message/TLV parser and handlers as L3 PTP — the “profile” is a config flag and a modified best-master comparison, not a new parser. The only genuinely new attacker-reachable code is the L2 frame wrapper (has_prp_trailer, a small PRP-trailer detector), which I fuzzed clean. Scoping the delta of a protocol variant, instead of re-fuzzing shared code, is a result in itself.

Reaching the crypto-gated parsers. The most interesting harness work was for NTS. An NTS server parses a client’s inner extension fields only after decoding an authenticated cookie and decrypting the request; a random packet dies at the cookie. So I held the server keys myself, generated valid cookies and authenticators with the implementation’s own routines, and made each fuzz input the decrypted inner content. That took the inner extension-field parser from 0% to full coverage on the server side (100%, parsing a client’s decrypted request) and to 87% on the client side (parsing a hostile server’s decrypted response). This is the right way to fuzz authenticated protocols: don’t fight the crypto, use it.

Catching your own false positives. Twice the harness “crashed” and twice it was the harness, not the target. A NULL-dereference in linuxptp’s unicast timer fired only because I manually triggered a timer the real daemon only arms when the feature is configured — unreachable in production. And several Assertion 'initialised' failed aborts in chrony were just missing module-init calls in my setup. The discipline that makes a “0 findings” claim credible is the same one that rejects a false finding: before believing a crash, prove the trigger is reachable in the real program.

Disclosure status

Nothing to disclose. I want to be precise about what that does and doesn’t mean, because “we fuzzed it and found nothing” is easy to say and hard to justify:

Lessons

Closing

I set out to find bugs in under-tested time protocols and instead spent most of the time proving I couldn’t. That’s an anticlimax for a bug hunter and a good day for anyone whose clocks depend on this code. The deployed reality is that the maintainers of linuxptp, chrony, and ntpsec have written parsers that survive structured, coverage-guided, sanitizer-backed fuzzing across their reachable attack surface — and that two independent implementations of each protocol agree on how to read the wire. That is a quieter story than a CVE, but it is the one the evidence supports, and I’d rather publish the result the data actually shows.

Glossary

Protocols & standards

Fuzzing & tooling terms

Implementations fuzzed

Tooling: timeFuzzer on GitHub. Reach me at alosafuzz@proton.me.