Bug 289177 - IPv6 temp address(es) with pltime=0 briefly loses deprecated on RA → OS may pick the old address as the source
Summary: IPv6 temp address(es) with pltime=0 briefly loses deprecated on RA → OS may p...
Status: Closed FIXED
Alias: None
Product: Base System
Classification: Unclassified
Component: kern (show other bugs)
Version: 14.3-RELEASE
Hardware: Any Any
: --- Affects Many People
Assignee: freebsd-net (Nobody)
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2025-08-30 06:11 UTC by Dmitrij Nexus
Modified: 2025-09-23 08:42 UTC (History)
3 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Dmitrij Nexus 2025-08-30 06:11:09 UTC
IPv6 temp address(es) with pltime=0 briefly loses deprecated on RA → OS may pick the old address as the source.

[Environment]
OS: FreeBSD 14.3-RELEASE-p2 (amd64, Intel CPU)
Interface: wlan0
SLAAC with temporary/privacy addresses enabled
All IPv6 addresses/prefixes below are obfuscated (e.g., 2001:db8:1234:5678:aaaa:bbbb:cccc:dddd)

[Summary]
Upon receipt of a Router Advertisement (RA), any existing IPv6 temporary address(es) whose preferred lifetime has already expired (pltime=0) briefly loses its deprecated state (~1 s) and then returns to deprecated. During this flip-flop window, the OS may select that old address as the source for new outbound connections, causing intermittent ACL denials on remote systems that whitelist only the current preferred address. This behavior appears non-compliant with RFC 4862/4941/8981 expectations (a pltime=0 address should remain deprecated for the rest of its valid lifetime) and can lead to unexpected, undesirable side effects, including transient exposure of the user’s old IPv6 addresses, which should not occur.

[Steps to Reproduce]
• Enable temporary addresses and connect to a network that advertises a global prefix.
• Wait until an older temp address reaches pltime 0 while still having vltime > 0 (deprecated but valid).
• Sample ifconfig <if> inet6 once per second while RAs are being received.
• Observe that on each RA the deprecated address loses the “deprecated” label, then regains it after ~1 second.

[Expected Behavior]
Once pltime=0, the address should remain deprecated continuously; subsequent RAs must not create a window where it appears usable for new connections.

[Actual Behavior]
On each RA, the already-expired temporary address(es) becomes non-deprecated for ~1 second and then flips back to deprecated.

[Impact]
• During the ~1-second flip after each RA, the kernel may select a previously deprecated temporary address (pltime=0) as the source for new flows.
• Leads to intermittent IP-ACL denials, policy/geofencing mismatches, and flaky behavior in automation that relies on stable source addresses.
• Privacy risk: transient re-exposure of old temporary IPv6 addresses in outbound traffic, logs, and third-party telemetry—contrary to the privacy intent of RFC 4941/8981 rotation.
• Disrupts observability and auditing (source attribution changes mid-session), complicating incident analysis and per-IP accounting/billing.

[Code details] (FreeBSD 14.3-RELEASE-p2) (by ChatGPT - may have errors)
Where “deprecated” is decided — sys/netinet6/in6.h:
#define IFA6_IS_DEPRECATED(a) \
	((a)->ia6_lifetime.ia6t_pltime != ND6_INFINITE_LIFETIME && \
	(u_int32_t)((time_uptime - (a)->ia6_updatetime)) > \
	(a)->ia6_lifetime.ia6t_pltime)

#define IFA6_IS_INVALID(a) \
	((a)->ia6_lifetime.ia6t_vltime != ND6_INFINITE_LIFETIME && \
	(u_int32_t)((time_uptime - (a)->ia6_updatetime)) > \
	(a)->ia6_lifetime.ia6t_vltime)
Where RA lifetimes are refreshed — Router Advertisements are processed in sys/netinet6/nd6_rtr.c (nd6_ra_input(...)). During PIO handling, address lifetimes are updated and the address “update time” (ia6_updatetime) is refreshed (via the address/prefix update path in the IPv6 stack).

Why the flip-flop happens (step by step)
1. Before RA: the old temporary address has pltime=0, so it is deprecated.
2. RA arrives → the RA path refreshes lifetimes and sets ia6_updatetime = time_uptime (now).
3. The predicate becomes:
(time_uptime - ia6_updatetime) > pltime
→ (0) > 0  == false
Thus, for ~1 tick, the address evaluates as non-deprecated.
4. After ~1 second, the left side becomes > 0, the predicate turns true, and the address is again deprecated.
5. In that short window the kernel may pick the old temp address as a source for new flows (observed in practice).

[Evidence] (obfuscated sources)
ifconfig wlan0 inet6:
inet6 2001:db8:aaaa:bbbb:5555:6666:7777:8888 prefixlen 64 deprecated autoconf temporary pltime 0 vltime 400150
# immediately after RA:
inet6 2001:db8:aaaa:bbbb:5555:6666:7777:8888 prefixlen 64 autoconf temporary pltime 0 vltime 400100   # lost 'deprecated' for ~1s
# next second:
inet6 2001:db8:aaaa:bbbb:5555:6666:7777:8888 prefixlen 64 deprecated autoconf temporary pltime 0 vltime 400100

$ repeat 500 ( sleep 0.5; ping -o google.com )
...
--- google.com ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 17.833/17.833/17.833/0.000 ms
PING(56=40+8+8 bytes) 2001:db8:aaaa:bbbb:1111:2222:3333:4444 --> 2a00:1450:401b:808::200e
16 bytes from 2a00:1450:401b:808::200e, icmp_seq=0 hlim=120 time=17.495 ms

--- google.com ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 17.495/17.495/17.495/0.000 ms
PING(56=40+8+8 bytes) 2001:db8:aaaa:bbbb:5555:6666:7777:8888 --> 2a00:1450:401b:808::200e  # Used deprecated IPv6 as source
16 bytes from 2a00:1450:401b:808::200e, icmp_seq=0 hlim=118 time=18.777 ms

--- google.com ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 18.777/18.777/18.777/0.000 ms
PING(56=40+8+8 bytes) 2001:db8:aaaa:bbbb:5555:6666:7777:8888 --> 2a00:1450:401b:808::200e  # Used deprecated IPv6 as source
16 bytes from 2a00:1450:401b:808::200e, icmp_seq=0 hlim=118 time=17.698 ms

--- google.com ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 17.698/17.698/17.698/0.000 ms
PING(56=40+8+8 bytes) 2001:db8:aaaa:bbbb:1111:2222:3333:4444 --> 2a00:1450:401b:808::200e
16 bytes from 2a00:1450:401b:808::200e, icmp_seq=0 hlim=120 time=18.116 ms

--- google.com ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 18.116/18.116/18.116/0.000 ms
PING(56=40+8+8 bytes) 2001:db8:aaaa:bbbb:1111:2222:3333:4444 --> 2a00:1450:401b:808::200e
16 bytes from 2a00:1450:401b:808::200e, icmp_seq=0 hlim=120 time=18.183 ms
Comment 1 Marek Zarychta 2025-08-30 21:16:06 UTC
(In reply to Dmitry Nexus from comment #0)
Nice catch, thanks for reporting. While the current mechanism probably deserves a more thorough rewrite, this simple patch should at least fix the off-by-one error. Does it resolve the issue for you?

--- a/sys/netinet6/in6.h
+++ b/sys/netinet6/in6.h
@@ -358,11 +358,11 @@ extern const struct in6_addr in6addr_linklocal_allv2routers;
 
 #define IFA6_IS_DEPRECATED(a) \
        ((a)->ia6_lifetime.ia6t_pltime != ND6_INFINITE_LIFETIME && \
-        (u_int32_t)((time_uptime - (a)->ia6_updatetime)) > \
+        (u_int32_t)((time_uptime - (a)->ia6_updatetime)) >= \
         (a)->ia6_lifetime.ia6t_pltime)
 #define IFA6_IS_INVALID(a) \
        ((a)->ia6_lifetime.ia6t_vltime != ND6_INFINITE_LIFETIME && \
-        (u_int32_t)((time_uptime - (a)->ia6_updatetime)) > \
+        (u_int32_t)((time_uptime - (a)->ia6_updatetime)) >= \
         (a)->ia6_lifetime.ia6t_vltime)
 #endif /* _KERNEL */
Comment 2 Marek Zarychta 2025-09-01 07:22:30 UTC
It will be nice to fix this issue before branching stable/15 this week.
Comment 3 Dmitrij Nexus 2025-09-01 10:26:57 UTC
(In reply to Marek Zarychta from comment #2)
I am currently testing this and plan to provide feedback within a few hours.
Comment 4 karl 2025-09-01 12:27:28 UTC
This is related if FreeBSD is the gateway......

https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=288426
Comment 5 Dmitrij Nexus 2025-09-01 17:01:37 UTC
(In reply to Marek Zarychta from comment #1)
Tested on a FreeBSD 14.3-RELEASE test system with the proposed patch applied.
Router Advertisements were sent at a high frequency (every few seconds).

For faster IPv6 temporary address rotation, I configured short lifetimes via sysctl:
net.inet6.ip6.temppltime=600
net.inet6.ip6.tempvltime=1500

The system with multiple deprecated addresses was observed for 30+ minutes. The flip-flop of the deprecated flag did not occur anymore.

Many thanks for the quick fix and support.
Comment 6 Marek Zarychta 2025-09-01 17:44:40 UTC
(In reply to Dmitry Nexus from comment #5)
Thanks for testing!
I have created review D52323.
Comment 7 commit-hook freebsd_committer freebsd_triage 2025-09-16 07:39:26 UTC
A commit in branch main references this bug:

URL: https://cgit.FreeBSD.org/src/commit/?id=588a5fad3e8b98955b60707e3e92b8b43566e3f7

commit 588a5fad3e8b98955b60707e3e92b8b43566e3f7
Author:     Andrey V. Elsukov <ae@FreeBSD.org>
AuthorDate: 2025-09-16 07:34:55 +0000
Commit:     Andrey V. Elsukov <ae@FreeBSD.org>
CommitDate: 2025-09-16 07:34:55 +0000

    IPv6: fix off-by-one in pltime and vltime expiration checks

    Previously, the macros used '>' instead of '>=' when comparing elapsed
    time against the preferred and valid lifetimes. This caused any deprecated
    address to become usable again for one extra second after receiving each
    Router Advertisement. In that short window, the address could be
    selected as a source for outgoing connections.

    Update the checks to use '>=' so that addresses are deprecated or
    invalid when their lifetime expires.

    PR:             289177
    Reported by:    Dmitry Nexus <fbsd.4f6a at nexus tel>
    Reviewed by:    zlei
    Submitted by:   Marek Zarychta
    MFC after:      1 week
    Differential Revision:  https://reviews.freebsd.org/D52323

 sys/netinet6/in6.h | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
Comment 8 commit-hook freebsd_committer freebsd_triage 2025-09-23 08:32:58 UTC
A commit in branch stable/15 references this bug:

URL: https://cgit.FreeBSD.org/src/commit/?id=fb4be8661166e18afa4b10921f7d5fbd22460390

commit fb4be8661166e18afa4b10921f7d5fbd22460390
Author:     Andrey V. Elsukov <ae@FreeBSD.org>
AuthorDate: 2025-09-16 07:34:55 +0000
Commit:     Andrey V. Elsukov <ae@FreeBSD.org>
CommitDate: 2025-09-23 08:31:12 +0000

    IPv6: fix off-by-one in pltime and vltime expiration checks

    Previously, the macros used '>' instead of '>=' when comparing elapsed
    time against the preferred and valid lifetimes. This caused any deprecated
    address to become usable again for one extra second after receiving each
    Router Advertisement. In that short window, the address could be
    selected as a source for outgoing connections.

    Update the checks to use '>=' so that addresses are deprecated or
    invalid when their lifetime expires.

    PR:             289177
    Reported by:    Dmitry Nexus <fbsd.4f6a at nexus tel>
    Reviewed by:    zlei
    Submitted by:   Marek Zarychta
    Differential Revision:  https://reviews.freebsd.org/D52323

    (cherry picked from commit 588a5fad3e8b98955b60707e3e92b8b43566e3f7)

 sys/netinet6/in6.h | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
Comment 9 commit-hook freebsd_committer freebsd_triage 2025-09-23 08:34:00 UTC
A commit in branch stable/14 references this bug:

URL: https://cgit.FreeBSD.org/src/commit/?id=586183128f2f04b84a9a564f83289963671f0ff2

commit 586183128f2f04b84a9a564f83289963671f0ff2
Author:     Andrey V. Elsukov <ae@FreeBSD.org>
AuthorDate: 2025-09-16 07:34:55 +0000
Commit:     Andrey V. Elsukov <ae@FreeBSD.org>
CommitDate: 2025-09-23 08:32:45 +0000

    IPv6: fix off-by-one in pltime and vltime expiration checks

    Previously, the macros used '>' instead of '>=' when comparing elapsed
    time against the preferred and valid lifetimes. This caused any deprecated
    address to become usable again for one extra second after receiving each
    Router Advertisement. In that short window, the address could be
    selected as a source for outgoing connections.

    Update the checks to use '>=' so that addresses are deprecated or
    invalid when their lifetime expires.

    PR:             289177
    Reported by:    Dmitry Nexus <fbsd.4f6a at nexus tel>
    Reviewed by:    zlei
    Submitted by:   Marek Zarychta
    Differential Revision:  https://reviews.freebsd.org/D52323

    (cherry picked from commit 588a5fad3e8b98955b60707e3e92b8b43566e3f7)

 sys/netinet6/in6.h | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
Comment 10 Andrey V. Elsukov freebsd_committer freebsd_triage 2025-09-23 08:42:51 UTC
Fixed in main, stable/15 and stable/14. Thanks!