expires_ns is evaluated against the caller’s clock (L4 auth / ACL)¶
An ACE may carry an absolute deadline: expires_ns, nanoseconds since the UNIX epoch, with 0
meaning never. It is not a timer and nothing sweeps it — the policy compares it to the now
its caller passed in, on every single check
(ADR-0050).
The example runs against the pure surface first, because effective_acl_t takes now as a
parameter — so it can step the clock without sleeping — and then confirms the identical rule at
the real graph_t door, which reads its own clock.
What to notice¶
One merged list, four verdicts, zero invalidations. The example asks the same
effective_acl_tabout severalnows, including going back to an earlier one. That is why the graph caches the effective-ACE merge but never a verdict: a merge stays valid as the clock moves, so expiry needs no invalidation mechanism at all.Expiry is
expires_ns <= now, not<. At the deadline the grant is already gone. The example pins both sides of that single nanosecond.An expired ACE grants nothing — and still closes the vertex. Presence is what closes (open by default), and an expired entry is present. So a temporary grant that lapses does not restore the open state it replaced; it leaves the vertex shut to everyone. This is the trap: “the badge expired, so we are back to normal” is exactly wrong.
0means never expires, not expired at the epoch. The example checks that too, because the two readings of a zero field differ by the entire lifetime of the grant.The parser refuses an empty
expires_nsfor the same reason. An empty payload would load as0— the permissive reading — so an absent value must not be allowed to mean “never expires” (strict ACL parsing).Nothing here is conditional, and nothing sleeps. No timers, no threads, no wall-clock rendezvous: the deadline is a number the example passes in. The graph-level half uses grants that expired one nanosecond after the epoch and one that expires in the year 2262, so it is deterministic on any machine, on any CI leg.
Source¶
1/*
2 * SPDX-License-Identifier: Apache-2.0
3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
4 */
5
6/**
7 * @file
8 * @brief ONE CONCEPT — `expires_ns` is evaluated against the CALLER's `now`, at check time.
9 *
10 * An ACE may carry an absolute deadline: `expires_ns`, nanoseconds since the UNIX epoch, with
11 * `0` meaning "never". It is not a timer and nothing sweeps it — the policy compares it to the
12 * `now` its caller passed in, on every single check. That is why the graph caches the merged
13 * effective-ACE list but never a verdict: a merge stays valid as the clock moves, so expiry
14 * needs no invalidation mechanism at all (ADR-0050).
15 *
16 * The consequence to hold on to is the one that catches people out: an expired ACE grants
17 * nothing AND still closes the vertex. Presence is what closes (see acl_open_by_default), and
18 * an expired entry is present. A temporary grant that lapses therefore does not restore the
19 * open state it replaced — it leaves the vertex shut to everyone.
20 *
21 * This runs against the PURE surface — `effective_acl_t` takes `now` as a parameter, so the
22 * example can step the clock without sleeping — and then confirms the same rule at the real
23 * `graph_t` door, which reads its own clock.
24 *
25 * Runs under ctest as `example_acl_expiry`; returns non-zero on any failed check.
26 */
27
28#include <cstdint>
29#include <cstdio>
30#include <cstring>
31#include <span>
32#include <string_view>
33#include <vector>
34
35#include "libtracer/graph.hpp"
36#include "libtracer/mem_heap.hpp"
37#include "libtracer/security_acl.hpp"
38
39namespace {
40
41using tr::graph::ace_t;
42using tr::graph::acl_right_t;
43using tr::graph::effective_acl_t;
44using tr::graph::graph_t;
45using tr::graph::path_t;
46using tr::graph::role_t;
47using tr::graph::status_t;
48using tr::graph::subject_token_t;
49using tr::graph::vertex_handle_t;
50
51/** @brief Report expectation @p what and record a failure on @p ok. */
52void check(bool& ok, bool cond, const char* what) {
53 std::printf(" [%s] %s\n", cond ? "ok" : "FAIL", what);
54 ok = ok && cond;
55}
56
57/** @brief @p s as opaque subject-token bytes. */
58std::vector<std::byte> as_bytes(std::string_view s) {
59 std::vector<std::byte> out(s.size());
60 std::memcpy(out.data(), s.data(), s.size());
61 return out;
62}
63
64/** @brief The caller context IS the subject token. */
65std::expected<subject_token_t, tr::wire::err_t> caller_is_subject(void*, std::string_view caller) {
66 return as_bytes(caller);
67}
68
69/** @brief @p right as the single `access_mask` bit it is. */
70constexpr std::uint32_t bit(acl_right_t right) { return static_cast<std::uint32_t>(right); }
71
72/** @brief A one-byte VALUE. */
73tr::view::view_t some_value() {
74 const std::byte one[1] = {std::byte{0x01}};
75 return *tr::view::over_bytes(one);
76}
77
78/** @brief True iff @p r was refused by an ACL gate. */
79template <class T>
80bool denied(const tr::graph::result_t<T>& r) {
81 return !r.has_value() && r.error() == status_t::PERMISSION_DENIED;
82}
83
84/** @brief A visitor's badge, valid until @ref kDeadline. */
85constexpr std::uint64_t kDeadline = 1'800'000'000'000'000'000ULL; // ~2027-01-15, in ns
86
87} // namespace
88
89int main() {
90 bool ok = true;
91
92 // The pure surface: build one effective-ACE list ONCE, then ask it about several `now`s.
93 // Nothing below rebuilds or invalidates it — that is the property being shown.
94 const ace_t issued[] = {{.subject = as_bytes("visitor"),
95 .access_mask = bit(acl_right_t::READ),
96 .expires_ns = kDeadline}};
97 effective_acl_t merged;
98 merged.append_own(issued);
99 const std::span<const std::byte> visitor_bytes = issued[0].subject;
100 const std::vector<std::byte> other = as_bytes("resident");
101
102 check(ok, merged.allows(visitor_bytes, bit(acl_right_t::READ), kDeadline - 1),
103 "one nanosecond before the deadline, the badge works");
104 check(ok, !merged.allows(visitor_bytes, bit(acl_right_t::READ), kDeadline),
105 "AT the deadline it does not — expiry is `expires_ns <= now`, not `<`");
106 check(ok, !merged.allows(visitor_bytes, bit(acl_right_t::READ), kDeadline + 1),
107 "and after it, of course, it does not");
108 check(ok, merged.allows(visitor_bytes, bit(acl_right_t::READ), kDeadline - 1),
109 "the SAME list answers yes again for an earlier `now`: no state changed, nothing "
110 "was invalidated, and nothing had to be");
111
112 // The expired entry is still an entry, so the vertex it guards stays shut.
113 check(ok, !merged.allows(other, bit(acl_right_t::READ), kDeadline - 1),
114 "the resident was never granted anything, and a present ACE closes the vertex");
115 check(ok, !merged.allows(other, bit(acl_right_t::READ), kDeadline + 1),
116 "…and the badge lapsing does not reopen it — this is the trap worth remembering");
117
118 // `0` is not a deadline in the distant past; it means the ACE never expires.
119 const ace_t permanent[] = {
120 {.subject = as_bytes("resident"), .access_mask = bit(acl_right_t::READ)}};
121 effective_acl_t forever;
122 forever.append_own(permanent);
123 check(ok, forever.allows(other, bit(acl_right_t::READ), kDeadline + 1),
124 "expires_ns == 0 means never expires, not expired at the epoch");
125
126 // The same predicate at the real door, where `now` comes from the graph's own clock.
127 graph_t g;
128 g.configure_subject_resolver(caller_is_subject, nullptr);
129 const vertex_handle_t v = g.register_vertex(path_t("/room"), role_t::STORED_VALUE);
130 (void)g.write(v, some_value());
131 const ace_t both[] = {
132 {.subject = as_bytes("live"),
133 .access_mask = bit(acl_right_t::READ),
134 .expires_ns = ~0ULL >> 1},
135 {.subject = as_bytes("stale"), .access_mask = bit(acl_right_t::READ), .expires_ns = 1},
136 };
137 (void)g.write(path_t("/room:acl"), *tr::view::over_bytes(tr::graph::encode_acl(both)));
138 check(ok, g.read(v, "live").has_value(), "a grant expiring in the year 2262 still reads");
139 check(ok, denied(g.read(v, "stale")), "a grant that expired 1 ns after the epoch does not");
140
141 std::printf("one ACE list, four verdicts, zero invalidations — deadline %llu ns\n",
142 static_cast<unsigned long long>(kDeadline));
143 return ok ? 0 : 1;
144}
See also: security-acl module · protocol TLVs · open by default · inheritance · strict ACL parsing.