├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── ccronexpr.c ├── ccronexpr.h ├── cronevent.c ├── cronevent.h ├── fnv1a.c ├── fnv1a.h ├── limit_process.c ├── limit_process.h ├── musl-make ├── restrict_process.h ├── restrict_process_capsicum.c ├── restrict_process_null.c ├── restrict_process_pledge.c ├── restrict_process_rlimit.c ├── restrict_process_seccomp.c ├── runcron.c ├── runcron.h ├── setproctitle.c ├── setproctitle.h ├── strtonum.c ├── strtonum.h ├── test └── 10-crontab.bats ├── timestamp.c ├── timestamp.h ├── waitfor.c └── waitfor.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | runcron 55 | .runcron.lock 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-2025, Michael Santos 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean test 2 | 3 | PROG= runcron 4 | SRCS= runcron.c \ 5 | cronevent.c \ 6 | ccronexpr.c \ 7 | fnv1a.c \ 8 | strtonum.c \ 9 | timestamp.c \ 10 | setproctitle.c \ 11 | waitfor.c \ 12 | limit_process.c \ 13 | restrict_process_capsicum.c \ 14 | restrict_process_null.c \ 15 | restrict_process_pledge.c \ 16 | restrict_process_rlimit.c \ 17 | restrict_process_seccomp.c 18 | 19 | UNAME_SYS := $(shell uname -s) 20 | ifeq ($(UNAME_SYS), Linux) 21 | CFLAGS ?= -D_FORTIFY_SOURCE=2 -O2 -fstack-protector-strong \ 22 | -Wformat -Werror=format-security \ 23 | -pie -fPIE \ 24 | -fno-strict-aliasing 25 | LDFLAGS += -Wl,-z,relro,-z,now -Wl,-z,noexecstack 26 | RESTRICT_PROCESS ?= seccomp 27 | else ifeq ($(UNAME_SYS), OpenBSD) 28 | CFLAGS ?= -DHAVE_SETPROCTITLE \ 29 | -D_FORTIFY_SOURCE=2 -O2 -fstack-protector-strong \ 30 | -Wformat -Werror=format-security \ 31 | -pie -fPIE \ 32 | -fno-strict-aliasing 33 | LDFLAGS += -Wno-missing-braces -Wl,-z,relro,-z,now -Wl,-z,noexecstack 34 | RESTRICT_PROCESS ?= pledge 35 | else ifeq ($(UNAME_SYS), FreeBSD) 36 | CFLAGS ?= -DHAVE_SETPROCTITLE \ 37 | -D_FORTIFY_SOURCE=2 -O2 -fstack-protector-strong \ 38 | -Wformat -Werror=format-security \ 39 | -pie -fPIE \ 40 | -fno-strict-aliasing 41 | LDFLAGS += -Wno-missing-braces -Wl,-z,relro,-z,now -Wl,-z,noexecstack 42 | RESTRICT_PROCESS ?= capsicum 43 | else ifeq ($(UNAME_SYS), Darwin) 44 | CFLAGS ?= -D_FORTIFY_SOURCE=2 -O2 -fstack-protector-strong \ 45 | -Wformat -Werror=format-security \ 46 | -pie -fPIE \ 47 | -fno-strict-aliasing 48 | LDFLAGS += -Wno-missing-braces 49 | endif 50 | 51 | RM ?= rm 52 | 53 | RESTRICT_PROCESS ?= rlimit 54 | RUNCRON_CFLAGS ?= -g -Wall -Wextra -fwrapv -pedantic -Wno-unused-parameter 55 | 56 | CFLAGS += $(RUNCRON_CFLAGS) \ 57 | -DCRON_USE_LOCAL_TIME \ 58 | -DRESTRICT_PROCESS=\"$(RESTRICT_PROCESS)\" \ 59 | -DRESTRICT_PROCESS_$(RESTRICT_PROCESS) 60 | 61 | LDFLAGS += $(RUNCRON_LDFLAGS) 62 | 63 | all: $(PROG) 64 | 65 | $(PROG): 66 | $(CC) $(CFLAGS) -o $(PROG) $(SRCS) $(LDFLAGS) 67 | 68 | clean: 69 | -@$(RM) $(PROG) 70 | 71 | test: $(PROG) 72 | @PATH=.:$(PATH) bats test 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | runcron - simple, safe, container-friendly cron alternative 2 | 3 | # SYNOPSIS 4 | 5 | runcron [*options*] *crontab expression* *command* *arg* *...* 6 | 7 | # DESCRIPTION 8 | 9 | `runcron` is a minimal cron running as part of a process supervision 10 | tree for automated environments. runcron is intended to be simple, 11 | safe and container-friendly. 12 | 13 | `runcron` runs under a supervisor like 14 | [daemontools](https://cr.yp.to/daemontools.html) and exits after task 15 | completion. The supervisor restarts runcron, taking any action based on 16 | the task exit status: 17 | 18 | ``` 19 | svscan,17276,17276 service/ 20 | |-supervise,17277,17276 date17 21 | | `-runcron,17308,17276 */17 * * * * * sh -c echo 17: $(date) 22 | |-supervise,17279,17276 date33 23 | | `-runcron,17303,17276 */33 * * * * * sh -c echo 33: $(date) 24 | `-supervise,17280,17276 sleep 25 | `-runcron,17282,17276 @reboot sleep inf 26 | `-sleep,17288,17288 inf 27 | ``` 28 | 29 | `runcron` supervises tasks: 30 | 31 | * only allows a single instance of a job to run 32 | 33 | * job runtime is limited to the next cron interval 34 | 35 | * when the task is complete, exits with value set to the task exit status 36 | 37 | * periodically retries the job if it exits non-0 38 | 39 | * if the tasks succeeds (exits 0), when restarted, sleeps until the next 40 | cron interval 41 | 42 | * terminates any background subprocesses when the foreground process exits 43 | 44 | * attempts to prevent running unkillable (setuid) subprocesses 45 | 46 | * standard input is forwarded to the task 47 | 48 | cron expressions are parsed using 49 | [ccronexpr](https://github.com/staticlibs/ccronexpr). 50 | 51 | The standard crontab(5) expressions (and some additional expressions) 52 | are supported. The seconds field is optional: 53 | 54 | ``` 55 | field allowed values 56 | ----- -------------- 57 | second 0-59 (optional) 58 | minute 0-59 59 | hour 0-23 60 | day of month 1-31 61 | month 1-12 (or names, see below) 62 | day of week 0-7 (0 or 7 is Sun, or use names) 63 | ``` 64 | 65 | crontab(5) aliases pseudorandomly assign a run time from the alias 66 | interval. To run exactly at the start of the interval, use the "=" 67 | alias variant: 68 | 69 | ``` 70 | string meaning 71 | ------ ------- 72 | @reboot Run once, at startup (see below). 73 | @yearly Run once a year, "0~59 0~59 0~23 1~28 1~12 *". 74 | @annually (same as @yearly) 75 | @monthly Run once a month, "0~59 0~59 0~23 1~28 * *". 76 | @weekly Run once a week, "0~59 0~59 0~23 * * 1~7". 77 | @daily Run once a day, "0~59 0~59 0~23 * * *". 78 | @midnight (same as =daily) 79 | @hourly Run once an hour, "0~59 0~59 * * * *". 80 | 81 | =reboot (same as @reboot) 82 | =yearly Run once a year, "0 0 1 1 *". 83 | =annually (same as =yearly) 84 | =monthly Run once a month, "0 0 1 * *". 85 | =weekly Run once a week, "0 0 * * 0". 86 | =daily Run once a day, "0 0 * * *". 87 | =midnight (same as =daily) 88 | =hourly Run once an hour, "0 * * * *". 89 | ``` 90 | 91 | ## Handling stdin 92 | 93 | Standard input is forwarded to the subprocess: 94 | 95 | ``` 96 | $ echo test | runcron '0~59 * * * * *' sed 's/e/3/g' 97 | t3st 98 | ``` 99 | 100 | ## crontab Expressions 101 | 102 | ### Randomized Intervals 103 | 104 | `runcron` supports random values in intervals inspired by 105 | [OpenBSD crontab](https://man.openbsd.org/crontab.5). 106 | 107 | The `~` character in an interval field will pseudorandomly choose an 108 | offset: 109 | 110 | ``` 111 | # run once, from Monday to Friday, between 12am and 8am 112 | 0 0~8 * * 1~5 113 | ``` 114 | 115 | The random offset is predictable, using the system hostname as the seed 116 | by default. Use the `-t` option to change the seed or set it to an empty 117 | string ("") to use the current time as the seed. 118 | 119 | ``` 120 | # runs: Tuesday at 7am 121 | runcron -t "www1.example.com" -vvv -p -n '0 0~8 * * 1~5' echo test 122 | 123 | # runs: Friday at 5am 124 | runcron -t "www2.example.com" -vvv -p -n '0 0~8 * * 1~5' echo test 125 | ``` 126 | 127 | ## @reboot 128 | 129 | The `@reboot` alias runs the task immediately. The behaviour of subsequent 130 | attempts to run the task depends on the exit status of the previous run: 131 | 132 | * 0: runcron will not run the task and sleep indefinitely 133 | * non-0: runcron will rerun the task after `--retry-interval` seconds 134 | (default: 3600) 135 | 136 | Since the runcron state is written to a file (see `-f` option), the 137 | state can persist between reboots. 138 | 139 | ``` 140 | umask 077 141 | mkdir -p /tmp/reboot 142 | runcron -f /tmp/reboot/runcron.lock ... 143 | ``` 144 | 145 | # EXAMPLES 146 | 147 | ``` 148 | # Attempt to connect to google daily 149 | # If the connection fails, the task will be retried hourly. 150 | runcron "43 7 * * *" nc -z google.com 80 151 | 152 | # Run at 9:03am on the 20th of each month. 153 | # After the first run, the job will be terminated before the 154 | # next scheduled run. 155 | runcron "3 9 20 * *" sleep inf 156 | 157 | # schedule a task randomly between nodes from Monday-Sunday 158 | # each node will choose the same offset based on the hostname 159 | runcron "11 * * * 1~7" echo test 160 | 161 | # specify a string to use as a seed 162 | runcron -t "foo.example.com" "11 * * * 1~7" echo test 163 | 164 | # or non-deterministically based on the time 165 | # 166 | # Since the next run interval will be randomly chosen, manually 167 | # set a timeout 168 | runcron -t "" -T 3600 "11 * * * 1~7" echo test 169 | ``` 170 | 171 | ## daemontools run script 172 | 173 | ``` 174 | #!/bin/sh 175 | 176 | # Run daily at 8:15am 177 | exec runcron "15 8 * * *" echo Running job 178 | ``` 179 | 180 | # OPTIONS 181 | 182 | -f, --file 183 | : lock file path (default: .runcron.lock) 184 | 185 | -C, --chdir 186 | : change working directory before running command 187 | 188 | -T, --timeout 189 | : specify command timeout in seconds (-1 to disable, default: next 190 | cron interval) 191 | 192 | -R, --retry-interval 193 | : interval to retry failed commands (default: 3600s) 194 | 195 | -n, --dryrun 196 | : do nothing 197 | 198 | -p, --print 199 | : output seconds to next timespec 200 | 201 | -s, --signal 202 | : signal sent on command timeout 203 | 204 | The signal is also sent on job completion to clean up any background 205 | tasks (use --disable-signal-on-exit to disable). 206 | 207 | Default: 15 (SIGTERM) 208 | 209 | -t, --tag 210 | : Seed used for for generating a pseudorandom offset for cron expressions 211 | with random intervals. The offset used in a job is constant between 212 | runs. 213 | 214 | Setting the tag to an empty string ("") will cause the offset to be 215 | pseudorandomly chosen based on the current time. The job timeout will 216 | also be random. 217 | 218 | Default: hostname (see `RUNCRON_TAG`) 219 | 220 | -v, --verbose 221 | : verbose mode 222 | 223 | --timestamp *YY-MM-DD hh-mm-ss|@epoch* 224 | : provide an initial time 225 | 226 | --limit-cpu 227 | : restrict cpu usage of cron expression parsing (default: 10 seconds) 228 | 229 | --limit-as 230 | : restrict memory (address space) of cron expression parsing (default: 1 Mb) 231 | 232 | --allow-setuid-subprocess 233 | : allow running potentially unkillable subprocesses 234 | 235 | --disable-process-restrictions 236 | : do not fork cron expression processing 237 | 238 | --disable-signal-on-exit 239 | : By default, any background subprocesses are terminated when the 240 | foreground process is terminated. Use this option to disable signalling 241 | background jobs on exit. 242 | 243 | # SIGNALS 244 | 245 | ## Waiting for Job 246 | 247 | While the task is waiting to run, signals sent to runcron are ignored 248 | except for: 249 | 250 | SIGUSR1/SIGALRM 251 | : Run the job immediately 252 | 253 | SIGUSR2 254 | : Print the remaining number of seconds to stderr 255 | 256 | SIGINT 257 | : Exit with status 111 258 | 259 | SIGTERM 260 | : Exit with status 111 261 | 262 | ## Running Job 263 | 264 | When the task is running, signals (excluding SIGKILL, SIGALRM, SIGUSR1 265 | and SIGUSR2) received by runcron are forwarded to the task process group. 266 | 267 | # ENVIRONMENT VARIABLES 268 | 269 | RUNCRON_TAG 270 | : Sets the default value for the `-t/--tag` option: if unset, the hostname 271 | is used 272 | 273 | ## Read-only 274 | 275 | runcron sets these values before executing the subprocess: 276 | 277 | RUNCRON_EXITSTATUS 278 | : Task exit status of previous run 279 | 280 | RUNCRON_TIMEOUT 281 | : Number of seconds before task is terminated 282 | 283 | # BUILDING 284 | 285 | ## Quick Install 286 | 287 | ``` 288 | make 289 | 290 | # to run tests: requires bats(1) 291 | make clean all test 292 | 293 | # selecting method for restricting cron expression parsing 294 | RESTRICT_PROCESS=seccomp make 295 | 296 | #### using musl 297 | # sudo apt install musl-dev musl-tools 298 | 299 | RESTRICT_PROCESS=rlimit ./musl-make clean all test 300 | 301 | ## linux seccomp sandbox: requires kernel headers 302 | 303 | # clone the kernel headers somewhere 304 | cd /path/to/dir 305 | git clone https://github.com/sabotage-linux/kernel-headers.git 306 | 307 | # then compile 308 | MUSL_INCLUDE=/path/to/dir ./musl-make clean all test 309 | ``` 310 | 311 | # ALTERNATIVES 312 | 313 | * [pseudocron](https://github.com/msantos/pseudocron) 314 | 315 | * [snooze](https://github.com/leahneukirchen/snooze) 316 | 317 | * [runwhen](http://code.dogmap.org/runwhen/) 318 | 319 | * [supercronic](https://github.com/aptible/supercronic) 320 | 321 | * [uschedule](https://ohse.de/uwe/uschedule.html) 322 | 323 | # SEE ALSO 324 | 325 | *crontab*(5) 326 | -------------------------------------------------------------------------------- /ccronexpr.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015, alex at staticlibs.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * File: ccronexpr.c 19 | * Author: alex 20 | * 21 | * Created on February 24, 2015, 9:35 AM 22 | */ 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #include "ccronexpr.h" 33 | 34 | #define CRON_MAX_SECONDS 60 35 | #define CRON_MAX_MINUTES 60 36 | #define CRON_MAX_HOURS 24 37 | #define CRON_MAX_DAYS_OF_WEEK 8 38 | #define CRON_MAX_DAYS_OF_MONTH 32 39 | #define CRON_MAX_MONTHS 12 40 | #define CRON_MAX_YEARS_DIFF 4 41 | 42 | #define CRON_CF_SECOND 0 43 | #define CRON_CF_MINUTE 1 44 | #define CRON_CF_HOUR_OF_DAY 2 45 | #define CRON_CF_DAY_OF_WEEK 3 46 | #define CRON_CF_DAY_OF_MONTH 4 47 | #define CRON_CF_MONTH 5 48 | #define CRON_CF_YEAR 6 49 | 50 | #define CRON_CF_ARR_LEN 7 51 | 52 | #define CRON_INVALID_INSTANT ((time_t) -1) 53 | 54 | static const char* const DAYS_ARR[] = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; 55 | #define CRON_DAYS_ARR_LEN 7 56 | static const char* const MONTHS_ARR[] = { "FOO", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; 57 | #define CRON_MONTHS_ARR_LEN 13 58 | 59 | #define CRON_MAX_STR_LEN_TO_SPLIT 256 60 | #define CRON_MAX_NUM_TO_SRING 1000000000 61 | /* computes number of digits in decimal number */ 62 | #define CRON_NUM_OF_DIGITS(num) (abs(num) < 10 ? 1 : \ 63 | (abs(num) < 100 ? 2 : \ 64 | (abs(num) < 1000 ? 3 : \ 65 | (abs(num) < 10000 ? 4 : \ 66 | (abs(num) < 100000 ? 5 : \ 67 | (abs(num) < 1000000 ? 6 : \ 68 | (abs(num) < 10000000 ? 7 : \ 69 | (abs(num) < 100000000 ? 8 : \ 70 | (abs(num) < 1000000000 ? 9 : 10))))))))) 71 | 72 | #ifndef CRON_TEST_MALLOC 73 | #define cron_malloc(x) malloc(x); 74 | #define cron_free(x) free(x); 75 | #else /* CRON_TEST_MALLOC */ 76 | void* cron_malloc(size_t n); 77 | void cron_free(void* p); 78 | #endif /* CRON_TEST_MALLOC */ 79 | 80 | /** 81 | * Time functions from standard library. 82 | * This part defines: cron_mktime: create time_t from tm 83 | * cron_time: create tm from time_t 84 | */ 85 | 86 | /* forward declarations for platforms that may need them */ 87 | /* can be hidden in time.h */ 88 | #if !defined(_WIN32) && !defined(__AVR__) && !defined(ESP8266) && !defined(ANDROID) 89 | struct tm *gmtime_r(const time_t *timep, struct tm *result); 90 | time_t timegm(struct tm* __tp); 91 | struct tm *localtime_r(const time_t *timep, struct tm *result); 92 | #endif /* PLEASE CHECK _WIN32 AND ANDROID NEEDS FOR THESE DECLARATIONS */ 93 | #ifdef __MINGW32__ 94 | /* To avoid warning when building with mingw */ 95 | time_t _mkgmtime(struct tm* tm); 96 | #endif /* __MINGW32__ */ 97 | 98 | /* function definitions */ 99 | time_t cron_mktime_gm(struct tm* tm) { 100 | #if defined(_WIN32) 101 | /* http://stackoverflow.com/a/22557778 */ 102 | return _mkgmtime(tm); 103 | #elif defined(__AVR__) 104 | /* https://www.nongnu.org/avr-libc/user-manual/group__avr__time.html */ 105 | return mk_gmtime(tm); 106 | #elif defined(ESP8266) 107 | /* https://linux.die.net/man/3/timegm */ 108 | /* http://www.catb.org/esr/time-programming/ */ 109 | /* portable version of timegm() */ 110 | time_t ret; 111 | char *tz; 112 | tz = getenv("TZ"); 113 | if (tz) 114 | tz = strdup(tz); 115 | setenv("TZ", "UTC+0", 1); 116 | tzset(); 117 | ret = mktime(tm); 118 | if (tz) { 119 | setenv("TZ", tz, 1); 120 | free(tz); 121 | } else 122 | unsetenv("TZ"); 123 | tzset(); 124 | return ret; 125 | #elif defined(ANDROID) 126 | /* https://github.com/adobe/chromium/blob/cfe5bf0b51b1f6b9fe239c2a3c2f2364da9967d7/base/os_compat_android.cc#L20 */ 127 | static const time_t kTimeMax = ~(1L << (sizeof (time_t) * CHAR_BIT - 1)); 128 | static const time_t kTimeMin = (1L << (sizeof (time_t) * CHAR_BIT - 1)); 129 | time64_t result = timegm64(tm); 130 | if (result < kTimeMin || result > kTimeMax) return -1; 131 | return result; 132 | #else 133 | return timegm(tm); 134 | #endif 135 | } 136 | 137 | struct tm* cron_time_gm(time_t* date, struct tm* out) { 138 | #if defined(__MINGW32__) 139 | (void)(out); /* To avoid unused warning */ 140 | return gmtime(date); 141 | #elif defined(_WIN32) 142 | errno_t err = gmtime_s(out, date); 143 | return 0 == err ? out : NULL; 144 | #elif defined(__AVR__) 145 | /* https://www.nongnu.org/avr-libc/user-manual/group__avr__time.html */ 146 | gmtime_r(date, out); 147 | return out; 148 | #else 149 | return gmtime_r(date, out); 150 | #endif 151 | } 152 | 153 | time_t cron_mktime_local(struct tm* tm) { 154 | tm->tm_isdst = -1; 155 | return mktime(tm); 156 | } 157 | 158 | struct tm* cron_time_local(time_t* date, struct tm* out) { 159 | #if defined(_WIN32) 160 | errno_t err = localtime_s(out, date); 161 | return 0 == err ? out : NULL; 162 | #elif defined(__AVR__) 163 | /* https://www.nongnu.org/avr-libc/user-manual/group__avr__time.html */ 164 | localtime_r(date, out); 165 | return out; 166 | #else 167 | return localtime_r(date, out); 168 | #endif 169 | } 170 | 171 | /* Defining 'cron_' time functions to use use UTC (default) or local time */ 172 | #ifndef CRON_USE_LOCAL_TIME 173 | time_t cron_mktime(struct tm* tm) { 174 | return cron_mktime_gm(tm); 175 | } 176 | 177 | struct tm* cron_time(time_t* date, struct tm* out) { 178 | return cron_time_gm(date, out); 179 | } 180 | 181 | #else /* CRON_USE_LOCAL_TIME */ 182 | time_t cron_mktime(struct tm* tm) { 183 | return cron_mktime_local(tm); 184 | } 185 | 186 | struct tm* cron_time(time_t* date, struct tm* out) { 187 | return cron_time_local(date, out); 188 | } 189 | 190 | #endif /* CRON_USE_LOCAL_TIME */ 191 | 192 | /** 193 | * Functions. 194 | */ 195 | 196 | void cron_set_bit(uint8_t* rbyte, int idx) { 197 | uint8_t j = (uint8_t) (idx / 8); 198 | uint8_t k = (uint8_t) (idx % 8); 199 | 200 | rbyte[j] |= (1 << k); 201 | } 202 | 203 | void cron_del_bit(uint8_t* rbyte, int idx) { 204 | uint8_t j = (uint8_t) (idx / 8); 205 | uint8_t k = (uint8_t) (idx % 8); 206 | 207 | rbyte[j] &= ~(1 << k); 208 | } 209 | 210 | uint8_t cron_get_bit(uint8_t* rbyte, int idx) { 211 | uint8_t j = (uint8_t) (idx / 8); 212 | uint8_t k = (uint8_t) (idx % 8); 213 | 214 | if (rbyte[j] & (1 << k)) { 215 | return 1; 216 | } else { 217 | return 0; 218 | } 219 | } 220 | 221 | static void free_splitted(char** splitted, size_t len) { 222 | size_t i; 223 | if (!splitted) return; 224 | for (i = 0; i < len; i++) { 225 | if (splitted[i]) { 226 | cron_free(splitted[i]); 227 | } 228 | } 229 | cron_free(splitted); 230 | } 231 | 232 | static char* strdupl(const char* str, size_t len) { 233 | if (!str) return NULL; 234 | char* res = (char*) cron_malloc(len + 1); 235 | if (!res) return NULL; 236 | memset(res, 0, len + 1); 237 | memcpy(res, str, len); 238 | return res; 239 | } 240 | 241 | static unsigned int next_set_bit(uint8_t* bits, unsigned int max, unsigned int from_index, int* notfound) { 242 | unsigned int i; 243 | if (!bits) { 244 | *notfound = 1; 245 | return 0; 246 | } 247 | for (i = from_index; i < max; i++) { 248 | if (cron_get_bit(bits, i)) return i; 249 | } 250 | *notfound = 1; 251 | return 0; 252 | } 253 | 254 | static void push_to_fields_arr(int* arr, int fi) { 255 | int i; 256 | if (!arr || -1 == fi) { 257 | return; 258 | } 259 | for (i = 0; i < CRON_CF_ARR_LEN; i++) { 260 | if (arr[i] == fi) return; 261 | } 262 | for (i = 0; i < CRON_CF_ARR_LEN; i++) { 263 | if (-1 == arr[i]) { 264 | arr[i] = fi; 265 | return; 266 | } 267 | } 268 | } 269 | 270 | static int add_to_field(struct tm* calendar, int field, int val) { 271 | if (!calendar || -1 == field) { 272 | return 1; 273 | } 274 | switch (field) { 275 | case CRON_CF_SECOND: 276 | calendar->tm_sec = calendar->tm_sec + val; 277 | break; 278 | case CRON_CF_MINUTE: 279 | calendar->tm_min = calendar->tm_min + val; 280 | break; 281 | case CRON_CF_HOUR_OF_DAY: 282 | calendar->tm_hour = calendar->tm_hour + val; 283 | break; 284 | case CRON_CF_DAY_OF_WEEK: /* mkgmtime ignores this field */ 285 | case CRON_CF_DAY_OF_MONTH: 286 | calendar->tm_mday = calendar->tm_mday + val; 287 | break; 288 | case CRON_CF_MONTH: 289 | calendar->tm_mon = calendar->tm_mon + val; 290 | break; 291 | case CRON_CF_YEAR: 292 | calendar->tm_year = calendar->tm_year + val; 293 | break; 294 | default: 295 | return 1; /* unknown field */ 296 | } 297 | time_t res = cron_mktime(calendar); 298 | if (CRON_INVALID_INSTANT == res) { 299 | return 1; 300 | } 301 | return 0; 302 | } 303 | 304 | /** 305 | * Reset the calendar setting all the fields provided to zero. 306 | */ 307 | static int reset_min(struct tm* calendar, int field) { 308 | if (!calendar || -1 == field) { 309 | return 1; 310 | } 311 | switch (field) { 312 | case CRON_CF_SECOND: 313 | calendar->tm_sec = 0; 314 | break; 315 | case CRON_CF_MINUTE: 316 | calendar->tm_min = 0; 317 | break; 318 | case CRON_CF_HOUR_OF_DAY: 319 | calendar->tm_hour = 0; 320 | break; 321 | case CRON_CF_DAY_OF_WEEK: 322 | calendar->tm_wday = 0; 323 | break; 324 | case CRON_CF_DAY_OF_MONTH: 325 | calendar->tm_mday = 1; 326 | break; 327 | case CRON_CF_MONTH: 328 | calendar->tm_mon = 0; 329 | break; 330 | case CRON_CF_YEAR: 331 | calendar->tm_year = 0; 332 | break; 333 | default: 334 | return 1; /* unknown field */ 335 | } 336 | time_t res = cron_mktime(calendar); 337 | if (CRON_INVALID_INSTANT == res) { 338 | return 1; 339 | } 340 | return 0; 341 | } 342 | 343 | static int reset_all_min(struct tm* calendar, int* fields) { 344 | int i; 345 | int res = 0; 346 | if (!calendar || !fields) { 347 | return 1; 348 | } 349 | for (i = 0; i < CRON_CF_ARR_LEN; i++) { 350 | if (-1 != fields[i]) { 351 | res = reset_min(calendar, fields[i]); 352 | if (0 != res) return res; 353 | } 354 | } 355 | return 0; 356 | } 357 | 358 | static int set_field(struct tm* calendar, int field, int val) { 359 | if (!calendar || -1 == field) { 360 | return 1; 361 | } 362 | switch (field) { 363 | case CRON_CF_SECOND: 364 | calendar->tm_sec = val; 365 | break; 366 | case CRON_CF_MINUTE: 367 | calendar->tm_min = val; 368 | break; 369 | case CRON_CF_HOUR_OF_DAY: 370 | calendar->tm_hour = val; 371 | break; 372 | case CRON_CF_DAY_OF_WEEK: 373 | calendar->tm_wday = val; 374 | break; 375 | case CRON_CF_DAY_OF_MONTH: 376 | calendar->tm_mday = val; 377 | break; 378 | case CRON_CF_MONTH: 379 | calendar->tm_mon = val; 380 | break; 381 | case CRON_CF_YEAR: 382 | calendar->tm_year = val; 383 | break; 384 | default: 385 | return 1; /* unknown field */ 386 | } 387 | time_t res = cron_mktime(calendar); 388 | if (CRON_INVALID_INSTANT == res) { 389 | return 1; 390 | } 391 | return 0; 392 | } 393 | 394 | /** 395 | * Search the bits provided for the next set bit after the value provided, 396 | * and reset the calendar. 397 | */ 398 | static unsigned int find_next(uint8_t* bits, unsigned int max, unsigned int value, struct tm* calendar, unsigned int field, unsigned int nextField, int* lower_orders, int* res_out) { 399 | int notfound = 0; 400 | int err = 0; 401 | unsigned int next_value = next_set_bit(bits, max, value, ¬found); 402 | /* roll over if needed */ 403 | if (notfound) { 404 | err = add_to_field(calendar, nextField, 1); 405 | if (err) goto return_error; 406 | err = reset_min(calendar, field); 407 | if (err) goto return_error; 408 | notfound = 0; 409 | next_value = next_set_bit(bits, max, 0, ¬found); 410 | } 411 | if (notfound || next_value != value) { 412 | err = set_field(calendar, field, next_value); 413 | if (err) goto return_error; 414 | err = reset_all_min(calendar, lower_orders); 415 | if (err) goto return_error; 416 | } 417 | return next_value; 418 | 419 | return_error: 420 | *res_out = 1; 421 | return 0; 422 | } 423 | 424 | static unsigned int find_next_day(struct tm* calendar, uint8_t* days_of_month, unsigned int day_of_month, uint8_t* days_of_week, unsigned int day_of_week, int* resets, int* res_out) { 425 | int err; 426 | unsigned int count = 0; 427 | unsigned int max = 366; 428 | while ((!cron_get_bit(days_of_month, day_of_month) || !cron_get_bit(days_of_week, day_of_week)) && count++ < max) { 429 | err = add_to_field(calendar, CRON_CF_DAY_OF_MONTH, 1); 430 | 431 | if (err) goto return_error; 432 | day_of_month = calendar->tm_mday; 433 | day_of_week = calendar->tm_wday; 434 | reset_all_min(calendar, resets); 435 | } 436 | return day_of_month; 437 | 438 | return_error: 439 | *res_out = 1; 440 | return 0; 441 | } 442 | 443 | static int do_next(cron_expr* expr, struct tm* calendar, unsigned int dot) { 444 | int i; 445 | int res = 0; 446 | int* resets = NULL; 447 | int* empty_list = NULL; 448 | unsigned int second = 0; 449 | unsigned int update_second = 0; 450 | unsigned int minute = 0; 451 | unsigned int update_minute = 0; 452 | unsigned int hour = 0; 453 | unsigned int update_hour = 0; 454 | unsigned int day_of_week = 0; 455 | unsigned int day_of_month = 0; 456 | unsigned int update_day_of_month = 0; 457 | unsigned int month = 0; 458 | unsigned int update_month = 0; 459 | 460 | resets = (int*) cron_malloc(CRON_CF_ARR_LEN * sizeof(int)); 461 | if (!resets) goto return_result; 462 | empty_list = (int*) cron_malloc(CRON_CF_ARR_LEN * sizeof(int)); 463 | if (!empty_list) goto return_result; 464 | for (i = 0; i < CRON_CF_ARR_LEN; i++) { 465 | resets[i] = -1; 466 | empty_list[i] = -1; 467 | } 468 | 469 | second = calendar->tm_sec; 470 | update_second = find_next(expr->seconds, CRON_MAX_SECONDS, second, calendar, CRON_CF_SECOND, CRON_CF_MINUTE, empty_list, &res); 471 | if (0 != res) goto return_result; 472 | if (second == update_second) { 473 | push_to_fields_arr(resets, CRON_CF_SECOND); 474 | } 475 | 476 | minute = calendar->tm_min; 477 | update_minute = find_next(expr->minutes, CRON_MAX_MINUTES, minute, calendar, CRON_CF_MINUTE, CRON_CF_HOUR_OF_DAY, resets, &res); 478 | if (0 != res) goto return_result; 479 | if (minute == update_minute) { 480 | push_to_fields_arr(resets, CRON_CF_MINUTE); 481 | } else { 482 | res = do_next(expr, calendar, dot); 483 | if (0 != res) goto return_result; 484 | } 485 | 486 | hour = calendar->tm_hour; 487 | update_hour = find_next(expr->hours, CRON_MAX_HOURS, hour, calendar, CRON_CF_HOUR_OF_DAY, CRON_CF_DAY_OF_WEEK, resets, &res); 488 | if (0 != res) goto return_result; 489 | if (hour == update_hour) { 490 | push_to_fields_arr(resets, CRON_CF_HOUR_OF_DAY); 491 | } else { 492 | res = do_next(expr, calendar, dot); 493 | if (0 != res) goto return_result; 494 | } 495 | 496 | day_of_week = calendar->tm_wday; 497 | day_of_month = calendar->tm_mday; 498 | update_day_of_month = find_next_day(calendar, expr->days_of_month, day_of_month, expr->days_of_week, day_of_week, resets, &res); 499 | if (0 != res) goto return_result; 500 | if (day_of_month == update_day_of_month) { 501 | push_to_fields_arr(resets, CRON_CF_DAY_OF_MONTH); 502 | } else { 503 | res = do_next(expr, calendar, dot); 504 | if (0 != res) goto return_result; 505 | } 506 | 507 | month = calendar->tm_mon; /*day already adds one if no day in same month is found*/ 508 | update_month = find_next(expr->months, CRON_MAX_MONTHS, month, calendar, CRON_CF_MONTH, CRON_CF_YEAR, resets, &res); 509 | if (0 != res) goto return_result; 510 | if (month != update_month) { 511 | if (calendar->tm_year - dot > 4) { 512 | res = -1; 513 | goto return_result; 514 | } 515 | res = do_next(expr, calendar, dot); 516 | if (0 != res) goto return_result; 517 | } 518 | goto return_result; 519 | 520 | return_result: 521 | if (!resets || !empty_list) { 522 | res = -1; 523 | } 524 | if (resets) { 525 | cron_free(resets); 526 | } 527 | if (empty_list) { 528 | cron_free(empty_list); 529 | } 530 | return res; 531 | } 532 | 533 | static int to_upper(char* str) { 534 | if (!str) return 1; 535 | int i; 536 | for (i = 0; '\0' != str[i]; i++) { 537 | int c = (int)str[i]; 538 | str[i] = (char) toupper(c); 539 | } 540 | return 0; 541 | } 542 | 543 | static char* to_string(int num) { 544 | if (abs(num) >= CRON_MAX_NUM_TO_SRING) return NULL; 545 | char* str = (char*) cron_malloc(CRON_NUM_OF_DIGITS(num) + 1); 546 | if (!str) return NULL; 547 | int res = sprintf(str, "%d", num); 548 | if (res < 0) { 549 | cron_free(str); 550 | return NULL; 551 | } 552 | return str; 553 | } 554 | 555 | static char* str_replace(char *orig, const char *rep, const char *with) { 556 | char *result; /* the return string */ 557 | char *ins; /* the next insert point */ 558 | char *tmp; /* varies */ 559 | size_t len_rep; /* length of rep */ 560 | size_t len_with; /* length of with */ 561 | size_t len_front; /* distance between rep and end of last rep */ 562 | int count; /* number of replacements */ 563 | if (!orig) return NULL; 564 | if (!rep) rep = ""; 565 | if (!with) with = ""; 566 | len_rep = strlen(rep); 567 | len_with = strlen(with); 568 | 569 | ins = orig; 570 | for (count = 0; NULL != (tmp = strstr(ins, rep)); ++count) { 571 | ins = tmp + len_rep; 572 | } 573 | 574 | /* first time through the loop, all the variable are set correctly 575 | from here on, 576 | tmp points to the end of the result string 577 | ins points to the next occurrence of rep in orig 578 | orig points to the remainder of orig after "end of rep" 579 | */ 580 | tmp = result = (char*) cron_malloc(strlen(orig) + (len_with - len_rep) * count + 1); 581 | if (!result) return NULL; 582 | 583 | while (count--) { 584 | ins = strstr(orig, rep); 585 | len_front = ins - orig; 586 | tmp = strncpy(tmp, orig, len_front) + len_front; 587 | tmp = strcpy(tmp, with) + len_with; 588 | orig += len_front + len_rep; /* move to next "end of rep" */ 589 | } 590 | strcpy(tmp, orig); 591 | return result; 592 | } 593 | 594 | static unsigned int parse_uint(const char* str, int* errcode) { 595 | char* endptr; 596 | errno = 0; 597 | long int l = strtol(str, &endptr, 0); 598 | if (errno == ERANGE || *endptr != '\0' || l < 0 || l > INT_MAX) { 599 | *errcode = 1; 600 | return 0; 601 | } else { 602 | *errcode = 0; 603 | return (unsigned int) l; 604 | } 605 | } 606 | 607 | static char** split_str(const char* str, char del, size_t* len_out) { 608 | size_t i; 609 | size_t stlen = 0; 610 | size_t len = 0; 611 | int accum = 0; 612 | char* buf = NULL; 613 | char** res = NULL; 614 | size_t bi = 0; 615 | size_t ri = 0; 616 | char* tmp; 617 | 618 | if (!str) goto return_error; 619 | for (i = 0; '\0' != str[i]; i++) { 620 | stlen += 1; 621 | if (stlen >= CRON_MAX_STR_LEN_TO_SPLIT) goto return_error; 622 | } 623 | 624 | for (i = 0; i < stlen; i++) { 625 | int c = str[i]; 626 | if (del == str[i]) { 627 | if (accum > 0) { 628 | len += 1; 629 | accum = 0; 630 | } 631 | } else if (!isspace(c)) { 632 | accum += 1; 633 | } 634 | } 635 | /* tail */ 636 | if (accum > 0) { 637 | len += 1; 638 | } 639 | if (0 == len) return NULL; 640 | 641 | buf = (char*) cron_malloc(stlen + 1); 642 | if (!buf) goto return_error; 643 | memset(buf, 0, stlen + 1); 644 | res = (char**) cron_malloc(len * sizeof(char*)); 645 | if (!res) goto return_error; 646 | memset(res, 0, len * sizeof(char*)); 647 | 648 | for (i = 0; i < stlen; i++) { 649 | int c = str[i]; 650 | if (del == str[i]) { 651 | if (bi > 0) { 652 | tmp = strdupl(buf, bi); 653 | if (!tmp) goto return_error; 654 | res[ri++] = tmp; 655 | memset(buf, 0, stlen + 1); 656 | bi = 0; 657 | } 658 | } else if (!isspace(c)) { 659 | buf[bi++] = str[i]; 660 | } 661 | } 662 | /* tail */ 663 | if (bi > 0) { 664 | tmp = strdupl(buf, bi); 665 | if (!tmp) goto return_error; 666 | res[ri++] = tmp; 667 | } 668 | cron_free(buf); 669 | *len_out = len; 670 | return res; 671 | 672 | return_error: 673 | if (buf) { 674 | cron_free(buf); 675 | } 676 | free_splitted(res, len); 677 | *len_out = 0; 678 | return NULL; 679 | } 680 | 681 | static char* replace_ordinals(char* value, const char* const * arr, size_t arr_len) { 682 | size_t i; 683 | char* cur = value; 684 | char* res = NULL; 685 | int first = 1; 686 | for (i = 0; i < arr_len; i++) { 687 | char* strnum = to_string((int) i); 688 | if (!strnum) { 689 | if (!first) { 690 | cron_free(cur); 691 | } 692 | return NULL; 693 | } 694 | res = str_replace(cur, arr[i], strnum); 695 | cron_free(strnum); 696 | if (!first) { 697 | cron_free(cur); 698 | } 699 | if (!res) { 700 | return NULL; 701 | } 702 | cur = res; 703 | if (first) { 704 | first = 0; 705 | } 706 | } 707 | return res; 708 | } 709 | 710 | static int has_char(char* str, char ch) { 711 | size_t i; 712 | size_t len = 0; 713 | if (!str) return 0; 714 | len = strlen(str); 715 | for (i = 0; i < len; i++) { 716 | if (str[i] == ch) return 1; 717 | } 718 | return 0; 719 | } 720 | 721 | static unsigned int* get_range(char* field, unsigned int min, unsigned int max, const char** error) { 722 | 723 | char** parts = NULL; 724 | size_t len = 0; 725 | unsigned int* res = (unsigned int*) cron_malloc(2 * sizeof(unsigned int)); 726 | if (!res) goto return_error; 727 | if (!field) goto return_error; 728 | 729 | res[0] = 0; 730 | res[1] = 0; 731 | if (1 == strlen(field) && '*' == field[0]) { 732 | res[0] = min; 733 | res[1] = max - 1; 734 | } else if (!(has_char(field, '-') || has_char(field, '~'))) { 735 | int err = 0; 736 | unsigned int val = parse_uint(field, &err); 737 | if (err) { 738 | *error = "Unsigned integer parse error 1"; 739 | goto return_error; 740 | } 741 | 742 | res[0] = val; 743 | res[1] = val; 744 | } else { 745 | if (has_char(field, '-')) { 746 | parts = split_str(field, '-', &len); 747 | } 748 | else if (has_char(field, '~')) { 749 | parts = split_str(field, '~', &len); 750 | } 751 | 752 | if (2 != len) { 753 | *error = "Specified range requires two fields"; 754 | goto return_error; 755 | } 756 | int err = 0; 757 | res[0] = parse_uint(parts[0], &err); 758 | if (err) { 759 | *error = "Unsigned integer parse error 2"; 760 | goto return_error; 761 | } 762 | res[1] = parse_uint(parts[1], &err); 763 | if (err) { 764 | *error = "Unsigned integer parse error 3"; 765 | goto return_error; 766 | } 767 | } 768 | if (res[0] >= max || res[1] >= max) { 769 | *error = "Specified range exceeds maximum"; 770 | goto return_error; 771 | } 772 | if (res[0] < min || res[1] < min) { 773 | *error = "Specified range is less than minimum"; 774 | goto return_error; 775 | } 776 | if (res[0] > res[1]) { 777 | *error = "Specified range start exceeds range end"; 778 | goto return_error; 779 | } 780 | 781 | free_splitted(parts, len); 782 | *error = NULL; 783 | return res; 784 | 785 | return_error: 786 | free_splitted(parts, len); 787 | if (res) { 788 | cron_free(res); 789 | } 790 | 791 | return NULL; 792 | } 793 | 794 | static void set_number_hits(const char* value, uint8_t* target, unsigned int min, unsigned int max, const char** error) { 795 | size_t i; 796 | unsigned int i1; 797 | size_t len = 0; 798 | 799 | char** fields = split_str(value, ',', &len); 800 | if (!fields) { 801 | *error = "Comma split error"; 802 | goto return_result; 803 | } 804 | 805 | for (i = 0; i < len; i++) { 806 | if (!has_char(fields[i], '/')) { 807 | /* Not an incrementer so it must be a range (possibly empty) */ 808 | 809 | unsigned int* range = get_range(fields[i], min, max, error); 810 | 811 | if (*error) { 812 | if (range) { 813 | cron_free(range); 814 | } 815 | goto return_result; 816 | 817 | } 818 | 819 | if (has_char(fields[i], '~')) { 820 | i1 = (random() % (range[1] - range[0] + 1)) + range[0]; 821 | cron_set_bit(target, i1); 822 | } 823 | else { 824 | for (i1 = range[0]; i1 <= range[1]; i1++) { 825 | cron_set_bit(target, i1); 826 | } 827 | } 828 | cron_free(range); 829 | 830 | } else { 831 | size_t len2 = 0; 832 | char** split = split_str(fields[i], '/', &len2); 833 | if (2 != len2) { 834 | *error = "Incrementer must have two fields"; 835 | free_splitted(split, len2); 836 | goto return_result; 837 | } 838 | unsigned int* range = get_range(split[0], min, max, error); 839 | if (*error) { 840 | if (range) { 841 | cron_free(range); 842 | } 843 | free_splitted(split, len2); 844 | goto return_result; 845 | } 846 | if (!(has_char(split[0], '-') || has_char(split[0], '~'))) { 847 | range[1] = max - 1; 848 | } 849 | int err = 0; 850 | unsigned int delta = parse_uint(split[1], &err); 851 | if (err) { 852 | *error = "Unsigned integer parse error 4"; 853 | cron_free(range); 854 | free_splitted(split, len2); 855 | goto return_result; 856 | } 857 | if (0 == delta) { 858 | *error = "Incrementer may not be zero"; 859 | cron_free(range); 860 | free_splitted(split, len2); 861 | goto return_result; 862 | } 863 | if (has_char(fields[i], '~')) { 864 | i1 = ((random() % (range[1] - range[0] + 1)) / delta) * delta + 865 | range[0]; 866 | cron_set_bit(target, i1); 867 | } 868 | else { 869 | for (i1 = range[0]; i1 <= range[1]; i1 += delta) { 870 | cron_set_bit(target, i1); 871 | } 872 | } 873 | free_splitted(split, len2); 874 | cron_free(range); 875 | 876 | } 877 | } 878 | goto return_result; 879 | 880 | return_result: 881 | free_splitted(fields, len); 882 | 883 | } 884 | 885 | static void set_months(char* value, uint8_t* targ, const char** error) { 886 | unsigned int i; 887 | unsigned int max = 12; 888 | 889 | char* replaced = NULL; 890 | 891 | to_upper(value); 892 | replaced = replace_ordinals(value, MONTHS_ARR, CRON_MONTHS_ARR_LEN); 893 | if (!replaced) { 894 | *error = "Invalid month format"; 895 | return; 896 | } 897 | set_number_hits(replaced, targ, 1, max + 1, error); 898 | cron_free(replaced); 899 | 900 | /* ... and then rotate it to the front of the months */ 901 | for (i = 1; i <= max; i++) { 902 | if (cron_get_bit(targ, i)) { 903 | cron_set_bit(targ, i - 1); 904 | cron_del_bit(targ, i); 905 | } 906 | } 907 | } 908 | 909 | static void set_days_of_week(char* field, uint8_t* targ, const char** error) { 910 | unsigned int max = 7; 911 | char* replaced = NULL; 912 | 913 | if (1 == strlen(field) && '?' == field[0]) { 914 | field[0] = '*'; 915 | } 916 | to_upper(field); 917 | replaced = replace_ordinals(field, DAYS_ARR, CRON_DAYS_ARR_LEN); 918 | if (!replaced) { 919 | *error = "Invalid day format"; 920 | return; 921 | } 922 | set_number_hits(replaced, targ, 0, max + 1, error); 923 | cron_free(replaced); 924 | if (cron_get_bit(targ, 7)) { 925 | /* Sunday can be represented as 0 or 7*/ 926 | cron_set_bit(targ, 0); 927 | cron_del_bit(targ, 7); 928 | } 929 | } 930 | 931 | static void set_days_of_month(char* field, uint8_t* targ, const char** error) { 932 | /* Days of month start with 1 (in Cron and Calendar) so add one */ 933 | if (1 == strlen(field) && '?' == field[0]) { 934 | field[0] = '*'; 935 | } 936 | set_number_hits(field, targ, 1, CRON_MAX_DAYS_OF_MONTH, error); 937 | } 938 | 939 | void cron_parse_expr(const char* expression, cron_expr* target, const char** error) { 940 | const char* err_local; 941 | size_t len = 0; 942 | char** fields = NULL; 943 | if (!error) { 944 | error = &err_local; 945 | } 946 | *error = NULL; 947 | if (!expression) { 948 | *error = "Invalid NULL expression"; 949 | goto return_res; 950 | } 951 | if (!target) { 952 | *error = "Invalid NULL target"; 953 | goto return_res; 954 | } 955 | 956 | fields = split_str(expression, ' ', &len); 957 | if (len != 6) { 958 | *error = "Invalid number of fields, expression must consist of 6 fields"; 959 | goto return_res; 960 | } 961 | memset(target, 0, sizeof(*target)); 962 | set_number_hits(fields[0], target->seconds, 0, 60, error); 963 | if (*error) goto return_res; 964 | set_number_hits(fields[1], target->minutes, 0, 60, error); 965 | if (*error) goto return_res; 966 | set_number_hits(fields[2], target->hours, 0, 24, error); 967 | if (*error) goto return_res; 968 | set_days_of_month(fields[3], target->days_of_month, error); 969 | if (*error) goto return_res; 970 | set_months(fields[4], target->months, error); 971 | if (*error) goto return_res; 972 | set_days_of_week(fields[5], target->days_of_week, error); 973 | if (*error) goto return_res; 974 | 975 | goto return_res; 976 | 977 | return_res: 978 | free_splitted(fields, len); 979 | } 980 | 981 | time_t cron_next(cron_expr* expr, time_t date) { 982 | /* 983 | The plan: 984 | 985 | 1 Round up to the next whole second 986 | 987 | 2 If seconds match move on, otherwise find the next match: 988 | 2.1 If next match is in the next minute then roll forwards 989 | 990 | 3 If minute matches move on, otherwise find the next match 991 | 3.1 If next match is in the next hour then roll forwards 992 | 3.2 Reset the seconds and go to 2 993 | 994 | 4 If hour matches move on, otherwise find the next match 995 | 4.1 If next match is in the next day then roll forwards, 996 | 4.2 Reset the minutes and seconds and go to 2 997 | 998 | ... 999 | */ 1000 | if (!expr) return CRON_INVALID_INSTANT; 1001 | struct tm calval; 1002 | memset(&calval, 0, sizeof(struct tm)); 1003 | struct tm* calendar = cron_time(&date, &calval); 1004 | if (!calendar) return CRON_INVALID_INSTANT; 1005 | time_t original = cron_mktime(calendar); 1006 | if (CRON_INVALID_INSTANT == original) return CRON_INVALID_INSTANT; 1007 | 1008 | int res = do_next(expr, calendar, calendar->tm_year); 1009 | if (0 != res) return CRON_INVALID_INSTANT; 1010 | 1011 | time_t calculated = cron_mktime(calendar); 1012 | if (CRON_INVALID_INSTANT == calculated) return CRON_INVALID_INSTANT; 1013 | if (calculated == original) { 1014 | /* We arrived at the original timestamp - round up to the next whole second and try again... */ 1015 | res = add_to_field(calendar, CRON_CF_SECOND, 1); 1016 | if (0 != res) return CRON_INVALID_INSTANT; 1017 | res = do_next(expr, calendar, calendar->tm_year); 1018 | if (0 != res) return CRON_INVALID_INSTANT; 1019 | } 1020 | 1021 | return cron_mktime(calendar); 1022 | } 1023 | 1024 | 1025 | /* https://github.com/staticlibs/ccronexpr/pull/8 */ 1026 | 1027 | static unsigned int prev_set_bit(uint8_t* bits, int from_index, int to_index, int* notfound) { 1028 | int i; 1029 | if (!bits) { 1030 | *notfound = 1; 1031 | return 0; 1032 | } 1033 | for (i = from_index; i >= to_index; i--) { 1034 | if (cron_get_bit(bits, i)) return i; 1035 | } 1036 | *notfound = 1; 1037 | return 0; 1038 | } 1039 | 1040 | static int last_day_of_month(int month, int year) { 1041 | struct tm cal; 1042 | time_t t; 1043 | memset(&cal,0,sizeof(cal)); 1044 | cal.tm_sec=0; 1045 | cal.tm_min=0; 1046 | cal.tm_hour=0; 1047 | cal.tm_mon = month+1; 1048 | cal.tm_mday = 0; 1049 | cal.tm_year=year; 1050 | t=mktime(&cal); 1051 | return gmtime(&t)->tm_mday; 1052 | } 1053 | 1054 | /** 1055 | * Reset the calendar setting all the fields provided to zero. 1056 | */ 1057 | static int reset_max(struct tm* calendar, int field) { 1058 | if (!calendar || -1 == field) { 1059 | return 1; 1060 | } 1061 | switch (field) { 1062 | case CRON_CF_SECOND: 1063 | calendar->tm_sec = 59; 1064 | break; 1065 | case CRON_CF_MINUTE: 1066 | calendar->tm_min = 59; 1067 | break; 1068 | case CRON_CF_HOUR_OF_DAY: 1069 | calendar->tm_hour = 23; 1070 | break; 1071 | case CRON_CF_DAY_OF_WEEK: 1072 | calendar->tm_wday = 6; 1073 | break; 1074 | case CRON_CF_DAY_OF_MONTH: 1075 | calendar->tm_mday = last_day_of_month(calendar->tm_mon, calendar->tm_year); 1076 | break; 1077 | case CRON_CF_MONTH: 1078 | calendar->tm_mon = 11; 1079 | break; 1080 | case CRON_CF_YEAR: 1081 | /* I don't think this is supposed to happen ... */ 1082 | fprintf(stderr, "reset CRON_CF_YEAR\n"); 1083 | break; 1084 | default: 1085 | return 1; /* unknown field */ 1086 | } 1087 | time_t res = cron_mktime(calendar); 1088 | if (CRON_INVALID_INSTANT == res) { 1089 | return 1; 1090 | } 1091 | return 0; 1092 | } 1093 | 1094 | static int reset_all_max(struct tm* calendar, int* fields) { 1095 | int i; 1096 | int res = 0; 1097 | if (!calendar || !fields) { 1098 | return 1; 1099 | } 1100 | for (i = 0; i < CRON_CF_ARR_LEN; i++) { 1101 | if (-1 != fields[i]) { 1102 | res = reset_max(calendar, fields[i]); 1103 | if (0 != res) return res; 1104 | } 1105 | } 1106 | return 0; 1107 | } 1108 | 1109 | /** 1110 | * Search the bits provided for the next set bit after the value provided, 1111 | * and reset the calendar. 1112 | */ 1113 | static unsigned int find_prev(uint8_t* bits, unsigned int max, unsigned int value, struct tm* calendar, unsigned int field, unsigned int nextField, int* lower_orders, int* res_out) { 1114 | int notfound = 0; 1115 | int err = 0; 1116 | unsigned int next_value = prev_set_bit(bits, value, 0, ¬found); 1117 | /* roll under if needed */ 1118 | if (notfound) { 1119 | err = add_to_field(calendar, nextField, -1); 1120 | if (err) goto return_error; 1121 | err = reset_max(calendar, field); 1122 | if (err) goto return_error; 1123 | notfound = 0; 1124 | next_value = prev_set_bit(bits, max - 1, value, ¬found); 1125 | } 1126 | if (notfound || next_value != value) { 1127 | err = set_field(calendar, field, next_value); 1128 | if (err) goto return_error; 1129 | err = reset_all_max(calendar, lower_orders); 1130 | if (err) goto return_error; 1131 | } 1132 | return next_value; 1133 | 1134 | return_error: 1135 | *res_out = 1; 1136 | return 0; 1137 | } 1138 | 1139 | static unsigned int find_prev_day(struct tm* calendar, uint8_t* days_of_month, unsigned int day_of_month, uint8_t* days_of_week, unsigned int day_of_week, int* resets, int* res_out) { 1140 | int err; 1141 | unsigned int count = 0; 1142 | unsigned int max = 366; 1143 | while ((!cron_get_bit(days_of_month, day_of_month) || !cron_get_bit(days_of_week, day_of_week)) && count++ < max) { 1144 | err = add_to_field(calendar, CRON_CF_DAY_OF_MONTH, -1); 1145 | 1146 | if (err) goto return_error; 1147 | day_of_month = calendar->tm_mday; 1148 | day_of_week = calendar->tm_wday; 1149 | reset_all_max(calendar, resets); 1150 | } 1151 | return day_of_month; 1152 | 1153 | return_error: 1154 | *res_out = 1; 1155 | return 0; 1156 | } 1157 | 1158 | static int do_prev(cron_expr* expr, struct tm* calendar, unsigned int dot) { 1159 | int i; 1160 | int res = 0; 1161 | int* resets = NULL; 1162 | int* empty_list = NULL; 1163 | unsigned int second = 0; 1164 | unsigned int update_second = 0; 1165 | unsigned int minute = 0; 1166 | unsigned int update_minute = 0; 1167 | unsigned int hour = 0; 1168 | unsigned int update_hour = 0; 1169 | unsigned int day_of_week = 0; 1170 | unsigned int day_of_month = 0; 1171 | unsigned int update_day_of_month = 0; 1172 | unsigned int month = 0; 1173 | unsigned int update_month = 0; 1174 | 1175 | resets = (int*) cron_malloc(CRON_CF_ARR_LEN * sizeof(int)); 1176 | if (!resets) goto return_result; 1177 | empty_list = (int*) cron_malloc(CRON_CF_ARR_LEN * sizeof(int)); 1178 | if (!empty_list) goto return_result; 1179 | for (i = 0; i < CRON_CF_ARR_LEN; i++) { 1180 | resets[i] = -1; 1181 | empty_list[i] = -1; 1182 | } 1183 | 1184 | second = calendar->tm_sec; 1185 | update_second = find_prev(expr->seconds, CRON_MAX_SECONDS, second, calendar, CRON_CF_SECOND, CRON_CF_MINUTE, empty_list, &res); 1186 | if (0 != res) goto return_result; 1187 | if (second == update_second) { 1188 | push_to_fields_arr(resets, CRON_CF_SECOND); 1189 | } 1190 | 1191 | minute = calendar->tm_min; 1192 | update_minute = find_prev(expr->minutes, CRON_MAX_MINUTES, minute, calendar, CRON_CF_MINUTE, CRON_CF_HOUR_OF_DAY, resets, &res); 1193 | if (0 != res) goto return_result; 1194 | if (minute == update_minute) { 1195 | push_to_fields_arr(resets, CRON_CF_MINUTE); 1196 | } else { 1197 | res = do_prev(expr, calendar, dot); 1198 | if (0 != res) goto return_result; 1199 | } 1200 | 1201 | hour = calendar->tm_hour; 1202 | update_hour = find_prev(expr->hours, CRON_MAX_HOURS, hour, calendar, CRON_CF_HOUR_OF_DAY, CRON_CF_DAY_OF_WEEK, resets, &res); 1203 | if (0 != res) goto return_result; 1204 | if (hour == update_hour) { 1205 | push_to_fields_arr(resets, CRON_CF_HOUR_OF_DAY); 1206 | } else { 1207 | res = do_prev(expr, calendar, dot); 1208 | if (0 != res) goto return_result; 1209 | } 1210 | 1211 | day_of_week = calendar->tm_wday; 1212 | day_of_month = calendar->tm_mday; 1213 | update_day_of_month = find_prev_day(calendar, expr->days_of_month, day_of_month, expr->days_of_week, day_of_week, resets, &res); 1214 | if (0 != res) goto return_result; 1215 | if (day_of_month == update_day_of_month) { 1216 | push_to_fields_arr(resets, CRON_CF_DAY_OF_MONTH); 1217 | } else { 1218 | res = do_prev(expr, calendar, dot); 1219 | if (0 != res) goto return_result; 1220 | } 1221 | 1222 | month = calendar->tm_mon; /*day already adds one if no day in same month is found*/ 1223 | update_month = find_prev(expr->months, CRON_MAX_MONTHS, month, calendar, CRON_CF_MONTH, CRON_CF_YEAR, resets, &res); 1224 | if (0 != res) goto return_result; 1225 | if (month != update_month) { 1226 | if (dot - calendar->tm_year > CRON_MAX_YEARS_DIFF) { 1227 | res = -1; 1228 | goto return_result; 1229 | } 1230 | res = do_prev(expr, calendar, dot); 1231 | if (0 != res) goto return_result; 1232 | } 1233 | goto return_result; 1234 | 1235 | return_result: 1236 | if (!resets || !empty_list) { 1237 | res = -1; 1238 | } 1239 | if (resets) { 1240 | cron_free(resets); 1241 | } 1242 | if (empty_list) { 1243 | cron_free(empty_list); 1244 | } 1245 | return res; 1246 | } 1247 | 1248 | time_t cron_prev(cron_expr* expr, time_t date) { 1249 | /* 1250 | The plan: 1251 | 1252 | 1 Round down to a whole second 1253 | 1254 | 2 If seconds match move on, otherwise find the next match: 1255 | 2.1 If next match is in the next minute then roll forwards 1256 | 1257 | 3 If minute matches move on, otherwise find the next match 1258 | 3.1 If next match is in the next hour then roll forwards 1259 | 3.2 Reset the seconds and go to 2 1260 | 1261 | 4 If hour matches move on, otherwise find the next match 1262 | 4.1 If next match is in the next day then roll forwards, 1263 | 4.2 Reset the minutes and seconds and go to 2 1264 | 1265 | ... 1266 | */ 1267 | if (!expr) return CRON_INVALID_INSTANT; 1268 | struct tm calval; 1269 | memset(&calval, 0, sizeof(struct tm)); 1270 | struct tm* calendar = cron_time(&date, &calval); 1271 | if (!calendar) return CRON_INVALID_INSTANT; 1272 | time_t original = cron_mktime(calendar); 1273 | if (CRON_INVALID_INSTANT == original) return CRON_INVALID_INSTANT; 1274 | 1275 | /* calculate the previous occurrence */ 1276 | int res = do_prev(expr, calendar, calendar->tm_year); 1277 | if (0 != res) return CRON_INVALID_INSTANT; 1278 | 1279 | /* check for a match, try from the next second if one wasn't found */ 1280 | time_t calculated = cron_mktime(calendar); 1281 | if (CRON_INVALID_INSTANT == calculated) return CRON_INVALID_INSTANT; 1282 | if (calculated == original) { 1283 | /* We arrived at the original timestamp - round up to the next whole second and try again... */ 1284 | res = add_to_field(calendar, CRON_CF_SECOND, -1); 1285 | if (0 != res) return CRON_INVALID_INSTANT; 1286 | res = do_prev(expr, calendar, calendar->tm_year); 1287 | if (0 != res) return CRON_INVALID_INSTANT; 1288 | } 1289 | 1290 | return cron_mktime(calendar); 1291 | } 1292 | -------------------------------------------------------------------------------- /ccronexpr.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015, alex at staticlibs.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * File: ccronexpr.h 19 | * Author: alex 20 | * 21 | * Created on February 24, 2015, 9:35 AM 22 | */ 23 | 24 | #ifndef CCRONEXPR_H 25 | #define CCRONEXPR_H 26 | 27 | #if defined(__cplusplus) && !defined(CRON_COMPILE_AS_CXX) 28 | extern "C" { 29 | #endif 30 | 31 | #ifndef ANDROID 32 | #include 33 | #else /* ANDROID */ 34 | #include 35 | #endif /* ANDROID */ 36 | 37 | #include /*added for use if uint*_t data types*/ 38 | 39 | /** 40 | * Parsed cron expression 41 | */ 42 | typedef struct { 43 | uint8_t seconds[8]; 44 | uint8_t minutes[8]; 45 | uint8_t hours[3]; 46 | uint8_t days_of_week[1]; 47 | uint8_t days_of_month[4]; 48 | uint8_t months[2]; 49 | } cron_expr; 50 | 51 | /** 52 | * Parses specified cron expression. 53 | * 54 | * @param expression cron expression as nul-terminated string, 55 | * should be no longer that 256 bytes 56 | * @param pointer to cron expression structure, it's client code responsibility 57 | * to free/destroy it afterwards 58 | * @param error output error message, will be set to string literal 59 | * error message in case of error. Will be set to NULL on success. 60 | * The error message should NOT be freed by client. 61 | */ 62 | void cron_parse_expr(const char* expression, cron_expr* target, const char** error); 63 | 64 | /** 65 | * Uses the specified expression to calculate the next 'fire' date after 66 | * the specified date. All dates are processed as UTC (GMT) dates 67 | * without timezones information. To use local dates (current system timezone) 68 | * instead of GMT compile with '-DCRON_USE_LOCAL_TIME' 69 | * 70 | * @param expr parsed cron expression to use in next date calculation 71 | * @param date start date to start calculation from 72 | * @return next 'fire' date in case of success, '((time_t) -1)' in case of error. 73 | */ 74 | time_t cron_next(cron_expr* expr, time_t date); 75 | 76 | /** 77 | * Uses the specified expression to calculate the previous 'fire' date after 78 | * the specified date. All dates are processed as UTC (GMT) dates 79 | * without timezones information. To use local dates (current system timezone) 80 | * instead of GMT compile with '-DCRON_USE_LOCAL_TIME' 81 | * 82 | * @param expr parsed cron expression to use in previous date calculation 83 | * @param date start date to start calculation from 84 | * @return previous 'fire' date in case of success, '((time_t) -1)' in case of error. 85 | */ 86 | time_t cron_prev(cron_expr* expr, time_t date); 87 | 88 | 89 | #if defined(__cplusplus) && !defined(CRON_COMPILE_AS_CXX) 90 | } /* extern "C"*/ 91 | #endif 92 | 93 | #endif /* CCRONEXPR_H */ 94 | 95 | 96 | -------------------------------------------------------------------------------- /cronevent.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2023, Michael Santos 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted, provided that the above 5 | * copyright notice and this permission notice appear in all copies. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | */ 15 | #include "runcron.h" 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #ifdef RESTRICT_PROCESS_capsicum 28 | #include 29 | #endif 30 | 31 | #include "cronevent.h" 32 | #include "limit_process.h" 33 | #include "restrict_process.h" 34 | #include "waitfor.h" 35 | 36 | #include "ccronexpr.h" 37 | 38 | static int cronexpr_proc(runcron_t *rp, char *cronentry, unsigned int *seconds, 39 | time_t now); 40 | static int cronexpr(runcron_t *rp, char *cronentry, unsigned int *seconds, 41 | time_t now); 42 | static int fields(const char *s); 43 | static int arg_to_timespec(const char *arg, size_t arglen, char *buf, 44 | size_t buflen); 45 | static const char *alias_to_timespec(const char *name); 46 | 47 | static struct runcron_alias { 48 | const char *name; 49 | const char *timespec; 50 | } runcron_aliases[] = { 51 | {"@yearly", "0~59 0~59 0~23 1~28 1~12 *"}, 52 | {"@annually", "0~59 0~59 0~23 1~28 1~12 *"}, 53 | {"@monthly", "0~59 0~59 0~23 1~28 * *"}, 54 | {"@weekly", "0~59 0~59 0~23 * * 1~7"}, 55 | {"@daily", "0~59 0~59 0~23 * * *"}, 56 | {"@hourly", "0~59 0~59 * * * *"}, 57 | 58 | {"=yearly", "0 0 0 1 1 *"}, 59 | {"=annually", "0 0 0 1 1 *"}, 60 | {"=monthly", "0 0 0 1 * *"}, 61 | {"=weekly", "0 0 0 * * 0"}, 62 | {"=daily", "0 0 0 * * *"}, 63 | {"=hourly", "0 0 * * * *"}, 64 | 65 | {"@midnight", "0 0 0 * * *"}, 66 | {"=midnight", "0 0 0 * * *"}, 67 | 68 | {"@reboot", "@reboot"}, 69 | {"=reboot", "@reboot"}, 70 | 71 | {NULL, NULL}, 72 | }; 73 | 74 | int cronevent(runcron_t *rp, char *cronentry, unsigned int *seconds, 75 | time_t now) { 76 | return (rp->opt & OPT_DISABLE_PROCESS_RESTRICTIONS) 77 | ? cronexpr(rp, cronentry, seconds, now) 78 | : cronexpr_proc(rp, cronentry, seconds, now); 79 | } 80 | 81 | static int cronexpr_proc(runcron_t *rp, char *cronentry, unsigned int *sec, 82 | time_t now) { 83 | pid_t pid; 84 | int sv[2]; 85 | unsigned int seconds; 86 | int status; 87 | int exit_value = 0; 88 | int n; 89 | int fdp = -1; 90 | 91 | if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) 92 | return -1; 93 | 94 | #ifdef RESTRICT_PROCESS_capsicum 95 | pid = pdfork(&fdp, PD_CLOEXEC); 96 | #else 97 | pid = fork(); 98 | #endif 99 | 100 | switch (pid) { 101 | case -1: 102 | n = errno; 103 | (void)close(sv[0]); 104 | (void)close(sv[1]); 105 | errno = n; 106 | return -1; 107 | 108 | case 0: 109 | if (close(sv[0]) < 0) 110 | exit(111); 111 | if (limit_process(rp) < 0) 112 | exit(111); 113 | if (restrict_process() < 0) 114 | exit(111); 115 | exit_value = cronexpr(rp, cronentry, &seconds, now); 116 | if (exit_value < 0) 117 | _exit(128); 118 | 119 | while ((n = write(sv[1], &seconds, sizeof(seconds))) == -1 && 120 | errno == EINTR) 121 | ; 122 | 123 | if (n != sizeof(seconds)) 124 | _exit(111); 125 | 126 | _exit(0); 127 | 128 | default: 129 | if (close(sv[1]) < 0) 130 | return -1; 131 | 132 | if (waitfor(fdp, &status) < 0) 133 | return -1; 134 | 135 | if (WIFEXITED(status)) 136 | exit_value = WEXITSTATUS(status); 137 | else if (WIFSIGNALED(status)) 138 | exit_value = 128 + WTERMSIG(status); 139 | 140 | switch (exit_value) { 141 | case 0: 142 | break; 143 | 144 | case (128 + SIGXCPU): 145 | (void)fprintf( 146 | stderr, 147 | "error: cron expression parsing exceeded allotted runtime: %s\n", 148 | cronentry); 149 | return -1; 150 | 151 | case (128 + SIGSEGV): 152 | (void)fprintf( 153 | stderr, 154 | "error: cron expression parsing exceeded allotted memory usage: %s\n", 155 | cronentry); 156 | return -1; 157 | 158 | default: 159 | return -1; 160 | } 161 | 162 | while ((n = read(sv[0], &seconds, sizeof(seconds))) == -1 && errno == EINTR) 163 | ; 164 | 165 | if (n != sizeof(seconds)) 166 | return -1; 167 | 168 | *sec = seconds; 169 | 170 | if (close(sv[0]) < 0) 171 | return -1; 172 | } 173 | 174 | return 0; 175 | } 176 | 177 | static int cronexpr(runcron_t *rp, char *cronentry, unsigned int *seconds, 178 | time_t now) { 179 | cron_expr expr = {0}; 180 | const char *errbuf = NULL; 181 | char buf[255] = {0}; 182 | char arg[252] = {0}; 183 | char *p; 184 | time_t next; 185 | double diff; 186 | int rv; 187 | 188 | rv = snprintf(arg, sizeof(arg), "%s", cronentry); 189 | if (rv < 0 || (unsigned)rv >= sizeof(arg)) { 190 | warnx("error: timespec exceeds maximum length: %zu", sizeof(arg)); 191 | return -1; 192 | } 193 | 194 | /* replace tabs with spaces */ 195 | for (p = arg; *p != '\0'; p++) 196 | if (*p == '\t' || *p == '\n' || *p == '\r') 197 | *p = ' '; 198 | 199 | if (arg_to_timespec(arg, sizeof(arg), buf, sizeof(buf)) < 0) { 200 | warnx("error: invalid crontab timespec"); 201 | return -1; 202 | } 203 | 204 | if (rp->verbose > 1) 205 | (void)fprintf(stderr, "crontab=%s\n", buf); 206 | 207 | if (strcmp(buf, "@reboot") == 0) { 208 | *seconds = UINT32_MAX; 209 | return 0; 210 | } 211 | 212 | cron_parse_expr(buf, &expr, &errbuf); 213 | if (errbuf) { 214 | warnx("error: invalid crontab timespec: %s", errbuf); 215 | return -1; 216 | } 217 | 218 | next = cron_next(&expr, now); 219 | if (next == -1) { 220 | warnx("error: cron_next: %s: %s", cronentry, 221 | errno == 0 ? "invalid timespec" : strerror(errno)); 222 | return -1; 223 | } 224 | 225 | if (rp->verbose > 0) { 226 | (void)fprintf(stderr, "now[%lld]=%s", (long long)now, ctime(&now)); 227 | (void)fprintf(stderr, "next[%lld]=%s", (long long)next, ctime(&next)); 228 | } 229 | 230 | diff = difftime(next, now); 231 | if (diff < 0) { 232 | warnx("error: difftime: negative duration: %.f seconds", diff); 233 | return -1; 234 | } 235 | 236 | *seconds = diff > UINT32_MAX ? UINT32_MAX : (unsigned int)diff; 237 | return 0; 238 | } 239 | 240 | static int fields(const char *s) { 241 | int n = 0; 242 | const char *p = s; 243 | int field = 0; 244 | 245 | for (; *p != '\0'; p++) { 246 | if (*p != ' ') { 247 | if (!field) { 248 | n++; 249 | field = 1; 250 | } 251 | } else { 252 | field = 0; 253 | } 254 | } 255 | 256 | return n; 257 | } 258 | 259 | static int arg_to_timespec(const char *arg, size_t arglen, char *buf, 260 | size_t buflen) { 261 | const char *timespec; 262 | int n; 263 | int rv; 264 | 265 | n = fields(arg); 266 | 267 | switch (n) { 268 | case 1: 269 | timespec = alias_to_timespec(arg); 270 | 271 | if (timespec == NULL) 272 | return -1; 273 | 274 | rv = snprintf(buf, buflen, "%s", timespec); 275 | break; 276 | 277 | case 5: 278 | rv = snprintf(buf, buflen, "0 %s", arg); 279 | break; 280 | 281 | case 6: 282 | default: 283 | rv = snprintf(buf, buflen, "%s", arg); 284 | break; 285 | } 286 | 287 | return (rv < 0 || (unsigned)rv >= buflen) ? -1 : 0; 288 | } 289 | 290 | static const char *alias_to_timespec(const char *name) { 291 | struct runcron_alias *ap; 292 | 293 | for (ap = runcron_aliases; ap->name != NULL; ap++) { 294 | if (strcmp(name, ap->name) == 0) { 295 | return ap->timespec; 296 | } 297 | } 298 | 299 | return NULL; 300 | } 301 | -------------------------------------------------------------------------------- /cronevent.h: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2020, Michael Santos 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted, provided that the above 5 | * copyright notice and this permission notice appear in all copies. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | */ 15 | int cronevent(runcron_t *rp, char *cronentry, unsigned int *seconds, 16 | time_t now); 17 | -------------------------------------------------------------------------------- /fnv1a.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "fnv1a.h" 5 | 6 | static const uint32_t fnv1a32_prime = 16777619; 7 | static const uint32_t fnv1a32_offset = 2166136261U; 8 | 9 | uint32_t fnv1a(uint8_t *buf, size_t buf_size) { 10 | uint32_t h = fnv1a32_offset; 11 | for (size_t i = 0; i < buf_size; i++) { 12 | h = (h ^ buf[i]) * fnv1a32_prime; 13 | } 14 | return h; 15 | } 16 | -------------------------------------------------------------------------------- /fnv1a.h: -------------------------------------------------------------------------------- 1 | uint32_t fnv1a(uint8_t *buf, size_t buf_size); 2 | -------------------------------------------------------------------------------- /limit_process.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2023, Michael Santos 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted, provided that the above 5 | * copyright notice and this permission notice appear in all copies. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | */ 15 | #include "runcron.h" 16 | #include 17 | #include 18 | 19 | #include "limit_process.h" 20 | 21 | int limit_process(runcron_t *rp) { 22 | struct rlimit rl; 23 | 24 | #ifdef RLIMIT_CPU 25 | if (getrlimit(RLIMIT_CPU, &rl) < 0) 26 | return -1; 27 | 28 | rl.rlim_cur = rp->cpu; 29 | 30 | if (setrlimit(RLIMIT_CPU, &rl) < 0) 31 | return -1; 32 | #endif 33 | 34 | #ifdef RLIMIT_AS 35 | if (getrlimit(RLIMIT_AS, &rl) < 0) 36 | return -1; 37 | 38 | rl.rlim_cur = rp->as; 39 | 40 | if (setrlimit(RLIMIT_AS, &rl) < 0) 41 | return -1; 42 | #endif 43 | 44 | return 0; 45 | } 46 | -------------------------------------------------------------------------------- /limit_process.h: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019, Michael Santos 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted, provided that the above 5 | * copyright notice and this permission notice appear in all copies. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | */ 15 | int limit_process(runcron_t *rp); 16 | -------------------------------------------------------------------------------- /musl-make: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | MACHTYPE="$(uname -m)" 6 | case "${MACHTYPE}" in 7 | armv6l) ;& 8 | armv7l) MACHTYPE=arm ;; 9 | *) ;; 10 | esac 11 | 12 | export MUSL_INCLUDE="${MUSL_INCLUDE-/usr/local/lib}" 13 | export RUNCRON_LDFLAGS="-I$MUSL_INCLUDE/kernel-headers/generic/include -I$MUSL_INCLUDE/kernel-headers/${MACHTYPE}/include" 14 | export RUNCRON_CFLAGS="-g -Wall -fwrapv -pedantic" 15 | export CC="musl-gcc -static -Os" 16 | exec make $@ 17 | -------------------------------------------------------------------------------- /restrict_process.h: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2021, Michael Santos 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted, provided that the above 5 | * copyright notice and this permission notice appear in all copies. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | */ 15 | int disable_setuid_subprocess(void); 16 | int restrict_process_init(void); 17 | int restrict_process(void); 18 | int restrict_process_wait(int fdp); 19 | int restrict_process_signal_on_supervisor_exit(void); 20 | -------------------------------------------------------------------------------- /restrict_process_capsicum.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2022, Michael Santos 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted, provided that the above 5 | * copyright notice and this permission notice appear in all copies. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | */ 15 | #include "restrict_process.h" 16 | #ifdef RESTRICT_PROCESS_capsicum 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | #include 27 | 28 | int disable_setuid_subprocess(void) { 29 | #ifdef PROC_NO_NEW_PRIVS_CTL 30 | int data = PROC_NO_NEW_PRIVS_ENABLE; 31 | return procctl(P_PID, 0, PROC_NO_NEW_PRIVS_CTL, &data); 32 | #else 33 | return 0; 34 | #endif 35 | } 36 | 37 | int restrict_process_signal_on_supervisor_exit(void) { return 0; } 38 | 39 | int restrict_process_init(void) { return 0; } 40 | 41 | int restrict_process(void) { 42 | struct rlimit rl = {0}; 43 | cap_rights_t policy_read; 44 | cap_rights_t policy_write; 45 | 46 | if (setrlimit(RLIMIT_NPROC, &rl) < 0) 47 | return -1; 48 | 49 | (void)cap_rights_init(&policy_read, CAP_READ); 50 | (void)cap_rights_init(&policy_write, CAP_WRITE); 51 | 52 | if (cap_rights_limit(STDIN_FILENO, &policy_read) < 0) 53 | return -1; 54 | 55 | if (cap_rights_limit(STDOUT_FILENO, &policy_write) < 0) 56 | return -1; 57 | 58 | if (cap_rights_limit(STDERR_FILENO, &policy_write) < 0) 59 | return -1; 60 | 61 | return cap_enter(); 62 | } 63 | 64 | int restrict_process_wait(int fdp) { 65 | struct rlimit rl = {0}; 66 | cap_rights_t policy_read; 67 | cap_rights_t policy_write; 68 | cap_rights_t policy_rw; 69 | 70 | if (setrlimit(RLIMIT_NPROC, &rl) < 0) 71 | return -1; 72 | 73 | if (cap_enter() == -1) 74 | return -1; 75 | 76 | (void)cap_rights_init(&policy_read, CAP_READ); 77 | (void)cap_rights_init(&policy_write, CAP_WRITE); 78 | (void)cap_rights_init(&policy_rw, CAP_READ, CAP_WRITE, CAP_EVENT, CAP_PDKILL); 79 | 80 | if (cap_rights_limit(STDIN_FILENO, &policy_read) < 0) 81 | return -1; 82 | 83 | if (cap_rights_limit(STDOUT_FILENO, &policy_write) < 0) 84 | return -1; 85 | 86 | if (cap_rights_limit(STDERR_FILENO, &policy_write) < 0) 87 | return -1; 88 | 89 | if (cap_rights_limit(fdp, &policy_rw) < 0) 90 | return -1; 91 | 92 | return 0; 93 | } 94 | #endif 95 | -------------------------------------------------------------------------------- /restrict_process_null.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2023, Michael Santos 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted, provided that the above 5 | * copyright notice and this permission notice appear in all copies. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | */ 15 | #include "restrict_process.h" 16 | #ifdef RESTRICT_PROCESS_null 17 | int disable_setuid_subprocess(void) { return 0; } 18 | 19 | int restrict_process_signal_on_supervisor_exit(void) { return 0; } 20 | 21 | int restrict_process_init(void) { return 0; } 22 | 23 | int restrict_process(void) { return 0; } 24 | 25 | int restrict_process_wait(int fdp) { 26 | (void)fdp; 27 | return 0; 28 | } 29 | #endif 30 | -------------------------------------------------------------------------------- /restrict_process_pledge.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2023, Michael Santos 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted, provided that the above 5 | * copyright notice and this permission notice appear in all copies. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | */ 15 | #include "restrict_process.h" 16 | #ifdef RESTRICT_PROCESS_pledge 17 | #include 18 | #include 19 | 20 | #define PLEDGENAMES 21 | #include 22 | 23 | int disable_setuid_subprocess(void) { 24 | char execpromises[1024] = {0}; 25 | int i; 26 | 27 | for (i = 0; pledgenames[i].name != NULL; i++) { 28 | if ((strlcat(execpromises, pledgenames[i].name, sizeof(execpromises)) >= 29 | sizeof(execpromises)) || 30 | (strlcat(execpromises, " ", sizeof(execpromises)) >= 31 | sizeof(execpromises))) 32 | return -1; 33 | } 34 | return pledge(NULL, execpromises); 35 | } 36 | 37 | int restrict_process_signal_on_supervisor_exit(void) { return 0; } 38 | 39 | int restrict_process_init(void) { 40 | return pledge("stdio exec proc rpath wpath cpath flock", NULL); 41 | } 42 | 43 | int restrict_process(void) { return pledge("stdio", NULL); } 44 | 45 | int restrict_process_wait(int fdp) { 46 | (void)fdp; 47 | return pledge("stdio proc", NULL); 48 | } 49 | #endif 50 | -------------------------------------------------------------------------------- /restrict_process_rlimit.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2023, Michael Santos 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted, provided that the above 5 | * copyright notice and this permission notice appear in all copies. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | */ 15 | #include "restrict_process.h" 16 | #ifdef RESTRICT_PROCESS_rlimit 17 | #include 18 | #include 19 | 20 | int disable_setuid_subprocess(void) { return 0; } 21 | 22 | int restrict_process_signal_on_supervisor_exit(void) { return 0; } 23 | 24 | int restrict_process_init(void) { return 0; } 25 | 26 | int restrict_process(void) { 27 | struct rlimit rl_zero = {0}; 28 | 29 | if (setrlimit(RLIMIT_NPROC, &rl_zero) < 0) 30 | return -1; 31 | 32 | if (setrlimit(RLIMIT_FSIZE, &rl_zero) < 0) 33 | return -1; 34 | 35 | return setrlimit(RLIMIT_NOFILE, &rl_zero); 36 | } 37 | 38 | int restrict_process_wait(int fdp) { 39 | struct rlimit rl_zero = {0}; 40 | 41 | (void)fdp; 42 | 43 | if (setrlimit(RLIMIT_NPROC, &rl_zero) < 0) 44 | return -1; 45 | 46 | return setrlimit(RLIMIT_NOFILE, &rl_zero); 47 | } 48 | #endif 49 | -------------------------------------------------------------------------------- /restrict_process_seccomp.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2021, Michael Santos 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted, provided that the above 5 | * copyright notice and this permission notice appear in all copies. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | */ 15 | #include "restrict_process.h" 16 | #ifdef RESTRICT_PROCESS_seccomp 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include 23 | #include 24 | #include 25 | 26 | /* macros from openssh-7.2/sandbox-seccomp-filter.c */ 27 | 28 | /* Linux seccomp_filter sandbox */ 29 | #define SECCOMP_FILTER_FAIL SECCOMP_RET_KILL 30 | 31 | /* Use a signal handler to emit violations when debugging */ 32 | #ifdef RESTRICT_PROCESS_SECCOMP_FILTER_DEBUG 33 | #undef SECCOMP_FILTER_FAIL 34 | #define SECCOMP_FILTER_FAIL SECCOMP_RET_TRAP 35 | #endif /* RESTRICT_PROCESS_SECCOMP_FILTER_DEBUG */ 36 | 37 | /* Simple helpers to avoid manual errors (but larger BPF programs). */ 38 | #define SC_DENY(_nr, _errno) \ 39 | BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_##_nr, 0, 1), \ 40 | BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (_errno)) 41 | #define SC_ALLOW(_nr) \ 42 | BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_##_nr, 0, 1), \ 43 | BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) 44 | #define SC_ALLOW_ARG(_nr, _arg_nr, _arg_val) \ 45 | BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_##_nr, 0, \ 46 | 4), /* load first syscall argument */ \ 47 | BPF_STMT(BPF_LD + BPF_W + BPF_ABS, \ 48 | offsetof(struct seccomp_data, args[(_arg_nr)])), \ 49 | BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, (_arg_val), 0, 1), \ 50 | BPF_STMT(BPF_RET + BPF_K, \ 51 | SECCOMP_RET_ALLOW), /* reload syscall number; all rules expect \ 52 | it in accumulator */ \ 53 | BPF_STMT(BPF_LD + BPF_W + BPF_ABS, offsetof(struct seccomp_data, nr)) 54 | 55 | /* 56 | * http://outflux.net/teach-seccomp/ 57 | * https://github.com/gebi/teach-seccomp 58 | * 59 | */ 60 | #define syscall_nr (offsetof(struct seccomp_data, nr)) 61 | #define arch_nr (offsetof(struct seccomp_data, arch)) 62 | 63 | #if defined(__i386__) 64 | #define SECCOMP_AUDIT_ARCH AUDIT_ARCH_I386 65 | #elif defined(__x86_64__) 66 | #define SECCOMP_AUDIT_ARCH AUDIT_ARCH_X86_64 67 | #elif defined(__arm__) 68 | #define SECCOMP_AUDIT_ARCH AUDIT_ARCH_ARM 69 | #elif defined(__aarch64__) 70 | #define SECCOMP_AUDIT_ARCH AUDIT_ARCH_AARCH64 71 | #else 72 | #warning "seccomp: unsupported platform" 73 | #define SECCOMP_AUDIT_ARCH 0 74 | #endif 75 | 76 | int disable_setuid_subprocess(void) { 77 | return prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); 78 | } 79 | 80 | int restrict_process_signal_on_supervisor_exit(void) { 81 | return prctl(PR_SET_PDEATHSIG, 9); 82 | } 83 | 84 | int restrict_process_init(void) { return 0; } 85 | 86 | int restrict_process(void) { 87 | struct sock_filter filter[] = { 88 | /* Ensure the syscall arch convention is as expected. */ 89 | BPF_STMT(BPF_LD + BPF_W + BPF_ABS, offsetof(struct seccomp_data, arch)), 90 | BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, SECCOMP_AUDIT_ARCH, 1, 0), 91 | BPF_STMT(BPF_RET + BPF_K, SECCOMP_FILTER_FAIL), 92 | /* Load the syscall number for checking. */ 93 | BPF_STMT(BPF_LD + BPF_W + BPF_ABS, offsetof(struct seccomp_data, nr)), 94 | 95 | /* Syscalls to non-fatally deny */ 96 | 97 | /* Syscalls to allow */ 98 | #ifdef __NR_brk 99 | SC_ALLOW(brk), 100 | #endif 101 | #ifdef __NR_exit_group 102 | SC_ALLOW(exit_group), 103 | #endif 104 | 105 | /* /etc/localtime */ 106 | #ifdef __NR_fstat 107 | SC_ALLOW(fstat), 108 | #endif 109 | #ifdef __NR_fstat64 110 | SC_ALLOW(fstat64), 111 | #endif 112 | #ifdef __NR_stat 113 | SC_ALLOW(stat), 114 | #endif 115 | #ifdef __NR_stat64 116 | SC_ALLOW(stat64), 117 | #endif 118 | #ifdef __NR_newfstatat 119 | SC_ALLOW(newfstatat), 120 | #endif 121 | 122 | /* stdio */ 123 | #ifdef __NR_write 124 | SC_ALLOW(write), 125 | #endif 126 | #ifdef __NR_writev 127 | SC_ALLOW(writev), 128 | #endif 129 | 130 | #ifdef __NR_restart_syscall 131 | SC_ALLOW(restart_syscall), 132 | #endif 133 | 134 | /* Default deny */ 135 | BPF_STMT(BPF_RET + BPF_K, SECCOMP_FILTER_FAIL)}; 136 | 137 | struct sock_fprog prog = { 138 | .len = (unsigned short)(sizeof(filter) / sizeof(filter[0])), 139 | .filter = filter, 140 | }; 141 | 142 | if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) 143 | return -1; 144 | 145 | return prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog); 146 | } 147 | 148 | int restrict_process_wait(int fdp) { 149 | struct sock_filter filter[] = { 150 | /* Ensure the syscall arch convention is as expected. */ 151 | BPF_STMT(BPF_LD + BPF_W + BPF_ABS, offsetof(struct seccomp_data, arch)), 152 | BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, SECCOMP_AUDIT_ARCH, 1, 0), 153 | BPF_STMT(BPF_RET + BPF_K, SECCOMP_FILTER_FAIL), 154 | /* Load the syscall number for checking. */ 155 | BPF_STMT(BPF_LD + BPF_W + BPF_ABS, offsetof(struct seccomp_data, nr)), 156 | 157 | /* Syscalls to non-fatally deny */ 158 | 159 | /* Syscalls to allow */ 160 | #ifdef __NR_exit_group 161 | SC_ALLOW(exit_group), 162 | #endif 163 | 164 | /* stdio */ 165 | #ifdef __NR_write 166 | SC_ALLOW(write), 167 | #endif 168 | #ifdef __NR_writev 169 | SC_ALLOW(writev), 170 | #endif 171 | 172 | #ifdef __NR_restart_syscall 173 | SC_ALLOW(restart_syscall), 174 | #endif 175 | 176 | #ifdef __NR_rt_sigaction 177 | SC_ALLOW(rt_sigaction), 178 | #endif 179 | #ifdef __NR_rt_sigprocmask 180 | SC_ALLOW(rt_sigprocmask), 181 | #endif 182 | #ifdef __NR_sigprocmask 183 | SC_ALLOW(sigprocmask), 184 | #endif 185 | #ifdef __NR_rt_sigreturn 186 | SC_ALLOW(rt_sigreturn), 187 | #endif 188 | #ifdef __NR_sigreturn 189 | SC_ALLOW(sigreturn), 190 | #endif 191 | #ifdef __NR_wait4 192 | SC_ALLOW(wait4), 193 | #endif 194 | #ifdef __NR_alarm 195 | SC_ALLOW(alarm), 196 | #endif 197 | #ifdef __NR_setitimer 198 | SC_ALLOW(setitimer), 199 | #endif 200 | #ifdef __NR_lseek 201 | SC_ALLOW(lseek), 202 | #endif 203 | #ifdef __NR__llseek 204 | SC_ALLOW(_llseek), 205 | #endif 206 | #ifdef __NR_kill 207 | SC_ALLOW(kill), 208 | #endif 209 | 210 | /* Default deny */ 211 | BPF_STMT(BPF_RET + BPF_K, SECCOMP_FILTER_FAIL)}; 212 | 213 | struct sock_fprog prog = { 214 | .len = (unsigned short)(sizeof(filter) / sizeof(filter[0])), 215 | .filter = filter, 216 | }; 217 | 218 | (void)fdp; 219 | 220 | return prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog); 221 | } 222 | #endif 223 | -------------------------------------------------------------------------------- /runcron.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2025, Michael Santos 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted, provided that the above 5 | * copyright notice and this permission notice appear in all copies. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | */ 15 | #include "runcron.h" 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | #include "cronevent.h" 34 | #include "fnv1a.h" 35 | #include "restrict_process.h" 36 | #include "timestamp.h" 37 | #include "waitfor.h" 38 | #ifndef HAVE_STRTONUM 39 | #include "strtonum.h" 40 | #endif 41 | #ifndef HAVE_SETPROCTITLE 42 | #include "setproctitle.h" 43 | #endif 44 | 45 | #ifdef RESTRICT_PROCESS_capsicum 46 | #include 47 | #endif 48 | 49 | #ifdef HAVE_SETPROCTITLE 50 | #define RUNCRON_TITLE "(%s %ds) %s" 51 | #else 52 | #define RUNCRON_TITLE "runcron: (%s %ds) %s" 53 | #endif 54 | 55 | #define RUNCRON_VERSION "0.19.4" 56 | 57 | static int open_exit_status(char *file, int *status); 58 | static int read_exit_status(int fd, int *status); 59 | static int write_exit_status(int fd, int status); 60 | void sleepfor(unsigned int seconds); 61 | int signal_init(void (*handler)(int, siginfo_t *, void *)); 62 | void sa_handler_sleep(int sig, siginfo_t *info, void *context); 63 | void sa_handler_wait(int sig, siginfo_t *info, void *context); 64 | static int set_env(char *key, int val); 65 | static void print_argv(int argc, char *argv[]); 66 | static uint32_t seed_from_time(void); 67 | static int randinit(char *tag); 68 | static char *join(char **arg, size_t n); 69 | static void usage(void); 70 | 71 | static const struct option long_options[] = { 72 | {"file", required_argument, NULL, 'f'}, 73 | {"chdir", required_argument, NULL, 'C'}, 74 | {"tag", required_argument, NULL, 't'}, 75 | {"timeout", required_argument, NULL, 'T'}, 76 | {"retry-interval", required_argument, NULL, 'R'}, 77 | {"poll-interval", required_argument, NULL, 'P'}, 78 | {"dryrun", no_argument, NULL, 'n'}, 79 | {"print", no_argument, NULL, 'p'}, 80 | {"signal", required_argument, NULL, 's'}, 81 | {"limit-cpu", required_argument, NULL, OPT_LIMIT_CPU}, 82 | {"limit-as", required_argument, NULL, OPT_LIMIT_AS}, 83 | {"timestamp", required_argument, NULL, OPT_TIMESTAMP}, 84 | {"allow-setuid-subprocess", no_argument, NULL, OPT_ALLOW_SETUID_SUBPROCESS}, 85 | {"disable-process-restrictions", no_argument, NULL, 86 | OPT_DISABLE_PROCESS_RESTRICTIONS}, 87 | {"disable-signal-on-exit", no_argument, NULL, OPT_DISABLE_SIGNAL_ON_EXIT}, 88 | {"verbose", no_argument, NULL, 'v'}, 89 | {"version", no_argument, NULL, 'V'}, 90 | {"help", no_argument, NULL, 'h'}, 91 | {NULL, 0, NULL, 0}, 92 | }; 93 | 94 | static pid_t pid; 95 | static int fdp = -1; 96 | static int default_signal = SIGTERM; 97 | static volatile sig_atomic_t runnow = 0; 98 | static volatile sig_atomic_t remaining = 0; 99 | 100 | int main(int argc, char *argv[]) { 101 | runcron_t *rp; 102 | char *file = ".runcron.lock"; 103 | char *cwd = NULL; 104 | char *cronentry; 105 | char *tag = NULL; 106 | int fd; 107 | int status = 0; 108 | time_t now; 109 | unsigned int seconds; 110 | unsigned int timeout = 0; 111 | unsigned int retry_interval = 3600; /* 1 hour */ 112 | const char *errstr = NULL; 113 | int exit_value = 0; 114 | int signal_on_exit = 1; 115 | int allow_setuid_subprocess = 0; 116 | 117 | char **oargv = argv + 1; 118 | int oargc = argc - 1; 119 | char *procname; 120 | 121 | int ch; 122 | 123 | #ifndef HAVE_SETPROCTITLE 124 | spt_init(argc, argv); 125 | #endif 126 | 127 | if (restrict_process_init() < 0) 128 | err(EXIT_FAILURE, "restrict_process_init"); 129 | 130 | if (setvbuf(stdout, NULL, _IOLBF, 0) < 0) 131 | err(EXIT_FAILURE, "setvbuf"); 132 | 133 | rp = calloc(1, sizeof(runcron_t)); 134 | 135 | if (rp == NULL) 136 | err(EXIT_FAILURE, "calloc"); 137 | 138 | rp->cpu = 10; 139 | rp->as = 1 * 1024 * 1024; 140 | 141 | tag = getenv("RUNCRON_TAG"); 142 | 143 | now = time(NULL); 144 | if (now == -1) 145 | err(EXIT_FAILURE, "time"); 146 | 147 | (void)localtime(&now); 148 | 149 | while ((ch = getopt_long(argc, argv, "+C:f:hnpP:R:s:t:T:vV", long_options, 150 | NULL)) != -1) { 151 | switch (ch) { 152 | case 'C': 153 | cwd = optarg; 154 | break; 155 | 156 | case 'f': 157 | file = optarg; 158 | break; 159 | 160 | case 'n': 161 | rp->opt |= OPT_DRYRUN; 162 | break; 163 | 164 | case 'p': 165 | rp->opt |= OPT_PRINT; 166 | break; 167 | 168 | case 'P': /* deprecated: use -R */ 169 | case 'R': 170 | errno = 0; 171 | retry_interval = strtonum(optarg, 0, INT_MAX, &errstr); 172 | if (errstr != NULL) 173 | err(2, "strtonum: %s: %s", optarg, errstr); 174 | break; 175 | 176 | case 's': 177 | errno = 0; 178 | default_signal = strtonum(optarg, 0, NSIG, &errstr); 179 | if (errstr != NULL) 180 | err(2, "strtonum: %s: %s", optarg, errstr); 181 | break; 182 | 183 | case 't': 184 | tag = optarg; 185 | break; 186 | 187 | case 'T': 188 | errno = 0; 189 | timeout = strtonum(optarg, -1, UINT32_MAX, &errstr); 190 | if (errstr != NULL) 191 | err(2, "strtonum: %s: %s", optarg, errstr); 192 | break; 193 | 194 | case 'v': 195 | rp->verbose++; 196 | break; 197 | 198 | case 'V': 199 | (void)printf("%s\n", RUNCRON_VERSION); 200 | exit(EXIT_SUCCESS); 201 | break; 202 | 203 | case OPT_ALLOW_SETUID_SUBPROCESS: 204 | allow_setuid_subprocess = 1; 205 | break; 206 | 207 | case OPT_LIMIT_CPU: 208 | errno = 0; 209 | rp->cpu = strtonum(optarg, -1, UINT32_MAX, &errstr); 210 | if (errstr != NULL) 211 | err(2, "strtonum: %s: %s", optarg, errstr); 212 | break; 213 | 214 | case OPT_LIMIT_AS: 215 | errno = 0; 216 | rp->as = strtonum(optarg, -1, UINT32_MAX, &errstr); 217 | if (errstr != NULL) 218 | err(2, "strtonum: %s: %s", optarg, errstr); 219 | break; 220 | 221 | case OPT_TIMESTAMP: 222 | now = timestamp(optarg); 223 | if (now == -1) 224 | errx(2, "error: invalid timestamp: %s", optarg); 225 | break; 226 | 227 | case OPT_DISABLE_PROCESS_RESTRICTIONS: 228 | rp->opt |= OPT_DISABLE_PROCESS_RESTRICTIONS; 229 | break; 230 | 231 | case OPT_DISABLE_SIGNAL_ON_EXIT: 232 | signal_on_exit = 0; 233 | break; 234 | 235 | case 'h': 236 | default: 237 | usage(); 238 | exit(0); 239 | } 240 | } 241 | 242 | argc -= optind; 243 | argv += optind; 244 | 245 | if (argc < 2) { 246 | usage(); 247 | exit(2); 248 | } 249 | 250 | cronentry = argv[0]; 251 | 252 | argc--; 253 | argv++; 254 | 255 | procname = join(oargv, oargc); 256 | if (procname == NULL) 257 | err(111, "join"); 258 | 259 | if (randinit(tag) < 0) 260 | err(111, "randinit"); 261 | 262 | if (!allow_setuid_subprocess && disable_setuid_subprocess() < 0) 263 | err(111, "disable_setuid_subprocess"); 264 | 265 | if (cronevent(rp, cronentry, &seconds, now) < 0) 266 | exit(111); 267 | 268 | /* @reboot:if the runcron state file doesn't exist, set the exit status 269 | * to 255. */ 270 | if (seconds == UINT32_MAX) 271 | status = 255; 272 | 273 | fd = open_exit_status(file, &status); 274 | if (fd < 0) 275 | err(111, "open_exit_status: %s", file); 276 | 277 | /* @reboot: run immediately */ 278 | if (seconds == UINT32_MAX && status == 255) 279 | seconds = 0; 280 | 281 | if (!(rp->opt & OPT_DRYRUN) && (flock(fd, LOCK_EX | LOCK_NB) < 0)) 282 | err(111, "flock"); 283 | 284 | if ((cwd != NULL) && (chdir(cwd) < 0)) { 285 | err(111, "chdir: %s", cwd); 286 | } 287 | 288 | if (status != 0) { 289 | if (seconds > retry_interval) { 290 | seconds = retry_interval; 291 | } 292 | } 293 | 294 | if (rp->opt & OPT_PRINT) 295 | (void)printf("%lu\n", (long unsigned int)seconds); 296 | 297 | if (timeout == 0) { 298 | if (cronevent(rp, cronentry, &timeout, now + seconds) < 0) 299 | exit(111); 300 | } 301 | 302 | if ((set_env("RUNCRON_TIMEOUT", timeout) < 0) || 303 | (set_env("RUNCRON_EXITSTATUS", status) < 0)) 304 | err(111, "set_env"); 305 | 306 | if (rp->verbose >= 1) { 307 | print_argv(argc, argv); 308 | (void)fprintf( 309 | stderr, 310 | ": last exit status was %d, sleep interval is %ds, command timeout " 311 | "is %us\n", 312 | status, seconds, timeout); 313 | } 314 | 315 | if (rp->opt & OPT_DRYRUN) 316 | exit(0); 317 | 318 | if (signal_init(sa_handler_sleep) < 0) 319 | err(111, "signal_init"); 320 | 321 | setproctitle(RUNCRON_TITLE, status == 0 ? "sleep" : "retry", seconds, 322 | procname); 323 | 324 | sleepfor(seconds); 325 | 326 | if (status == 0) { 327 | if (write_exit_status(fd, 128 + SIGKILL) < 0) 328 | err(111, "write_exit_status: %s", file); 329 | } 330 | 331 | #ifdef RESTRICT_PROCESS_capsicum 332 | pid = pdfork(&fdp, PD_CLOEXEC); 333 | #else 334 | pid = fork(); 335 | #endif 336 | 337 | switch (pid) { 338 | case -1: 339 | err(111, "fork"); 340 | case 0: 341 | if (setsid() < 0) 342 | err(111, "setsid"); 343 | 344 | if (restrict_process_signal_on_supervisor_exit() < 0) 345 | err(111, "restrict_process_signal_on_supervisor_exit"); 346 | 347 | (void)execvp(argv[0], argv); 348 | exit(errno == ENOENT ? 127 : 126); 349 | default: 350 | if (restrict_process_wait(fdp) < 0) { 351 | err(111, "restrict_process_wait"); 352 | } 353 | 354 | if (signal_init(sa_handler_wait) < 0) { 355 | #ifdef RESTRICT_PROCESS_capsicum 356 | (void)pdkill(fdp, default_signal); 357 | #else 358 | (void)kill(-pid, default_signal); 359 | #endif 360 | } 361 | if (rp->verbose >= 1) { 362 | print_argv(argc, argv); 363 | (void)fprintf(stderr, ": running command: timeout is set to %us\n", 364 | timeout); 365 | } 366 | if (timeout < UINT32_MAX) { 367 | alarm(timeout); 368 | } 369 | setproctitle(RUNCRON_TITLE, "running", timeout, procname); 370 | if (waitfor(fdp, &status) < 0) { 371 | warn("waitfor"); 372 | #ifdef RESTRICT_PROCESS_capsicum 373 | (void)pdkill(fdp, default_signal); 374 | #else 375 | (void)kill(-pid, default_signal); 376 | #endif 377 | exit(111); 378 | } 379 | alarm(0); 380 | } 381 | 382 | if (WIFEXITED(status)) 383 | exit_value = WEXITSTATUS(status); 384 | else if (WIFSIGNALED(status)) 385 | exit_value = 128 + WTERMSIG(status); 386 | 387 | if (rp->verbose >= 3) 388 | (void)fprintf(stderr, "status=%d exit_value=%d\n", status, exit_value); 389 | 390 | if (write_exit_status(fd, exit_value) < 0) 391 | err(111, "write_exit_status: %s", file); 392 | 393 | if (signal_on_exit) { 394 | #ifdef RESTRICT_PROCESS_capsicum 395 | (void)pdkill(fdp, default_signal); 396 | #else 397 | (void)kill(-pid, default_signal); 398 | #endif 399 | } 400 | 401 | exit(exit_value); 402 | } 403 | 404 | void sleepfor(unsigned int seconds) { 405 | while (seconds > 0 && !runnow) { 406 | if (remaining) { 407 | (void)fprintf(stderr, "%u\n", seconds); 408 | remaining = 0; 409 | } 410 | seconds = sleep(seconds); 411 | } 412 | } 413 | 414 | void sa_handler_sleep(int sig, siginfo_t *info, void *context) { 415 | switch (sig) { 416 | case SIGUSR1: 417 | case SIGALRM: 418 | runnow = 1; 419 | break; 420 | case SIGUSR2: 421 | remaining = 1; 422 | break; 423 | case SIGINT: 424 | _exit(111); 425 | case SIGTERM: 426 | _exit(111); 427 | default: 428 | break; 429 | } 430 | } 431 | 432 | void sa_handler_wait(int sig, siginfo_t *info, void *context) { 433 | switch (sig) { 434 | case SIGUSR1: 435 | case SIGUSR2: 436 | break; 437 | case SIGALRM: 438 | /* ignore SIGALRM generated by kill(2), sigqueue(3) */ 439 | if (info->si_pid != 0) { 440 | return; 441 | } 442 | /* fallthrough */ 443 | default: 444 | if (pid > 0) 445 | #ifdef RESTRICT_PROCESS_capsicum 446 | (void)pdkill(fdp, sig == SIGALRM ? default_signal : sig); 447 | #else 448 | (void)kill(-pid, sig == SIGALRM ? default_signal : sig); 449 | #endif 450 | } 451 | } 452 | 453 | int signal_init(void (*handler)(int, siginfo_t *, void *)) { 454 | struct sigaction act = {0}; 455 | int sig; 456 | 457 | act.sa_flags |= SA_SIGINFO; 458 | act.sa_sigaction = handler; 459 | (void)sigfillset(&act.sa_mask); 460 | 461 | for (sig = 1; sig < NSIG; sig++) { 462 | switch (sig) { 463 | case SIGCHLD: 464 | continue; 465 | default: 466 | break; 467 | } 468 | 469 | if (sigaction(sig, &act, NULL) < 0) { 470 | if (errno == EINVAL) 471 | continue; 472 | 473 | return -1; 474 | } 475 | } 476 | 477 | return 0; 478 | } 479 | 480 | static int open_exit_status(char *file, int *status) { 481 | int fd; 482 | 483 | fd = open(file, O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0600); 484 | 485 | if (fd < 0) { 486 | switch (errno) { 487 | case EEXIST: 488 | fd = open(file, O_RDWR | O_CLOEXEC, 0); 489 | if (fd < 0) 490 | return -1; 491 | if (read_exit_status(fd, status) < 0) { 492 | (void)close(fd); 493 | return -1; 494 | } 495 | return fd; 496 | default: 497 | return -1; 498 | } 499 | } 500 | 501 | if (write_exit_status(fd, *status) < 0) { 502 | (void)close(fd); 503 | return -1; 504 | } 505 | 506 | return fd; 507 | } 508 | 509 | static int write_exit_status(int fd, int status) { 510 | unsigned char buf; 511 | 512 | buf = status > 255 ? 128 : (unsigned char)status; 513 | 514 | if (lseek(fd, 0, SEEK_SET) < 0) 515 | return -1; 516 | 517 | if (write(fd, &buf, 1) != 1) 518 | return -1; 519 | 520 | return 0; 521 | } 522 | 523 | static int read_exit_status(int fd, int *status) { 524 | unsigned char buf; 525 | 526 | if (lseek(fd, 0, SEEK_SET) < 0) 527 | return -1; 528 | 529 | if (read(fd, &buf, 1) != 1) 530 | return -1; 531 | 532 | *status = buf; 533 | return 0; 534 | } 535 | 536 | static int set_env(char *key, int val) { 537 | char str[11]; 538 | int rv; 539 | 540 | rv = snprintf(str, sizeof(str), "%u", val); 541 | if (rv < 0 || (unsigned)rv >= sizeof(str)) 542 | return -1; 543 | 544 | if ((setenv(key, str, 1) < 0)) 545 | return -1; 546 | 547 | return 0; 548 | } 549 | 550 | static void print_argv(int argc, char *argv[]) { 551 | int i; 552 | int space = 0; 553 | for (i = 0; i < argc; i++) { 554 | (void)fprintf(stderr, "%s%s", (space == 1 ? " " : ""), argv[i]); 555 | space = 1; 556 | } 557 | } 558 | 559 | static uint32_t seed_from_time(void) { 560 | struct timeval tv = {0}; 561 | 562 | (void)gettimeofday(&tv, NULL); 563 | 564 | return getpid() ^ tv.tv_sec ^ tv.tv_usec; 565 | } 566 | 567 | static int randinit(char *tag) { 568 | uint32_t seed; 569 | char name[MAXHOSTNAMELEN] = {0}; 570 | size_t len; 571 | 572 | if (tag == NULL) { 573 | if (gethostname(name, sizeof(name) - 1) < 0) 574 | return -1; 575 | tag = name; 576 | } 577 | 578 | len = strlen(tag); 579 | seed = len == 0 ? seed_from_time() : fnv1a((uint8_t *)tag, len); 580 | 581 | #if defined(__OpenBSD__) 582 | srandom_deterministic(seed); 583 | #else 584 | srandom(seed); 585 | #endif 586 | return 0; 587 | } 588 | 589 | static char *join(char **arg, size_t n) { 590 | size_t len = 0; 591 | size_t alen = 0; 592 | char *buf; 593 | size_t i; 594 | int append = 0; 595 | char *space = " "; 596 | 597 | if (n == 0) { 598 | errno = EINVAL; 599 | return NULL; 600 | } 601 | 602 | for (i = 0; i < n; i++) { 603 | len += strlen(arg[i]); 604 | } 605 | 606 | len += n - 1; /* spaces */ 607 | buf = calloc(len + 1, 1); 608 | if (buf == NULL) 609 | return NULL; 610 | 611 | for (i = 0; i < n; i++) { 612 | size_t argsz; 613 | 614 | if (append) { 615 | if (alen + 1 > len) 616 | goto ERR; 617 | (void)memcpy(buf + alen, space, 1); 618 | alen += 1; 619 | } 620 | argsz = strlen(arg[i]); 621 | if (alen + argsz > len) 622 | goto ERR; 623 | (void)memcpy(buf + alen, arg[i], argsz); 624 | alen += argsz; 625 | append = 1; 626 | } 627 | 628 | return buf; 629 | 630 | ERR: 631 | free(buf); 632 | errno = EINVAL; 633 | return NULL; 634 | } 635 | 636 | static void usage(void) { 637 | (void)fprintf( 638 | stderr, 639 | "[OPTION] <...>\n" 640 | "version: %s (using %s mode process restriction)\n\n" 641 | "-f, --file lock file path (default: .runcron.lock)\n" 642 | "-T, --timeout specify command timeout\n" 643 | "-R, --retry-interval retry failed command (default: 3600)\n" 644 | "-C, --chdir change working directory\n" 645 | "-n, --dryrun do nothing\n" 646 | "-p, --print output seconds to next timespec\n" 647 | "-s, --signal signal sent task on timeout (default: " 648 | "15)\n" 649 | "-t, --tag seed used for random intervals\n" 650 | "-v, --verbose verbose mode\n" 651 | "-V, --version runcron version\n" 652 | " --limit-cpu restrict cpu usage of cron expression\n" 653 | " parsing\n" 654 | " --limit-as restrict memory (address space) of cron\n" 655 | " expression parsing\n" 656 | " --allow-setuid-subprocess allow running unkillable tasks\n" 657 | " --disable-process-restrictions\n" 658 | " do not fork cron expression processing\n" 659 | " --disable-signal-on-exit disable termination of subprocesses on " 660 | "exit\n" 661 | " --timestamp \n" 662 | " set current time\n", 663 | RUNCRON_VERSION, RESTRICT_PROCESS); 664 | } 665 | -------------------------------------------------------------------------------- /runcron.h: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020, Michael Santos 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted, provided that the above 5 | * copyright notice and this permission notice appear in all copies. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | */ 15 | #include 16 | #include 17 | 18 | typedef struct { 19 | int opt; 20 | int verbose; 21 | rlim_t cpu; 22 | rlim_t as; 23 | } runcron_t; 24 | 25 | enum { 26 | OPT_TIMESTAMP = 1 << 0, 27 | OPT_PRINT = 1 << 1, 28 | OPT_DRYRUN = 1 << 2, 29 | OPT_DISABLE_PROCESS_RESTRICTIONS = 1 << 3, 30 | OPT_LIMIT_CPU = 1 << 4, 31 | OPT_LIMIT_AS = 1 << 5, 32 | OPT_DISABLE_SIGNAL_ON_EXIT = 1 << 6, 33 | OPT_ALLOW_SETUID_SUBPROCESS = 1 << 7, 34 | }; 35 | -------------------------------------------------------------------------------- /setproctitle.c: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | * setproctitle.c - Linux/Darwin setproctitle. 3 | * -------------------------------------------------------------------------- 4 | * Copyright (C) 2010 William Ahern 5 | * Copyright (C) 2013 Salvatore Sanfilippo 6 | * Copyright (C) 2013 Stam He 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a 9 | * copy of this software and associated documentation files (the 10 | * "Software"), to deal in the Software without restriction, including 11 | * without limitation the rights to use, copy, modify, merge, publish, 12 | * distribute, sublicense, and/or sell copies of the Software, and to permit 13 | * persons to whom the Software is furnished to do so, subject to the 14 | * following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included 17 | * in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 20 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 22 | * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 23 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 24 | * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 25 | * USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | * ========================================================================== 27 | */ 28 | #ifndef _GNU_SOURCE 29 | #define _GNU_SOURCE 30 | #endif 31 | 32 | #include /* NULL size_t */ 33 | #include /* va_list va_start va_end */ 34 | #include /* malloc(3) setenv(3) clearenv(3) setproctitle(3) getprogname(3) */ 35 | #include /* vsnprintf(3) snprintf(3) */ 36 | 37 | #include /* strlen(3) strchr(3) strdup(3) memset(3) memcpy(3) */ 38 | 39 | #include /* errno program_invocation_name program_invocation_short_name */ 40 | 41 | #include "setproctitle.h" 42 | 43 | #if !defined(HAVE_SETPROCTITLE) 44 | #if (defined __NetBSD__ || defined __FreeBSD__ || defined __OpenBSD__) 45 | #define HAVE_SETPROCTITLE 1 46 | #else 47 | #define HAVE_SETPROCTITLE 0 48 | #endif 49 | #endif 50 | 51 | 52 | #if !HAVE_SETPROCTITLE 53 | #if (defined __linux || defined __APPLE__) 54 | 55 | extern char **environ; 56 | 57 | static struct { 58 | /* original value */ 59 | const char *arg0; 60 | 61 | /* title space available */ 62 | char *base, *end; 63 | 64 | /* pointer to original nul character within base */ 65 | char *nul; 66 | 67 | _Bool reset; 68 | int error; 69 | } SPT; 70 | 71 | 72 | #ifndef SPT_MIN 73 | #define SPT_MIN(a, b) (((a) < (b))? (a) : (b)) 74 | #endif 75 | 76 | static inline size_t spt_min(size_t a, size_t b) { 77 | return SPT_MIN(a, b); 78 | } /* spt_min() */ 79 | 80 | 81 | /* 82 | * For discussion on the portability of the various methods, see 83 | * http://lists.freebsd.org/pipermail/freebsd-stable/2008-June/043136.html 84 | */ 85 | static int spt_clearenv(void) { 86 | #if __GLIBC__ 87 | clearenv(); 88 | 89 | return 0; 90 | #else 91 | extern char **environ; 92 | static char **tmp; 93 | 94 | if (!(tmp = malloc(sizeof *tmp))) 95 | return errno; 96 | 97 | tmp[0] = NULL; 98 | environ = tmp; 99 | 100 | return 0; 101 | #endif 102 | } /* spt_clearenv() */ 103 | 104 | 105 | static int spt_copyenv(char *oldenv[]) { 106 | extern char **environ; 107 | char *eq; 108 | int i, error; 109 | 110 | if (environ != oldenv) 111 | return 0; 112 | 113 | if ((error = spt_clearenv())) 114 | goto error; 115 | 116 | for (i = 0; oldenv[i]; i++) { 117 | if (!(eq = strchr(oldenv[i], '='))) 118 | continue; 119 | 120 | *eq = '\0'; 121 | error = (0 != setenv(oldenv[i], eq + 1, 1))? errno : 0; 122 | *eq = '='; 123 | 124 | if (error) 125 | goto error; 126 | } 127 | 128 | return 0; 129 | error: 130 | environ = oldenv; 131 | 132 | return error; 133 | } /* spt_copyenv() */ 134 | 135 | 136 | static int spt_copyargs(int argc, char *argv[]) { 137 | char *tmp; 138 | int i; 139 | 140 | for (i = 1; i < argc || (i >= argc && argv[i]); i++) { 141 | if (!argv[i]) 142 | continue; 143 | 144 | if (!(tmp = strdup(argv[i]))) 145 | return errno; 146 | 147 | argv[i] = tmp; 148 | } 149 | 150 | return 0; 151 | } /* spt_copyargs() */ 152 | 153 | 154 | void spt_init(int argc, char *argv[]) { 155 | char **envp = environ; 156 | char *base, *end, *nul, *tmp; 157 | int i, error; 158 | 159 | if (!(base = argv[0])) 160 | return; 161 | 162 | nul = &base[strlen(base)]; 163 | end = nul + 1; 164 | 165 | for (i = 0; i < argc || (i >= argc && argv[i]); i++) { 166 | if (!argv[i] || argv[i] < end) 167 | continue; 168 | 169 | end = argv[i] + strlen(argv[i]) + 1; 170 | } 171 | 172 | for (i = 0; envp[i]; i++) { 173 | if (envp[i] < end) 174 | continue; 175 | 176 | end = envp[i] + strlen(envp[i]) + 1; 177 | } 178 | 179 | if (!(SPT.arg0 = strdup(argv[0]))) 180 | goto syerr; 181 | 182 | #if __GLIBC__ 183 | if (!(tmp = strdup(program_invocation_name))) 184 | goto syerr; 185 | 186 | program_invocation_name = tmp; 187 | 188 | if (!(tmp = strdup(program_invocation_short_name))) 189 | goto syerr; 190 | 191 | program_invocation_short_name = tmp; 192 | #elif __APPLE__ 193 | if (!(tmp = strdup(getprogname()))) 194 | goto syerr; 195 | 196 | setprogname(tmp); 197 | #endif 198 | 199 | 200 | if ((error = spt_copyenv(envp))) 201 | goto error; 202 | 203 | if ((error = spt_copyargs(argc, argv))) 204 | goto error; 205 | 206 | SPT.nul = nul; 207 | SPT.base = base; 208 | SPT.end = end; 209 | 210 | return; 211 | syerr: 212 | error = errno; 213 | error: 214 | SPT.error = error; 215 | } /* spt_init() */ 216 | 217 | 218 | #ifndef SPT_MAXTITLE 219 | #define SPT_MAXTITLE 255 220 | #endif 221 | 222 | void setproctitle(const char *fmt, ...) { 223 | char buf[SPT_MAXTITLE + 1]; /* use buffer in case argv[0] is passed */ 224 | va_list ap; 225 | char *nul; 226 | int len, error; 227 | 228 | if (!SPT.base) 229 | return; 230 | 231 | if (fmt) { 232 | va_start(ap, fmt); 233 | len = vsnprintf(buf, sizeof buf, fmt, ap); 234 | va_end(ap); 235 | } else { 236 | len = snprintf(buf, sizeof buf, "%s", SPT.arg0); 237 | } 238 | 239 | if (len <= 0) 240 | { error = errno; goto error; } 241 | 242 | if (!SPT.reset) { 243 | memset(SPT.base, 0, SPT.end - SPT.base); 244 | SPT.reset = 1; 245 | } else { 246 | memset(SPT.base, 0, spt_min(sizeof buf, SPT.end - SPT.base)); 247 | } 248 | 249 | len = spt_min(len, spt_min(sizeof buf, SPT.end - SPT.base) - 1); 250 | memcpy(SPT.base, buf, len); 251 | nul = &SPT.base[len]; 252 | 253 | if (nul == SPT.nul && &nul[1] < SPT.end) { 254 | *SPT.nul = ' '; 255 | *++nul = '\0'; 256 | } 257 | 258 | return; 259 | error: 260 | SPT.error = error; 261 | } /* setproctitle() */ 262 | 263 | 264 | #endif /* __linux || __APPLE__ */ 265 | #endif /* !HAVE_SETPROCTITLE */ 266 | -------------------------------------------------------------------------------- /setproctitle.h: -------------------------------------------------------------------------------- 1 | #ifndef HAVE_SETPROCTITLE 2 | void spt_init(int argc, char *argv[]); 3 | void setproctitle(const char *fmt, ...); 4 | #endif 5 | -------------------------------------------------------------------------------- /strtonum.c: -------------------------------------------------------------------------------- 1 | /* $OpenBSD: strtonum.c,v 1.6 2004/08/03 19:38:01 millert Exp $ */ 2 | 3 | /* 4 | * Copyright (c) 2004 Ted Unangst and Todd Miller 5 | * All rights reserved. 6 | * 7 | * Permission to use, copy, modify, and distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 14 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 16 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 17 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | */ 19 | 20 | /* OPENBSD ORIGINAL: lib/libc/stdlib/strtonum.c */ 21 | 22 | #ifndef HAVE_STRTONUM 23 | #include 24 | #include 25 | #include 26 | 27 | #include "strtonum.h" 28 | 29 | #define INVALID 1 30 | #define TOOSMALL 2 31 | #define TOOLARGE 3 32 | 33 | long long strtonum(const char *numstr, long long minval, long long maxval, 34 | const char **errstrp) { 35 | long long ll = 0; 36 | char *ep; 37 | int error = 0; 38 | struct errval { 39 | const char *errstr; 40 | int err; 41 | } ev[4] = { 42 | {NULL, 0}, 43 | {"invalid", EINVAL}, 44 | {"too small", ERANGE}, 45 | {"too large", ERANGE}, 46 | }; 47 | 48 | ev[0].err = errno; 49 | errno = 0; 50 | if (minval > maxval) 51 | error = INVALID; 52 | else { 53 | ll = strtoll(numstr, &ep, 10); 54 | if (numstr == ep || *ep != '\0') 55 | error = INVALID; 56 | else if ((ll == LLONG_MIN && errno == ERANGE) || ll < minval) 57 | error = TOOSMALL; 58 | else if ((ll == LLONG_MAX && errno == ERANGE) || ll > maxval) 59 | error = TOOLARGE; 60 | } 61 | if (errstrp != NULL) 62 | *errstrp = ev[error].errstr; 63 | errno = ev[error].err; 64 | if (error) 65 | ll = 0; 66 | 67 | return (ll); 68 | } 69 | 70 | #endif /* HAVE_STRTONUM */ 71 | -------------------------------------------------------------------------------- /strtonum.h: -------------------------------------------------------------------------------- 1 | long long strtonum(const char *numstr, long long minval, long long maxval, 2 | const char **errstrp); 3 | -------------------------------------------------------------------------------- /test/10-crontab.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "crontab format: minutes scheduled" { 4 | rm -f .runcron.lock 5 | run runcron -np --timestamp="2018-01-24 18:18:18" "*/5 * 26 * *" true 6 | cat << EOF 7 | $output 8 | EOF 9 | [ "$status" -eq 0 ] 10 | [ "$output" -eq 106902 ] 11 | } 12 | 13 | @test "crontab format: seconds scheduled" { 14 | run runcron -np --timestamp="2018-01-24 18:18:18" "0 */5 * 26 * *" true 15 | cat << EOF 16 | $output 17 | EOF 18 | [ "$status" -eq 0 ] 19 | [ "$output" -eq 106902 ] 20 | } 21 | 22 | @test "crontab format: random intervals" { 23 | run runcron -np -t "www1.example.com" \ 24 | --timestamp="2018-01-24 18:18:18" "0 0~8/2 * * 1~5" true 25 | cat << EOF 26 | $output 27 | EOF 28 | [ "$status" -eq 0 ] 29 | 30 | # The interval depends on the random(3) implementation but is constant. 31 | case `uname -s` in 32 | FreeBSD) [ "$output" -eq 380502 ] ;; 33 | OpenBSD) [ "$output" -eq 474102 ] ;; 34 | Linux) 35 | # glbc || musl 36 | [ "$output" -eq 474102 ] || [ "$output" -eq 34902 ] ;; 37 | *) skip ;; 38 | esac 39 | 40 | run runcron -np -t "www2.example.com" \ 41 | --timestamp="2018-01-24 18:18:18" "0 0~8/2 * * 1~5" true 42 | cat << EOF 43 | $output 44 | EOF 45 | [ "$status" -eq 0 ] 46 | 47 | case `uname -s` in 48 | FreeBSD) [ "$output" -eq 553302 ] ;; 49 | OpenBSD) [ "$output" -eq 121302 ] ;; 50 | Linux) 51 | # glbc || musl 52 | [ "$output" -eq 121302 ] || [ "$output" -eq 452502 ] ;; 53 | *) skip ;; 54 | esac 55 | } 56 | 57 | @test "crontab format: space delimited fields" { 58 | run runcron -n " * * * * *" true 59 | cat << EOF 60 | $output 61 | EOF 62 | [ "$status" -eq 0 ] 63 | } 64 | 65 | @test "crontab format: tab delimited fields" { 66 | run runcron -n "* * * * * *" true 67 | cat << EOF 68 | $output 69 | EOF 70 | [ "$status" -eq 0 ] 71 | } 72 | 73 | @test "crontab format: invalid timespec" { 74 | run runcron -np "* 26 * *" true 75 | cat << EOF 76 | $output 77 | EOF 78 | [ "$status" -eq 111 ] 79 | [ "$output" = "runcron: error: invalid crontab timespec: Invalid number of fields, expression must consist of 6 fields" ] 80 | } 81 | 82 | @test "crontab format: cron_next: invalid timespec" { 83 | run runcron -np "3 3 31 2 *" true 84 | cat << EOF 85 | $output 86 | EOF 87 | [ "$status" -eq 111 ] 88 | [ "$output" = 'runcron: error: cron_next: 3 3 31 2 *: invalid timespec' ] 89 | } 90 | 91 | @test "crontab alias: =daily" { 92 | run runcron -np --timestamp="2018-01-24 18:18:18" "=daily" true 93 | cat << EOF 94 | $output 95 | EOF 96 | [ "$status" -eq 0 ] 97 | [ "$output" -eq 20502 ] 98 | } 99 | 100 | @test "crontab alias: @monthly" { 101 | run runcron -np --timestamp="2021-02-09 21:24:00" -t a "@monthly" true 102 | cat << EOF 103 | $output 104 | EOF 105 | # now[1612923840]=Tue Feb 9 21:24:00 2021 106 | # next[1614435577]=Sat Feb 27 09:19:37 2021 107 | # now[1614435577]=Sat Feb 27 09:19:37 2021 108 | # next[1616851177]=Sat Mar 27 09:19:37 2021 109 | 110 | # The interval depends on the random(3) implementation but is constant. 111 | case `uname -s` in 112 | FreeBSD) [ "$output" -eq 1124427 ] ;; 113 | OpenBSD) ;& 114 | Linux) 115 | # glbc || musl 116 | [ "$output" -eq 1511737 ] || [ "$output" -eq 2076481 ] 117 | ;; 118 | *) skip ;; 119 | esac 120 | } 121 | 122 | @test "crontab alias: invalid alias" { 123 | run runcron -np "@foo" true 124 | cat << EOF 125 | $output 126 | EOF 127 | [ "$status" -eq 111 ] 128 | [ "$output" = "runcron: error: invalid crontab timespec" ] 129 | } 130 | 131 | @test "crontab alias: timespec too long" { 132 | run runcron -np --timestamp="2018-01-24 18:18:18" "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 5 1 * * *" true 133 | cat << EOF 134 | $output 135 | EOF 136 | [ "$status" -eq 111 ] 137 | [ "$output" = "runcron: error: timespec exceeds maximum length: 252" ] 138 | } 139 | 140 | @test "timestamp: daylight savings" { 141 | run runcron -np --timestamp "2018-03-12 1:55:00" "15 2 * * *" true 142 | cat << EOF 143 | $output 144 | EOF 145 | [ "$status" -eq 0 ] 146 | [ "$output" -eq 1200 ] 147 | } 148 | 149 | @test "timestamp: accept epoch seconds" { 150 | run runcron -np --timestamp "@1520834100" "15 2 * * *" true 151 | if [[ $output =~ runcron:\ error:\ invalid\ timestamp:\ @1520834100 ]]; then 152 | skip 'strptime does not support "%s"' 153 | fi 154 | cat << EOF 155 | $output 156 | EOF 157 | [ "$status" -eq 0 ] 158 | [ "$output" -eq 1200 ] 159 | } 160 | 161 | @test "crontab format: spring daylight savings" { 162 | run runcron -np --timestamp "2019-03-09 11:43:00" "15 11 * * *" true 163 | cat << EOF 164 | $output 165 | EOF 166 | [ "$status" -eq 0 ] 167 | } 168 | 169 | @test "crontab format: invalid day of month" { 170 | run runcron -np --timestamp "2019-03-09 11:43:00" "* * * 30 2 *" true 171 | cat << EOF 172 | $output 173 | EOF 174 | [ "$status" -eq 111 ] 175 | } 176 | 177 | @test "crontab format: @reboot: first run" { 178 | rm -f .runcron.reboot 179 | run runcron -f .runcron.reboot -p "@reboot" true 180 | cat << EOF 181 | $output 182 | EOF 183 | [ "$status" -eq 0 ] 184 | [ "$output" -eq 0 ] 185 | 186 | [ -f .runcron.reboot ] 187 | # use --dryrun or the command will sleep UINT32_MAX seconds 188 | run runcron -f .runcron.reboot -np "@reboot" true 189 | cat << EOF 190 | $output 191 | EOF 192 | [ "$status" -eq 0 ] 193 | [ "$output" -eq 4294967295 ] 194 | } 195 | 196 | @test "options: arguments allowed in command" { 197 | rm -f .runcron.reboot 198 | run runcron -f .runcron.reboot -p "@reboot" ls -al 199 | cat << EOF 200 | $output 201 | EOF 202 | [ "$status" -eq 0 ] 203 | } 204 | 205 | @test "options: test changing working directory" { 206 | rm -f .runcron.reboot 207 | run runcron -C /dev -f .runcron.reboot "@reboot" ls null 208 | cat << EOF 209 | $output 210 | EOF 211 | [ "$status" -eq 0 ] 212 | 213 | rm -f .runcron.reboot 214 | run runcron -C / -f .runcron.reboot "@reboot" ls null 215 | cat << EOF 216 | $output 217 | EOF 218 | [ "$status" -ne 0 ] 219 | } 220 | 221 | @test "exit: terminate background subprocesses" { 222 | rm -f .runcron.reboot 223 | run runcron -f .runcron.reboot -p "@reboot" bash -c "exec -a RUNCRON_TEST_SLEEP sleep 60 &" 224 | [ "$status" -eq 0 ] 225 | run pgrep -f RUNCRON_TEST_SLEEP 226 | cat << EOF 227 | $output 228 | EOF 229 | [ "$status" -eq 1 ] 230 | } 231 | 232 | @test "prevent unkillable (setuid) subprocesses" { 233 | rm -f .runcron.reboot 234 | case `uname -s` in 235 | FreeBSD) ;& 236 | OpenBSD) ;& 237 | Linux) 238 | run runcron -f .runcron.reboot -p "@reboot" ping -c 1 127.0.0.1 239 | [ "$status" -ne 0 ] 240 | ;; 241 | *) skip ;; 242 | esac 243 | } 244 | 245 | @test "task: read from stdin" { 246 | rm -f .runcron.reboot 247 | run sh -c "echo test | runcron -f .runcron.reboot '@reboot' sed 's/e/3/g'" 248 | cat << EOF 249 | $output 250 | EOF 251 | [ "$status" -eq 0 ] 252 | [ "$output" = "t3st" ] 253 | } 254 | -------------------------------------------------------------------------------- /timestamp.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020, Michael Santos 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted, provided that the above 5 | * copyright notice and this permission notice appear in all copies. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | */ 15 | #define _XOPEN_SOURCE 700 16 | #include 17 | 18 | #include "timestamp.h" 19 | 20 | time_t timestamp(const char *s) { 21 | struct tm tm = {0}; 22 | 23 | switch (s[0]) { 24 | case '@': 25 | if (strptime(s + 1, "%s", &tm) == NULL) 26 | return -1; 27 | 28 | #if defined(__OpenBSD__) 29 | tm.tm_isdst = -1; 30 | return mktime(&tm) + tm.tm_gmtoff; 31 | #endif 32 | break; 33 | 34 | default: 35 | if (strptime(s, "%Y-%m-%d %T", &tm) == NULL) 36 | return -1; 37 | 38 | break; 39 | } 40 | 41 | tm.tm_isdst = -1; 42 | 43 | return mktime(&tm); 44 | } 45 | -------------------------------------------------------------------------------- /timestamp.h: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020, Michael Santos 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted, provided that the above 5 | * copyright notice and this permission notice appear in all copies. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | */ 15 | time_t timestamp(const char *s); 16 | -------------------------------------------------------------------------------- /waitfor.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2021, Michael Santos 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted, provided that the above 5 | * copyright notice and this permission notice appear in all copies. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | */ 15 | #include 16 | #include 17 | 18 | #ifdef RESTRICT_PROCESS_capsicum 19 | #include 20 | #include 21 | #endif 22 | 23 | #include "waitfor.h" 24 | 25 | int waitfor(int fdp, int *status) { 26 | #ifdef RESTRICT_PROCESS_capsicum 27 | struct kevent event; 28 | int kq; 29 | int rv; 30 | 31 | kq = kqueue(); 32 | if (kq == -1) 33 | return -1; 34 | 35 | EV_SET(&event, fdp, EVFILT_PROCDESC, EV_ADD | EV_CLEAR | EV_ONESHOT, 36 | NOTE_EXIT, 0, NULL); 37 | 38 | rv = kevent(kq, &event, 1, &event, 1, NULL); 39 | if (rv == -1) { 40 | switch (errno) { 41 | case EINTR: 42 | return 0; 43 | default: 44 | return -1; 45 | } 46 | } 47 | 48 | *status = (int)event.data; 49 | return 0; 50 | #else 51 | (void)fdp; 52 | for (;;) { 53 | errno = 0; 54 | if (wait(status) < 0) { 55 | if (errno == EINTR) 56 | continue; 57 | return -1; 58 | } 59 | return 0; 60 | } 61 | #endif 62 | } 63 | -------------------------------------------------------------------------------- /waitfor.h: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2021, Michael Santos 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted, provided that the above 5 | * copyright notice and this permission notice appear in all copies. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | */ 15 | #include 16 | #include 17 | 18 | int waitfor(int fdp, int *status); 19 | --------------------------------------------------------------------------------