From a77d91707e45b55580f8965f41a3535d25295219 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Mon, 17 Aug 2026 12:02:08 +0200 Subject: [PATCH] Fixed process_select and file_select time ranges on Windows ttime_range, stime_range, ctime, atime and mtime store their bounds in time_t, but parsed them through a (long *) cast. On Windows, long is narrower than time_t. Hence, only the lower half of each bound was written and the rest kept whatever garbage value the struct initializer left there. This caused the bounds to come out as large negative numbers and ttime_range matched no processes at all. Changelog: Title Signed-off-by: Lars Erik Wik Co-Authored-By: Claude Opus 5 (1M context) --- configure.ac | 5 +++ libpromises/attributes.c | 12 +++--- libpromises/conversion.c | 56 +++++++++++++++++++++++++--- libpromises/conversion.h | 1 + tests/unit/conversion_test.c | 71 ++++++++++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 11 deletions(-) diff --git a/configure.ac b/configure.ac index 9dad073aa9..d68120c2a3 100644 --- a/configure.ac +++ b/configure.ac @@ -954,6 +954,11 @@ AC_CHECK_DECLS(fgetgrent, [], [], [[#include ]]) AC_CHECK_DECLS(isfinite, [], [], [[#include ]]) +AC_CHECK_DECL([SCNd64], [], + [AC_MSG_ERROR(Cannot find SCNd64 in inttypes.h)], + [[#include ]] + ) + AC_CHECK_FUNCS(getpwent setpwent endpwent) AC_CHECK_FUNCS(fgetspent lckpwdf ulckpwdf) diff --git a/libpromises/attributes.c b/libpromises/attributes.c index 298e42d403..824376ddac 100644 --- a/libpromises/attributes.c +++ b/libpromises/attributes.c @@ -563,7 +563,7 @@ FileSelect GetSelectConstraints(const EvalContext *ctx, const Promise *pp) entries++; } - if (!IntegerRangeFromString(value, (long *) &s.min_size, (long *) &s.max_size)) + if (!IntegerRangeFromString(value, &s.min_size, &s.max_size)) { PromiseRef(LOG_LEVEL_ERR, pp); FatalError(ctx, "Could not make sense of integer range [%s]", value); @@ -576,7 +576,7 @@ FileSelect GetSelectConstraints(const EvalContext *ctx, const Promise *pp) entries++; } - if (!IntegerRangeFromString(value, (long *) &s.min_ctime, (long *) &s.max_ctime)) + if (!TimeRangeFromString(value, &s.min_ctime, &s.max_ctime)) { PromiseRef(LOG_LEVEL_ERR, pp); FatalError(ctx, "Could not make sense of integer range [%s]", value); @@ -589,7 +589,7 @@ FileSelect GetSelectConstraints(const EvalContext *ctx, const Promise *pp) entries++; } - if (!IntegerRangeFromString(value, (long *) &s.min_atime, (long *) &s.max_atime)) + if (!TimeRangeFromString(value, &s.min_atime, &s.max_atime)) { PromiseRef(LOG_LEVEL_ERR, pp); FatalError(ctx, "Could not make sense of integer range [%s]", value); @@ -602,7 +602,7 @@ FileSelect GetSelectConstraints(const EvalContext *ctx, const Promise *pp) entries++; } - if (!IntegerRangeFromString(value, (long *) &s.min_mtime, (long *) &s.max_mtime)) + if (!TimeRangeFromString(value, &s.min_mtime, &s.max_mtime)) { PromiseRef(LOG_LEVEL_ERR, pp); FatalError(ctx, "Could not make sense of integer range [%s]", value); @@ -1410,7 +1410,7 @@ ProcessSelect GetProcessFilterConstraints(const EvalContext *ctx, const Promise // get constraint cumulated CPU time value = PromiseGetConstraintAsRval(pp, "ttime_range", RVAL_TYPE_SCALAR); - if (!IntegerRangeFromString(value, (long *) &p.min_ttime, (long *) &p.max_ttime)) + if (!TimeRangeFromString(value, &p.min_ttime, &p.max_ttime)) { PromiseRef(LOG_LEVEL_ERR, pp); FatalError(ctx, "Could not make sense of integer range [%s]", value); @@ -1418,7 +1418,7 @@ ProcessSelect GetProcessFilterConstraints(const EvalContext *ctx, const Promise // get constraint start time value = PromiseGetConstraintAsRval(pp, "stime_range", RVAL_TYPE_SCALAR); - if (!IntegerRangeFromString(value, (long *) &p.min_stime, (long *) &p.max_stime)) + if (!TimeRangeFromString(value, &p.min_stime, &p.max_stime)) { PromiseRef(LOG_LEVEL_ERR, pp); FatalError(ctx, "Could not make sense of integer range [%s]", value); diff --git a/libpromises/conversion.c b/libpromises/conversion.c index 03dc17b3ab..be4d9e52bd 100644 --- a/libpromises/conversion.c +++ b/libpromises/conversion.c @@ -585,12 +585,18 @@ bool DoubleFromString(const char *s, double *value_out) /****************************************************************************/ /** + * Parse a "min,max" range into 64-bit bounds. + * + * Shared implementation behind IntegerRangeFromString() and + * TimeRangeFromString(). Parsing is always done in 64 bits so that the typed + * wrappers, not this function, decide how wide the caller's storage is. + * * @return true if successful */ -bool IntegerRangeFromString(const char *intrange, long *min_out, long *max_out) +static bool Int64RangeFromString(const char *intrange, int64_t *min_out, int64_t *max_out) { Item *split; - long lmax = CF_LOWINIT, lmin = CF_HIGHINIT; + int64_t lmax = CF_LOWINIT, lmin = CF_HIGHINIT; /* Numeric types are registered by range separated by comma str "min,max" */ @@ -603,15 +609,15 @@ bool IntegerRangeFromString(const char *intrange, long *min_out, long *max_out) split = SplitString(intrange, ','); - sscanf(split->name, "%ld", &lmin); + sscanf(split->name, "%" SCNd64, &lmin); if (strcmp(split->next->name, "inf") == 0) { - lmax = (long) CF_INFINITY; + lmax = (int64_t) CF_INFINITY; } else { - sscanf(split->next->name, "%ld", &lmax); + sscanf(split->next->name, "%" SCNd64, &lmax); } DeleteItemList(split); @@ -626,6 +632,46 @@ bool IntegerRangeFromString(const char *intrange, long *min_out, long *max_out) return true; } +/** + * @return true if successful + */ +bool IntegerRangeFromString(const char *intrange, long *min_out, long *max_out) +{ + int64_t min, max; + + if (!Int64RangeFromString(intrange, &min, &max)) + { + return false; + } + + *min_out = min; + *max_out = max; + return true; +} + +/** + * Like IntegerRangeFromString(), but for bounds stored as time_t. + * + * A separate entry point is needed because time_t is wider than long on LLP64 + * platforms (Windows). Hence, writing a bound through a long * there would + * leave half of the caller's time_t untouched. + * + * @return true if successful + */ +bool TimeRangeFromString(const char *timerange, time_t *min_out, time_t *max_out) +{ + int64_t min, max; + + if (!Int64RangeFromString(timerange, &min, &max)) + { + return false; + } + + *min_out = min; + *max_out = max; + return true; +} + AclMethod AclMethodFromString(const char *string) { static const char *const ACL_METHOD_TYPES[] = diff --git a/libpromises/conversion.h b/libpromises/conversion.h index d99cf1a525..e5196ebf48 100644 --- a/libpromises/conversion.h +++ b/libpromises/conversion.h @@ -62,6 +62,7 @@ bool StringIsBoolean(const char *val); long IntFromString(const char *s); bool DoubleFromString(const char *s, double *value_out); bool IntegerRangeFromString(const char *intrange, long *min_out, long *max_out); +bool TimeRangeFromString(const char *timerange, time_t *min_out, time_t *max_out); bool IsRealNumber(const char *s); diff --git a/tests/unit/conversion_test.c b/tests/unit/conversion_test.c index 323d72d4b2..46cc71e50d 100644 --- a/tests/unit/conversion_test.c +++ b/tests/unit/conversion_test.c @@ -181,6 +181,75 @@ static void test_double_from_string(void) assert_true(val == old_val); } +static void test_integer_range_from_string(void) +{ + long min, max; + + assert_true(IntegerRangeFromString("0,0", &min, &max)); + assert_int_equal(min, 0L); + assert_int_equal(max, 0L); + + assert_true(IntegerRangeFromString("-20,19", &min, &max)); + assert_int_equal(min, -20L); + assert_int_equal(max, 19L); + + assert_true(IntegerRangeFromString("1,inf", &min, &max)); + assert_int_equal(min, 1L); + assert_int_equal(max, CF_INFINITY); + + /* An absent attribute is reported as CF_NOINT. */ + assert_true(IntegerRangeFromString(NULL, &min, &max)); + assert_int_equal(min, CF_NOINT); + assert_int_equal(max, CF_NOINT); +} + +/** + * TimeRangeFromString() exists because time_t is wider than long on some + * platforms (notably Windows), where writing a bound through a long * fills + * only its low half and leaves the rest untouched. These cases check that + * bounds reach a time_t intact, at full width and with the correct sign. + */ +static void test_time_range_from_string(void) +{ + /* Pre-fill with a pattern that is wrong in both halves, so that a bound + * written only 4 bytes wide fails these assertions. */ + const time_t poison = (time_t) 0x5A5A5A5A5A5A5A5A; + time_t min = poison, max = poison; + + /* A bound that fits in 32 bits. */ + assert_true(TimeRangeFromString("0,283824000", &min, &max)); + assert_int_equal(min, 0); + assert_int_equal(max, 283824000); + + /* A bound beyond INT32_MAX, which cannot survive a round trip through a + * 32-bit long. Policy reaches these values easily: this one is what + * irange(0, accumulated(99,0,0,0,0,0)) expands to, i.e. 99 years. */ + min = poison; + max = poison; + assert_true(TimeRangeFromString("0,3122064000", &min, &max)); + assert_int_equal(min, 0); + assert_int_equal(max, (time_t) 3122064000); + + min = poison; + max = poison; + assert_true(TimeRangeFromString("-3122064000,-1", &min, &max)); + assert_int_equal(min, (time_t) -3122064000); + assert_int_equal(max, (time_t) -1); + + min = poison; + max = poison; + assert_true(TimeRangeFromString("0,inf", &min, &max)); + assert_int_equal(min, 0); + assert_int_equal(max, CF_INFINITY); + + /* An absent attribute is reported as CF_NOINT */ + min = poison; + max = poison; + assert_true(TimeRangeFromString(NULL, &min, &max)); + assert_int_equal(min, CF_NOINT); + assert_int_equal(max, CF_NOINT); +} + static void test_signal_from_string(void) { /* Lowercase (baseline -- should always work) */ @@ -309,6 +378,8 @@ int main() unit_test(test_boolean_from_string), unit_test(test_int_from_string), unit_test(test_double_from_string), + unit_test(test_integer_range_from_string), + unit_test(test_time_range_from_string), unit_test(test_CommandArg0_bound), unit_test(test_signal_from_string), };