Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions src/tools/cron.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
#include "channels/channel.h"
#include "core/config.h"
#include "cJSON.h"
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Expand Down Expand Up @@ -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;
Expand Down
33 changes: 33 additions & 0 deletions tests/test_cron.c
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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());
Expand Down
Loading