This is an automated email from the ASF dual-hosted git repository.
dmeden pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new a39c1ae39f Fix nullptr crash in RecConfigOverrideFromEnvironment with
runroot (#12917)
a39c1ae39f is described below
commit a39c1ae39f775a863c9a74ebc530884c7122258b
Author: Damian Meden <[email protected]>
AuthorDate: Tue Mar 10 11:43:30 2026 +0100
Fix nullptr crash in RecConfigOverrideFromEnvironment with runroot (#12917)
* RecConfigOverrideFromEnvironment: return resolved value and override
source
Change the return type to std::pair<std::string, RecConfigOverrideSource>
so callers know whether a record was overridden and by whom (ENV or
RUNROOT).
When runroot is active, return the actual Layout path (e.g. Layout::bindir)
instead of the old nullptr which caused undefined behaviour when converted
to std::string. Replace the RecConfigOverrideFromRunroot() strcmp chain
with a constexpr lookup table mapping record names to Layout members.
All three callers now log the override source at debug level. Preserve
nullptr for built-in defaults that are intentionally unset.
Adds records_runroot_precedence autest.
---
include/records/RecCore.h | 33 ++++-
src/records/RecConfigParse.cc | 54 ++++---
src/records/RecYAMLDecoder.cc | 9 +-
src/records/RecordsConfigUtils.cc | 17 ++-
.../records/records_runroot_precedence.test.py | 163 +++++++++++++++++++++
5 files changed, 242 insertions(+), 34 deletions(-)
diff --git a/include/records/RecCore.h b/include/records/RecCore.h
index d0a6ed5b4f..a355bd39cc 100644
--- a/include/records/RecCore.h
+++ b/include/records/RecCore.h
@@ -25,6 +25,8 @@
#include <functional>
#include <optional>
+#include <string>
+#include <utility>
#include "tscore/Diags.h"
@@ -69,9 +71,34 @@ std::string RecConfigReadConfigPath(const char
*file_variable, const char *defau
// Return a copy of the persistent stats file. This is
$RUNTIMEDIR/records.snap.
std::string RecConfigReadPersistentStatsPath();
-// Test whether the named configuration value is overridden by an environment
variable. Return either
-// the overridden value, or the original value. Caller MUST NOT free the
result.
-const char *RecConfigOverrideFromEnvironment(const char *name, const char
*value);
+/// Indicates why RecConfigOverrideFromEnvironment() chose its returned value.
+enum class RecConfigOverrideSource {
+ NONE, ///< No override — the original value was kept.
+ ENV, ///< Overridden by a PROXY_CONFIG_* environment variable.
+ RUNROOT, ///< Overridden with the resolved Layout path because runroot
manages this record.
+};
+
+/// Label for the override source (for logging).
+constexpr const char *
+RecConfigOverrideSourceName(RecConfigOverrideSource src)
+{
+ switch (src) {
+ case RecConfigOverrideSource::ENV:
+ return "environment variable";
+ case RecConfigOverrideSource::RUNROOT:
+ return "runroot";
+ case RecConfigOverrideSource::NONE:
+ return "none";
+ default:
+ return "unknown";
+ }
+}
+
+// Test whether the named configuration value is overridden by the execution
+// environment — either a PROXY_CONFIG_* environment variable or the runroot
+// mechanism. Returns the resolved value together with the source that
+// produced it.
+std::pair<std::string, RecConfigOverrideSource>
RecConfigOverrideFromEnvironment(const char *name, const char *value);
//-------------------------------------------------------------------------
// Stat Registration
diff --git a/src/records/RecConfigParse.cc b/src/records/RecConfigParse.cc
index 0819137f7d..d80a1237af 100644
--- a/src/records/RecConfigParse.cc
+++ b/src/records/RecConfigParse.cc
@@ -84,24 +84,20 @@ RecFileImport_Xmalloc(const char *file, char **file_buf,
int *file_size)
}
//-------------------------------------------------------------------------
-// RecConfigOverrideFromRunroot
+// Records whose paths are managed by runroot. When runroot is active the
+// value from records.yaml is replaced with the resolved Layout path.
//-------------------------------------------------------------------------
-bool
-RecConfigOverrideFromRunroot(const char *name)
-{
- if (!get_runroot().empty()) {
- if (!strcmp(name, "proxy.config.bin_path") || !strcmp(name,
"proxy.config.local_state_dir") ||
- !strcmp(name, "proxy.config.log.logfile_dir") || !strcmp(name,
"proxy.config.plugin.plugin_dir")) {
- return true;
- }
- }
- return false;
-}
+static constexpr std::pair<std::string_view, std::string Layout::*>
runroot_records[] = {
+ {"proxy.config.bin_path", &Layout::bindir },
+ {"proxy.config.local_state_dir", &Layout::runtimedir},
+ {"proxy.config.log.logfile_dir", &Layout::logdir },
+ {"proxy.config.plugin.plugin_dir", &Layout::libexecdir},
+};
//-------------------------------------------------------------------------
// RecConfigOverrideFromEnvironment
//-------------------------------------------------------------------------
-const char *
+std::pair<std::string, RecConfigOverrideSource>
RecConfigOverrideFromEnvironment(const char *name, const char *value)
{
ats_scoped_str envname(ats_strdup(name));
@@ -121,12 +117,18 @@ RecConfigOverrideFromEnvironment(const char *name, const
char *value)
envval = getenv(envname.get());
if (envval) {
- return envval;
- } else if (RecConfigOverrideFromRunroot(name)) {
- return nullptr;
+ return {envval, RecConfigOverrideSource::ENV};
+ }
+
+ if (!get_runroot().empty()) {
+ for (auto const &[rec_name, member] : runroot_records) {
+ if (rec_name == name) {
+ return {Layout::get()->*member, RecConfigOverrideSource::RUNROOT};
+ }
+ }
}
- return value;
+ return {value ? value : "", RecConfigOverrideSource::NONE};
}
//-------------------------------------------------------------------------
@@ -141,10 +143,9 @@ RecConfigFileParse(const char *path,
RecConfigEntryCallback handler)
const char *line;
int line_num;
- char *rec_type_str, *name_str, *data_type_str, *data_str;
- const char *value_str;
- RecT rec_type;
- RecDataT data_type;
+ char *rec_type_str, *name_str, *data_type_str, *data_str;
+ RecT rec_type;
+ RecDataT data_type;
Tokenizer line_tok("\r\n");
tok_iter_state line_tok_state;
@@ -245,8 +246,15 @@ RecConfigFileParse(const char *path,
RecConfigEntryCallback handler)
}
// OK, we parsed the record, send it to the handler ...
- value_str = RecConfigOverrideFromEnvironment(name_str, data_str);
- handler(rec_type, data_type, name_str, value_str, value_str == data_str ?
REC_SOURCE_EXPLICIT : REC_SOURCE_ENV);
+ {
+ auto [value_str, override_source] =
RecConfigOverrideFromEnvironment(name_str, data_str);
+ if (override_source != RecConfigOverrideSource::NONE) {
+ RecDebug(DL_Debug, "'%s' overridden with '%s' by %s", name_str,
value_str.c_str(),
+ RecConfigOverrideSourceName(override_source));
+ }
+ handler(rec_type, data_type, name_str, value_str.c_str(),
+ override_source == RecConfigOverrideSource::NONE ?
REC_SOURCE_EXPLICIT : REC_SOURCE_ENV);
+ }
// update our g_rec_config_contents_xxx
g_rec_config_contents_ht.emplace(name_str);
diff --git a/src/records/RecYAMLDecoder.cc b/src/records/RecYAMLDecoder.cc
index 29f2f12fa9..1bb7b9e45e 100644
--- a/src/records/RecYAMLDecoder.cc
+++ b/src/records/RecYAMLDecoder.cc
@@ -156,11 +156,12 @@ SetRecordFromYAMLNode(CfgNode const &field, swoc::Errata
&errata)
std::string field_value = field.value_node.as<std::string>(); // in case of
a string, the library will give us the literal
// 'null'
which is exactly what we want.
- std::string value_str =
RecConfigOverrideFromEnvironment(record_name.c_str(), field_value.c_str());
- RecSourceT source = (field_value == value_str ? REC_SOURCE_EXPLICIT :
REC_SOURCE_ENV);
+ auto [value_str, override_source] =
RecConfigOverrideFromEnvironment(record_name.c_str(), field_value.c_str());
+ RecSourceT source = (override_source ==
RecConfigOverrideSource::NONE) ? REC_SOURCE_EXPLICIT : REC_SOURCE_ENV;
- if (source == REC_SOURCE_ENV) {
- errata.note(ERRATA_DEBUG, "'{}' was override with '{}' using an env
variable", record_name, value_str);
+ if (override_source != RecConfigOverrideSource::NONE) {
+ errata.note(ERRATA_DEBUG, "'{}' overridden with '{}' by {}", record_name,
value_str,
+ RecConfigOverrideSourceName(override_source));
}
if (!check_expr.empty() && RecordValidityCheck(value_str.c_str(),
check_type, check_expr.c_str()) == false) {
diff --git a/src/records/RecordsConfigUtils.cc
b/src/records/RecordsConfigUtils.cc
index f572a95749..9d24451061 100644
--- a/src/records/RecordsConfigUtils.cc
+++ b/src/records/RecordsConfigUtils.cc
@@ -49,9 +49,14 @@ initialize_record(const RecordElement *record, void *)
access = record->access;
if (REC_TYPE_IS_CONFIG(type)) {
- const char *value = RecConfigOverrideFromEnvironment(record->name,
record->value);
- RecData data = {0};
- RecSourceT source = value == record->value ? REC_SOURCE_DEFAULT :
REC_SOURCE_ENV;
+ auto [value, override_source] =
RecConfigOverrideFromEnvironment(record->name, record->value);
+ RecData data = {0};
+ RecSourceT source = (override_source ==
RecConfigOverrideSource::NONE) ? REC_SOURCE_DEFAULT : REC_SOURCE_ENV;
+
+ if (override_source != RecConfigOverrideSource::NONE) {
+ RecDebug(DL_Debug, "'%s' overridden with '%s' by %s", record->name,
value.c_str(),
+ RecConfigOverrideSourceName(override_source));
+ }
// If you specify a consistency check, you have to specify a regex
expression. We abort here
// so that this breaks QA completely.
@@ -59,7 +64,11 @@ initialize_record(const RecordElement *record, void *)
ink_fatal("%s has a consistency check but no regular expression",
record->name);
}
- RecDataSetFromString(record->value_type, &data, value);
+ // When the built-in default is nullptr and no override was applied,
preserve
+ // nullptr so optional records (e.g. keylog_file, groups_list) stay unset.
+ const char *value_ptr =
+ (override_source == RecConfigOverrideSource::NONE && record->value ==
nullptr) ? nullptr : value.c_str();
+ RecDataSetFromString(record->value_type, &data, value_ptr);
RecErrT reg_status{REC_ERR_FAIL};
switch (record->value_type) {
case RECD_INT:
diff --git a/tests/gold_tests/records/records_runroot_precedence.test.py
b/tests/gold_tests/records/records_runroot_precedence.test.py
new file mode 100644
index 0000000000..49077c174c
--- /dev/null
+++ b/tests/gold_tests/records/records_runroot_precedence.test.py
@@ -0,0 +1,163 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+
+Test.Summary = '''
+Verify that when runroot is active, path records from records.yaml are
+overridden by the resolved Layout paths (precedence: env var > runroot >
records.yaml).
+
+When --run-root (or TS_RUNROOT) is set and the PROXY_CONFIG_* environment
+variables for path records are unset, RecConfigOverrideFromEnvironment()
+returns the actual Layout path (e.g. Layout::bindir, Layout::logdir) which
+was populated from runroot.yaml — effectively making runroot.yaml override
+records.yaml for these path records.
+'''
+Test.ContinueOnFail = True
+Test.SkipUnless(
+ Test.Variables.BINDIR.startswith(Test.Variables.PREFIX), "need to
guarantee bin path starts with prefix for runroot")
+
+ts = Test.MakeATSProcess("ts")
+ts_dir = os.path.join(Test.RunDirectory, "ts")
+
+# Set deliberately WRONG values in records.yaml for all 4 runroot-managed
+# path records. If runroot override works, these values must NOT be used.
+ts.Disk.records_config.append_to_document(
+ '''
+ bin_path: wrong_bin_path
+ local_state_dir: wrong_runtime
+ log:
+ logfile_dir: wrong_log
+ plugin:
+ plugin_dir: wrong_plugin
+''')
+
+# Test 3 setup: env var that must win over both runroot and records.yaml.
+ts.Env['PROXY_CONFIG_DIAGS_DEBUG_TAGS'] = 'env_wins'
+ts.Disk.records_config.update('''
+ diags:
+ debug:
+ enabled: 0
+ tags: config_value
+ ''')
+
+# Build the ATS command:
+# - Unset the 4 path env vars (the test framework always sets them,
+# which masks the runroot code path).
+# - Set TS_RUNROOT to the sandbox dir so the runroot mechanism activates.
+original_cmd = ts.Command
+ts.Command = (
+ "env"
+ " -u PROXY_CONFIG_BIN_PATH"
+ " -u PROXY_CONFIG_LOCAL_STATE_DIR"
+ " -u PROXY_CONFIG_LOG_LOGFILE_DIR"
+ " -u PROXY_CONFIG_PLUGIN_PLUGIN_DIR"
+ f" TS_RUNROOT={ts_dir}"
+ f" {original_cmd}")
+
+# ---------------------------------------------------------------------------
+# Test 0: Create runroot.yaml that maps to the sandbox layout, then start ATS.
+#
+# The runroot.yaml must exist before ATS starts because TS_RUNROOT triggers
+# Layout::runroot_setup() during initialization. We write a runroot.yaml
+# whose paths match the sandbox structure the test framework already created
+# (traffic_layout init would create a different FHS-style layout that does
+# not match the sandbox, so we write it manually).
+# ---------------------------------------------------------------------------
+runroot_yaml = os.path.join(ts_dir, 'runroot.yaml')
+
+runroot_lines = [
+ f"prefix: {ts_dir}",
+ f"bindir: {os.path.join(ts_dir, 'bin')}",
+ f"sbindir: {os.path.join(ts_dir, 'bin')}",
+ f"sysconfdir: {os.path.join(ts_dir, 'config')}",
+ f"logdir: {os.path.join(ts_dir, 'log')}",
+ f"libexecdir: {os.path.join(ts_dir, 'plugin')}",
+ f"localstatedir: {os.path.join(ts_dir, 'runtime')}",
+ f"runtimedir: {os.path.join(ts_dir, 'runtime')}",
+ f"cachedir: {os.path.join(ts_dir, 'cache')}",
+]
+runroot_content = "\\n".join(runroot_lines) + "\\n"
+
+tr = Test.AddTestRun("Create runroot.yaml")
+tr.Processes.Default.Command = f"mkdir -p {ts_dir} && printf
'{runroot_content}' > {runroot_yaml}"
+tr.Processes.Default.ReturnCode = 0
+
+# ---------------------------------------------------------------------------
+# Test 1: Start ATS with runroot active
+# ---------------------------------------------------------------------------
+tr = Test.AddTestRun("Start ATS with runroot")
+tr.Processes.Default.Command = 'echo start'
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.StartBefore(ts)
+tr.StillRunningAfter = ts
+
+# ATS must not crash (the original nullptr bug) and must complete startup.
+ts.Disk.traffic_out.Content = Testers.ExcludesExpression(
+ "basic_string", "must not crash with 'basic_string: construction from
null'")
+ts.Disk.traffic_out.Content += Testers.ContainsExpression("records parsing
completed", "ATS should complete records parsing")
+
+# Verify the override log messages appear in traffic.out.
+# The errata notes from RecYAMLDecoder are printed by RecCoreInit().
+ts.Disk.traffic_out.Content += Testers.ContainsExpression(
+ "'proxy.config.bin_path' overridden with .* by runroot", "bin_path
override by runroot must be logged")
+ts.Disk.traffic_out.Content += Testers.ContainsExpression(
+ "'proxy.config.local_state_dir' overridden with .* by runroot",
"local_state_dir override by runroot must be logged")
+ts.Disk.traffic_out.Content += Testers.ContainsExpression(
+ "'proxy.config.log.logfile_dir' overridden with .* by runroot",
"logfile_dir override by runroot must be logged")
+ts.Disk.traffic_out.Content += Testers.ContainsExpression(
+ "'proxy.config.plugin.plugin_dir' overridden with .* by runroot",
"plugin_dir override by runroot must be logged")
+ts.Disk.traffic_out.Content += Testers.ContainsExpression(
+ "'proxy.config.diags.debug.tags' overridden with 'env_wins' by environment
variable",
+ "diags.debug.tags override by environment variable must be logged")
+
+# ---------------------------------------------------------------------------
+# Test 2: Verify path records do NOT contain the records.yaml values.
+#
+# Because runroot is active and env vars are unset, the records should hold
+# the resolved Layout paths from runroot.yaml, not the records.yaml values.
+# ---------------------------------------------------------------------------
+tr = Test.AddTestRun("Verify runroot overrides records.yaml for path records")
+tr.Processes.Default.Command = (
+ 'traffic_ctl config get'
+ ' proxy.config.bin_path'
+ ' proxy.config.local_state_dir'
+ ' proxy.config.log.logfile_dir'
+ ' proxy.config.plugin.plugin_dir')
+tr.Processes.Default.Env = ts.Env
+tr.Processes.Default.ReturnCode = 0
+tr.StillRunningAfter = ts
+
+# The deliberately wrong records.yaml values must NOT appear.
+tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression(
+ 'wrong_bin_path', 'bin_path must be overridden by runroot, not
records.yaml')
+tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression(
+ 'wrong_runtime', 'local_state_dir must be overridden by runroot, not
records.yaml')
+tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression(
+ 'wrong_log', 'logfile_dir must be overridden by runroot, not records.yaml')
+tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression(
+ 'wrong_plugin', 'plugin_dir must be overridden by runroot, not
records.yaml')
+
+# ---------------------------------------------------------------------------
+# Test 3: Verify env vars still take highest precedence over runroot.
+# ---------------------------------------------------------------------------
+tr = Test.AddTestRun("Verify env var overrides both runroot and records.yaml")
+tr.Processes.Default.Command = 'traffic_ctl config get
proxy.config.diags.debug.tags'
+tr.Processes.Default.Env = ts.Env
+tr.Processes.Default.ReturnCode = 0
+tr.StillRunningAfter = ts
+tr.Processes.Default.Streams.stdout = Testers.ContainsExpression(
+ 'proxy.config.diags.debug.tags: env_wins', 'Env var must override both
runroot and records.yaml')