diff --git a/src/tools/cron.c b/src/tools/cron.c index a6a753b..9de58c7 100644 --- a/src/tools/cron.c +++ b/src/tools/cron.c @@ -10,6 +10,8 @@ #include "channels/channel.h" #include "core/config.h" #include "cJSON.h" +#include +#include #include #include #include @@ -125,15 +127,28 @@ static long long cron_next_from_expr(const char *cron_part, long long now) return -1; } +/* Reject a span that does not fit in now+seconds. Signed wrap stores a past + * next_run, and ack recomputes the same sum, so the job stays due forever. */ +static int interval_next_run(const char *num, long long now, long long *next_out) +{ + char *end; + long long sec; + + errno = 0; + sec = strtoll(num, &end, 10); + if (end == num || errno == ERANGE || sec <= 0) + return -1; + if (now < 0 || sec > LLONG_MAX - now) + return -1; + *next_out = now + sec; + return 0; +} + int cron_parse_next_run(const char *schedule, long long now, long long *next_out) { if (!schedule || !next_out) return -1; - if (strncmp(schedule, CRON_PREFIX_INTERVAL, strlen(CRON_PREFIX_INTERVAL)) == 0) { - long sec = strtol(schedule + strlen(CRON_PREFIX_INTERVAL), NULL, 10); - if (sec <= 0) return -1; - *next_out = now + sec; - return 0; - } + if (strncmp(schedule, CRON_PREFIX_INTERVAL, strlen(CRON_PREFIX_INTERVAL)) == 0) + return interval_next_run(schedule + strlen(CRON_PREFIX_INTERVAL), now, next_out); if (strncmp(schedule, CRON_PREFIX_AT, strlen(CRON_PREFIX_AT)) == 0) { long long ts = (long long)strtoll(schedule + strlen(CRON_PREFIX_AT), NULL, 10); *next_out = ts; diff --git a/tests/test_cron.c b/tests/test_cron.c index 2e8369c..fac1697 100644 --- a/tests/test_cron.c +++ b/tests/test_cron.c @@ -124,6 +124,38 @@ static int test_invalid_schedule(void) return 0; } +/* A seconds value that does not fit in now+sec used to wrap next_run negative. + * Ack recomputes that sum, so the job stays due and the agent runs it forever. */ +static int test_interval_overflow_does_not_stay_due(void) +{ + const char *path = "/tmp/shellclaw_test_cron_interval_overflow.db"; + const char *schedule = "interval:9223372036854775807"; + cron_job_row_t row; + long long now; + long long next = 0; + long long floor_next; + long long ceil_next; + + now = 1700000000; + ASSERT(cron_parse_next_run(schedule, now, &next) == -1); + ASSERT(cron_parse_next_run("interval:999999999999999999999", now, &next) == -1); + remove(path); + ASSERT(memory_init(path) == 0); + now = (long long)time(NULL); + ASSERT(cron_job_create("overflow", schedule, "msg", "cli", "default", now - 1, 1) == 0); + ASSERT(cron_ack_delivery("overflow") == 0); + memset(&row, 0, sizeof(row)); + ASSERT(cron_job_get_by_id("overflow", &row) == 1); + floor_next = now + 365LL * 24 * 3600 - 2; + ceil_next = now + 365LL * 24 * 3600 + 2; + ASSERT(row.next_run >= floor_next); + ASSERT(row.next_run <= ceil_next); + cron_job_row_free(&row); + memory_cleanup(); + remove(path); + return 0; +} + static int test_cron_job_crud_and_due(void) { const char *path = "/tmp/shellclaw_test_cron.db"; @@ -659,6 +691,7 @@ int main(void) RUN(test_cron_expr_skips_current_minute()); RUN(test_cron_expr_monthly_beyond_eight_days()); RUN(test_invalid_schedule()); + RUN(test_interval_overflow_does_not_stay_due()); RUN(test_cron_job_crud_and_due()); RUN(test_cron_tool_execute()); RUN(test_one_shot_detection());