From 02c5bcdadc2f1eb482de83f298c0a4fbf64147cc Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Mon, 26 Jan 2026 17:44:48 +0100 Subject: [PATCH 1/2] Added allow_non_convergent to pattern_matching promise in edit_line Ticket: ENT-3417 Signed-off-by: Victor Moene --- cf-agent/files_editline.c | 49 +++++++++++++++++++++++++++++++-------- libpromises/mod_files.c | 1 + 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/cf-agent/files_editline.c b/cf-agent/files_editline.c index 1b004eda72..9418419819 100644 --- a/cf-agent/files_editline.c +++ b/cf-agent/files_editline.c @@ -1263,6 +1263,7 @@ static bool ReplacePatterns(EvalContext *ctx, Item *file_start, Item *file_end, assert(a != NULL); assert(pp != NULL); assert(edcontext != NULL); + bool allow_non_convergent = PromiseGetConstraintAsBoolean(ctx, "allow_non_convergent", pp); char line_buff[CF_EXPANDSIZE]; char after[CF_BUFSIZE]; @@ -1330,17 +1331,32 @@ static bool ReplacePatterns(EvalContext *ctx, Item *file_start, Item *file_end, break; } } - + char line_buff_cp[CF_EXPANDSIZE]; if (NotAnchored(pp->promiser) && BlockTextMatch(ctx, pp->promiser, line_buff, &start_off, &end_off)) { - RecordInterruption(ctx, pp, a, - "Promised replacement '%s' on line '%s' for pattern '%s'" - " is not convergent while editing '%s'" - " (regular expression matches the replacement string)", - line_buff, ip->name, pp->promiser, edcontext->filename); - *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED); - PromiseRef(LOG_LEVEL_ERR, pp); - break; + strlcpy(line_buff_cp, line_buff, sizeof(line_buff_cp)); + strlcpy(after, line_buff_cp + end_off, sizeof(after)); + int needed = snprintf(line_buff_cp + start_off, sizeof(line_buff_cp) - start_off, + "%s%s", BufferData(replace), after); + + if (needed >= sizeof(line_buff_cp) - start_off) { + RecordInterruption(ctx, pp, a, "Buffer overflow: replacement string is too large. '%s' in '%s'", + a->column.column_separator, edcontext->filename); + *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED); + break; + } + + if (!allow_non_convergent || (strlen(line_buff) != strlen(line_buff_cp))) + { + RecordInterruption(ctx, pp, a, + "Promised replacement '%s' on line '%s' for pattern '%s'" + " is not convergent while editing '%s'" + " (regular expression matches the replacement string)", + line_buff, ip->name, pp->promiser, edcontext->filename); + *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED); + PromiseRef(LOG_LEVEL_ERR, pp); + break; + } } if (!MakingChanges(ctx, pp, a, result, "replace pattern '%s' in '%s'", pp->promiser, @@ -1366,8 +1382,21 @@ static bool ReplacePatterns(EvalContext *ctx, Item *file_start, Item *file_end, break; } - if (BlockTextMatch(ctx, pp->promiser, ip->name, &start_off, &end_off)) + if (BlockTextMatch(ctx, pp->promiser, ip->name, &start_off, &end_off) && (!allow_non_convergent + || (strlen(line_buff) != strlen(line_buff_cp)))) { + strlcpy(line_buff_cp, line_buff, sizeof(line_buff_cp)); + strlcpy(after, line_buff_cp + end_off, sizeof(after)); + int needed = snprintf(line_buff_cp + start_off, sizeof(line_buff_cp) - start_off, + "%s%s", BufferData(replace), after); + + if (needed >= sizeof(line_buff_cp) - start_off) { + RecordInterruption(ctx, pp, a, "Buffer overflow: replacement string is too large. '%s' in '%s'", + a->column.column_separator, edcontext->filename); + *result = PromiseResultUpdate(*result, PROMISE_RESULT_INTERRUPTED); + break; + } + RecordInterruption(ctx, pp, a, "Promised replacement '%s' for pattern '%s' is not properly convergent while editing '%s'" " (pattern still matches the end-state replacement string '%s', consider use" diff --git a/libpromises/mod_files.c b/libpromises/mod_files.c index 5ee17053ee..74ddf19a23 100644 --- a/libpromises/mod_files.c +++ b/libpromises/mod_files.c @@ -131,6 +131,7 @@ static const ConstraintSyntax CF_COLUMN_BODIES[] = static const ConstraintSyntax CF_REPLACE_BODIES[] = { ConstraintSyntaxNewBody("replace_with", &replace_with_body, "Search-replace pattern", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewBool("allow_non_convergent", "Allow to use non-convergent regular expressions in replace_patterns. Defaults to false", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewNull() }; From dfa347208a853b8d7a4db9c2135ca003828e891e Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Mon, 2 Feb 2026 12:09:24 +0100 Subject: [PATCH 2/2] Added acceptance test for allow_non_convergent option in replace_pattern Signed-off-by: Victor Moene --- .../replace_patterns/allow_non_convergent.cf | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 tests/acceptance/10_files/replace_patterns/allow_non_convergent.cf diff --git a/tests/acceptance/10_files/replace_patterns/allow_non_convergent.cf b/tests/acceptance/10_files/replace_patterns/allow_non_convergent.cf new file mode 100644 index 0000000000..029de67cab --- /dev/null +++ b/tests/acceptance/10_files/replace_patterns/allow_non_convergent.cf @@ -0,0 +1,69 @@ +####################################################### +# +# Replace a pattern using non-convergent regexes +# +####################################################### + +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +###################################################### + +bundle agent init +{ + files: + "/tmp/example.txt" + content => "foo PORT=23 bar"; + +} + +###################################################### + +bundle agent test +{ + files: + "/tmp/example.txt" + edit_line => _regex_replace( "PORT=[0-9]+", "PORT=22" ); +} + +bundle edit_line _regex_replace(find,replace) +{ + replace_patterns: + "$(find)" + replace_with => _value("$(replace)"), + comment => "Search and replace string", + allow_non_convergent => "true"; +} + +body replace_with _value(x) +{ + replace_value => "$(x)"; + occurrences => "all"; +} + +###################################################### + +bundle agent check +{ + vars: + "file_content" + string => readfile( "/tmp/example.txt" , "999" ); + + classes: + "ok" expression => strcmp("$(file_content)", "foo PORT=22 bar"); + + files: + "/tmp/example.txt" + delete => tidy; + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; +} +