listen: disarm the timers when closing a virtual listener
proto_load_step and proto_cron_crontab drive themselves from timers rather than from their fd, and neither had a close callback, so nothing stopped them when the listener was closed. The load generator kept manufacturing packets and handing them to fr_network_send_request() after the network had signalled the workers to close, which the assert added there caught.
Both now close the fd and remove their timer. Of the remaining app_io modules with timers, proto_detail_file, proto_detail_work and proto_ldap_sync_ldap have a close callback that does not obviously disarm anything, and are worth a look.
53bfbf591b updated configure.ac from valgrind.h to valgrind/valgrind.h, but hand-edited the generated configure rather than regenerating it, and typo'd the path as "valgind/valgrind.h". The cache variable name is spelled correctly, so only the check itself was broken.
HAVE_VALGRIND_VALGRIND_H has therefore been undefined since, which means RUNNING_ON_VALGRIND falls back to the 0 stub: dl.c dlcloses modules even under valgrind, losing the symbols of unloaded code, and radiusd -Xxv reports runtime-valgrind as false while running under it.
io: stop queueing requests to workers once the close has been signalled
worker.c asserts that the network adds nothing to a channel after FR_CHANNEL_CLOSE, but nothing enforced it. A packet queued between the signal and the worker acking is discarded by the worker without a reply, so the network never decrements the socket's outstanding count for it, and the socket can then never be freed.
nr->exiting is now set before the workers are signalled rather than after, and fr_network_send_request() - the single funnel all three senders go through - asserts it is clear. The listeners are closed before the signal, so a socket read cannot reach there; anything that does is a path worth seeing a backtrace for, and release builds still just drop the packet.
io: hand back requests still queued when a responder closes its channel
The worker cancelled the requests it had already popped, but anything still sitting in the channel's atomic queue was abandoned: after the ack the channel is gone, so those messages were never marked done, and the ring buffer they came from could not be reclaimed.
fr_channel_responder_discard() drains what is left and marks it done, called just before the ack. It is a safety net rather than a fix, and warns when it finds anything: the network is not supposed to enqueue once it has signalled the close, which is what worker.c's own "Network added messages to channel after sending FR_CHANNEL_CLOSE" assert claims, and a discarded request produces no reply, so the network never decrements its outstanding count for it and the socket cannot be freed regardless.
io: free the network sockets only once the workers have acked the close
A socket's message set backs every request queued to a worker, so freeing the socket while a worker still holds one leaves it reading poisoned memory: it faults in worker_request_bootstrap() on a cd->listen of 0x4242424242424242, which is non-canonical and so presents as a GPF rather than an ordinary SIGSEGV. fr_network_destroy() now closes the sockets and leaves them allocated, and fr_network() frees them after its loop, which already runs until every worker has acked for the same reason.
The reply drain stays where it was, before the workers are signalled, because that side of the ordering is the mirror image: replies live in the workers' message sets, which they free as they exit, so we have to be finished with those first.
io: separate closing a network socket from freeing it
Freeing a socket frees s->ms, but the workers hold messages allocated from it until they ack the channel close, so the descriptor and the memory cannot share a lifetime. network_socket_close() takes the event removal and the close, the destructor asserts it has already run, and the socket keeps the handle fr_event_fd_insert() gave it rather than tracking whether it has one.
The event is now parented off the socket rather than the network, so it cannot outlive the socket that owns the handle even if a path forgets to close. fr_network_socket_dead() closes up front, since nothing writes to a dead socket, and the remaining free sites close immediately beforehand, so the descriptor is released no later than before. fr_network_destroy() is still wrong - it frees the sockets before signalling the workers at all - but it no longer zeroes s->outstanding to hide it.
ci: gate the automatic merge on the DEB, RPM and sanitizer legs too
The merge ran as a job at the end of CI with needs: ci, which only waited for CI itself because needs: cannot reach jobs in another workflow. A red DEB, RPM or sanitizer leg did not stop a developer branch reaching master.
Moved to its own workflow_run workflow instead. That fires once per gating workflow completion rather than once when they have all finished, so it runs several times per commit and does nothing until the last leg lands, which avoids a job sitting in a poll loop holding a runner. It now merges the commit the workflows ran on rather than the branch ref, so a branch that moved on since cannot put an unvalidated commit onto master.
connection: expose is_closed so a closed connection's handle can't be used
The close callback frees the handle, but is_closed lived in the private struct so drivers had no way to tell a live connection from a dead one. A mysql connection dying mid-query aborted on talloc_get_type_abort() in sql_error(), reached from sql_get_map_list_resume() after the trunk had already closed the connection.
rlm_sql_print_error() is the only caller of driver->sql_error, and every driver reads the errors off the handle, so the check goes there once and reports "Connection closed" rather than in each driver.
Alan T. DeKok [Sun, 26 Jul 2026 15:06:38 +0000 (11:06 -0400)]
determine dictionary for virtual modules
by walking through the entire block, not just the top-level ones.
this makes the function more robust for things like redundant
containing load-balance.
multi-server: never publish profiling results from an unclean valgrind exit
Run 30180044334 published results from two suites where valgrind had killed
FreeRADIUS partway through. Callgrind output from a killed run is truncated at
whatever point it died, so the numbers are not comparable with a clean run, and
publishing them poisons the per-suite history the regression gate compares
against.
start_valgrind_profiling.sh now records valgrind's exit status in
valgrind-exit-status alongside the rest of a test's results, and says loudly
when the status means a signal. The status is written for clean runs too, so an
absent file means the wrapper never got that far rather than that the run was
fine.
publish-profiling-results.sh refuses to publish when any test's status is
non-zero, naming each suite and status, and exits non-zero so the leg goes red
rather than passing having uploaded nothing. Trees with no status file at all
publish as before.
The status file is small and travels with the results, so the store keeps the
provenance of every published run.
ci: collect core dumps as artifacts, keep them out of the profiling publish
Most suites run with allow_core_dumps = yes, but nothing in CI ever collected
a core, so the only record of a crash the logs cannot reconstruct was thrown
away with the runner.
scripts/ci/collect-core-dumps.sh stages cores into ci-core-dumps/, preserving
each path below the searched root so the suite that crashed stays identifiable.
The new .github/actions/collect-core-dumps composite action calls the script and
uploads the result, and ci, ci-sanitizers, ci-macos, ci-deb, ci-rpm and
ci-multi-server-tests all use it. Collection runs on success as well as failure:
FreeRADIUS crashing during shutdown does not necessarily fail the suite that
provoked the crash, which is how run 30180044334 reported success while dumping
two 1.5G cores.
publish-profiling-results.sh now drops cores from the store tarball. Those two
cores turned a 6M publish into 35M of mostly-zero pages, the store's synchronous
ingest then ran ~64s, and wproxy's 60s proxy_read_timeout returned a 504 for a
publish that had in fact landed. The multi-server leg passes move: true so a
core leaves prof-results/ and rides one artifact rather than inflating the
prof-results artifact too. Exclusions are always logged with their sizes; a
silently shrunk publish would be worse than a noisy one.
The shared name patterns live in scripts/ci/core-dump-names.inc so the script
that keeps cores and the script that drops them cannot disagree.
Not covered: ci-freebsd, whose VM syncs runner-to-vm only, and the fuzzing leg,
which has its own crash artifacts. Whether a core is written at all still
depends on the ambient RLIMIT_CORE and kernel.core_pattern, neither of which CI
sets; allow_core_dumps only restores the limit FreeRADIUS inherited.
build: categorise library targets via TGT_CATEGORY
Library-producing makefile fragments declare a category next to
TARGET: lib-protocol for the wire codecs under src/protocols, and
lib-util for the other support libraries. Modules, listeners and
binaries stay uncategorised for now.
One declaration per fragment, placed after the last TARGET
assignment or probe include and outside any conditional, so the
fragment's own value wins over one dragged in by textually including
another fragment (the configure-probe pattern used by rlm_json and
friends). Fragments which probe but aren't libraries keep a bare
'TGT_CATEGORY :=' reset for the same reason. Configure-generated
fragments carry the declaration in their all.mk.in template.
boiler.mk only gains generic plumbing: the TGT_CATEGORY passthrough
and a CATEGORY_TARGETS macro which expands to all targets in the
given categories.
'make library.list' prints '<category> <name>' pairs - the first
authoritative list of the libraries the build produces, usable to
keep the debian/redhat packaging manifests honest. Census:
14 lib-protocol, 18 lib-util.
src/stats/all.mk now derives the radict chain-load dependencies from
CATEGORY_TARGETS instead of name-prefix filtering ALL_TGTS.
build: stats generation depends on the dhcpv4/dhcpv6 libraries
radict chain-loads the DHCPv4 and DHCPv6 dictionary validation
libraries while parsing the RADIUS dictionaries (dictionary.rfc9445
has ref=@DHCPv4 / ref=@DHCPv6), but the DICT_STATS rules only
depended on libfreeradius-radius and radict itself, so a parallel
build could run radict before those libraries were linked and fail
with "cannot open shared object file", as seen intermittently on the
rocky10 service image build.
radiusd: only SIGTERM the process group when we lead it
The shutdown group-wide SIGTERM exists to reap children left over
from exec calls and triggers. When radiusd is daemonized (setsid),
or given its own group by systemd or a container runtime, the group
contains only radiusd and its children and the signal is safe. Run
in the foreground from a shell, the group belongs to the shell, and
the signal killed the shell and its terminal too.
multi-server: drive proto_load with max_requests / on_complete
The listen configs previously set 'num_messages' inside step { },
which nothing parsed, and relied on the wall-clock ramp to produce
the expected packet count - a few skipped timer slots on a loaded CI
runner meant fewer packets than the consumers expected, and flaky
kafka-produce failures.
Point the new max_requests knob at the per-test count and rename the
env var TEST_LOADGEN_NUM_MESSAGES to TEST_LOADGEN_MAX_REQUESTS to
match. on_complete = stop keeps the servers running for the rest of
each test.
proto_load: replace 'repeat' with 'on_complete', add 'max_requests'
The load test is now complete when the PPS ramp has passed max_pps,
max_requests packets (if set) have been sent, and every reply has
arrived. With max_requests set the generator holds the rate at
max_pps after the ramp instead of stopping, so tests get an exact,
predictable packet count even when a busy machine makes the timer
skip send slots.
on_complete = (exit|repeat|stop|continue) replaces the undocumented
'repeat' knob and controls what happens on completion. The default
'exit' terminates the server. 'continue' holds at max_pps forever
and cannot be combined with max_requests.
Completion is also now signalled when the final reply arrives before
the generator stops sending - previously that race left the listener
waiting forever.
scripts/docker: snapshot dnf mirrorlists at image build time
The mirrors.rockylinux.org / mirrors.fedoraproject.org redirectors
occasionally answer with an empty mirror set, which dnf reports as "No
URLs in mirrorlist" and treats as fatal rather than retryable, so each
mirror list is now fetched once at image build time into
/etc/dnf/mirrorlists/<repo> and the enabled repos point at the
snapshot with mirrorlist=file:// (explicitly supported by librepo,
mirrors tried in file order). The origin CDN leads each snapshot so
the fetched mirrors are failover only, minor-versioned paths are
rewritten to the major-version symlink form so a snapshot survives
point releases, and the unused metalink-only epel-cisco-openh264 repo
is disabled.
policy: filter_username rejects User-Names containing an xlat opener
Reject the '%{' and '%name(' expansion openers in User-Name so there's no chance of poorly written API code reflecting them back and having nested evaluation occur.
This is a belt and braces change, as expansion defaults to disabled in v4 for the rest module, and it's somewhat unlikely that an admin woul insert one of these raw values into an SQL policy. It's probably unecessary but at least it quiets the CVE farmers.
rlm_rest: tests for the response do_xlat and is_json defaults
Covers the JSON decoder (default off, do_xlat = yes, per-attribute
overrides in both directions, nested JSON with and without is_json)
and the POST decoder (default off, do_xlat = yes).
The mock API gains a /user/<username>/expand/ JSON endpoint and a
/post/expand reflector, both returning values containing xlat
expansions so the tests can verify whether the server expanded them.
raddb: document rlm_rest response json {} and post {} subsections
The response returned by a REST API is a policy evaluated by the
server. Expansion of values in responses is now disabled by default,
and should only be enabled where the API validates or escapes
untrusted input echoed back in the policy it returns.
Notes that configurations migrated from v3, where expansion was
enabled by default, must set do_xlat = yes if they rely on it.
rlm_rest: add response json {} and post {} config subsections
json { do_xlat, is_json } sets the defaults for the matching
per-attribute flags in JSON responses. post { do_xlat } controls
xlat expansion of values in POST responses, which was previously
unconditional.
Unlike v3, do_xlat defaults to no. The REST API must opt in to
expansion, either per decoder in the config, or with the
per-attribute do_xlat flag, which overrides the configured default
in both directions.
docs-v4: remove duplicate dictionary info from the dictionary tutorial. `xrefs` used in tutorial to guide user to reference files including the relevant configuration files.
The RDN is expanded at request time and joined to the front of every profile DN, selecting a child object of each profile. An expansion producing nothing leaves the profile DNs unmodified.
fr_ldap_map_getvalue now drains a value iterator instead of a duplicated berval array, and the map, check and valuepair readers iterate values straight out of the result message. The map proc's synthetic dn result is replaced by the exported fr_ldap_map_getdn callback, removing fr_ldap_result_t.
lib/ldap: add fr_ldap_value_iter_t for in place value iteration
The iterator is now the single implementation of the entry walk, with fr_ldap_result_values_len, fr_ldap_str_list_afrom_result and fr_ldap_entry_value_find rewritten on top of it. Group memberships are also iterated in place, removing the duplication of every membership value during collection.
rlm_ldap: merge profile DN sources into one pooled allocation
fr_ldap_str_list_afrom_result gains leading slots the caller fills with borrowed strings, so the default, group and user profile DNs merge into a single pooled array with the user values read in place from the result message. The now unused fr_ldap_berval_to_string_list is removed.
lib/ldap: add fr_ldap_entry_value_find for in place value access
Returns the first value of an attribute referenced in place from the result message, replacing ldap_get_values_len at call sites which only read a single value. Each converted site loses several allocations and a duplicate of the value bytes.
rlm_ldap: read profile references from user and group objects
Group object DNs are collected during membership resolution, then the profile bearing groups are fetched in one search with the profile attribute presence asserted in the filter, and their profiles apply between the default and the user object's. The profile attributes are now configured per object type as user { profile_attribute } and group { profile_attribute }, applied independently, and the profile { attribute } pair is deprecated.
lib/ldap: add functions for copying result values into a string list
fr_ldap_result_values_len sums an attribute's values across every entry of a result, so fr_ldap_str_list_afrom_result can build an exactly sized string list from one pooled allocation. Values are referenced in place from the result message, the only copies made are the strings in the list.
The list tracks an append cursor and the slot reserved for the NULL terminator, so appends cost no scans and batches cost at most one reallocation. Where talloc_pooled_object is available the list, its pointer array and every appended string come from a single talloc pool.
%ldap.whoami() runs the RFC 4532 Who am I? extended operation on the module's admin connections and returns the resolved authorization identity, stripping any dn: prefix.
lib/ldap: run directory discovery during connection establishment
The rootDSE query previously ran as an ordinary trunk request, so the first requests on a fresh trunk could run before discovery results were parsed. Connections now perform discovery in a new DISCOVER state before signalling connected, and trunks always start at least one connection so referral trunk watches still fire.
rlm_ldap: add tests covering the profile search_mode matrix
Fixture profiles carry priorities running opposite to their list order so seq and bulk application order differ observably. Zero result and default-only paths are covered for both modes.
seq keeps one search per profile DN, applied in list order with the default profile first. bulk retrieves every profile with a single sorted search matching objects by DN, requiring base scope and sort_by, and auto picks bulk when both hold, otherwise seq.
lib/ldap: add helpers for retrieving sets of objects by DN
fr_ldap_directory_common_base_find resolves DNs to a covering naming context using a hash table built when the rootDSE is parsed, and fr_ldap_filter_afrom_dn_list builds the matching filter using the directory's dn_attr, which fr_ldap_directory_alloc now defaults to entryDN before discovery completes. fr_ldap_berval_to_string_list converts berval values to a NULL terminated string list carved from a single talloc pool.
Names the attribute used to match an entry's own DN in a search filter, defaulting per detected directory type (distinguishedName on Active Directory and Samba, RFC 5020 entryDN elsewhere).
lib/ldap: expose connection state machine callbacks
Callers now pass fr_ldap_connection_init, fr_ldap_trunk_connection_init and fr_ldap_connection_close directly to connection_alloc(), replacing fr_ldap_connection_state_alloc(). Trunk connections share the ttrunk's fr_ldap_directory_t, which is now allocated with the trunk so the pointer is valid when the initial connections spawn.
rlm_krb5: assume thread-safety when cross-compiling (#5903)
When cross-compiling, the autoconf test program for krb5_is_thread_safe()
cannot be run on the host. Previously this left krb5threadsafe empty,
which is incorrect for all modern krb5 builds.
Fix by defaulting to thread-safety when the test cannot be performed.
Signed-off-by: Alexandru Ardelean <alex@shruggie.ro>
cf_section_dup: descend into child sections when copying (#5904)
The iterative rewrite in 013911166c allocated a copy of each nested
section but never walked into it, so pairs and subsections inside a
duplicated section were silently dropped. Add the missing descent,
and a regression test that duplicates a section with a nested
subsection containing multiple pairs.
eap_aka_sim: fix SIM-SQN check that rejected every value (#5905)
vector_umts_from_quintuplets() validated control.SIM-SQN by comparing
vp_length against MILENAGE_SQN_SIZE. SIM-SQN is FR_TYPE_UINT64, and
vp_length is only meaningful for variable length types -- it is 0 for
numeric boxes -- so the comparison could never succeed:
Every request supplying control.SIM-SQN alongside quintuplets was
rejected. Nothing else treats the attribute as octets:
vector_umts_from_ki() reads it as vp_uint64 with no length check, and the
SQN/AK derivations further down vector_umts_from_quintuplets() read
vp_uint64 as well.
Replace the length check with a range check. SQN is 48 bits and
uint48_to_buff() silently truncates anything wider, so reject those
values rather than derive AK from a mangled SQN.
The path is reachable when the server has no local Ki and takes
quintuplets from an external source, such as vectors fetched from a 3GPP
HSS over SWx. That is also the only way to satisfy the control.SIM-SQN
requirement RESUME(recv_aka_synchronization_failure) imposes after a
peer sends AKA-Synchronization-Failure, so SQN resync could not complete.
json: skip json_object_put leak assert on json-c >= 0.19
json-c 0.19 returns 0 from json_object_put() for freed scalars and empty containers, so the return value can't identify leaked references. Fix proposed upstream in json-c/json-c#945.