Dear Maintainer,
`crontab <file>` silently truncates the file path argument at 100 chars
instead of rejecting a too-long path, then fails against the truncated
path with a misleading "No such file or directory" or "Not a regular
file" error that doesn't match the path the user actually passed.
Root cause: cron.h defines
#define MAX_FNAME 100 /* max length of internally generated fn */
That buffer is meant for cron's own internally-generated filenames, but
crontab.c reuses the same constant for the buffer that holds the
user-supplied CLI path argument:
static char Filename[MAX_FNAME]; /* crontab.c:60 */
...
(void) strncpy (Filename, argv[optind], (sizeof Filename)-1);
Filename[(sizeof Filename)-1] = '\0'; /* crontab.c:255-256 */
There is no length check before the strncpy, so any path >=100 chars
(99 usable chars + NUL) is silently truncated. The program then tries
to open/stat the truncated path, which either doesn't exist (ENOENT,
"No such file or directory") or happens to resolve to an existing
directory ("Not a regular file") -- neither error message reflects
what actually went wrong, which makes this very confusing to debug:
the file genuinely exists at the path the user gave, but crontab is
silently operating on a different, truncated path.
Reproduction (byte-precision, confirmed on 3.0pl1-197):
$ f=/tmp/$(python3 -c "print('a'*86)")/x # 99 chars total
$ mkdir -p "$(dirname "$f")"; echo '* * * * * true' > "$f"
$ crontab "$f"; echo $?
0
$ f=/tmp/$(python3 -c "print('a'*87)")/x # 100 chars total
$ mkdir -p "$(dirname "$f")"; echo '* * * * * true' > "$f"
$ crontab "$f"
/tmp/aaaa....aaa: Not a regular file.
(the printed path is the truncated 99-char string, cut off before
the trailing "/x" -- it happens to equal the parent directory,
which is why stat() succeeds but S_ISREG() fails)
Proposed fix: reject with a clear error instead of truncating silently.
Minimal patch against crontab.c, just before the existing strncpy at
line 255:
--- a/crontab.c
+++ b/crontab.c
@@ -252,6 +252,11 @@
if (argv[optind] != NULL) {
Option = opt_replace;
+ if (strlen(argv[optind]) >= sizeof(Filename)) {
+ fprintf(stderr,
+ "%s: pathname too long (max %zu chars)\n",
+ argv[optind], sizeof(Filename) - 1);
+ exit(ERROR_EXIT);
+ }
(void) strncpy (Filename, argv[optind], (sizeof Filename)-1);
Filename[(sizeof Filename)-1] = '\0';
This preserves the existing MAX_FNAME=100 limit used elsewhere in the
codebase for internally-generated filenames, and just stops it from
silently truncating a user-supplied path into a different, unintended
path.