cronie/src/do_command.c
2025-08-06 17:45:07 +02:00

685 lines
19 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Copyright 1988,1990,1993,1994 by Paul Vixie
* All rights reserved
*/
/*
* Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC")
* Copyright (c) 1997,2000 by Internet Software Consortium, Inc.
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
* OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include "config.h"
#include <ctype.h>
#include <errno.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <dirent.h>
#include "externs.h"
#include "funcs.h"
#include "globals.h"
#include "structs.h"
#include "cronie_common.h"
#ifndef isascii
# define isascii(c) ((unsigned)(c)<=0177)
#endif
static int child_process(entry *, char **);
static int safe_p(const char *, const char *);
void do_command(entry * e, user * u) {
pid_t pid = getpid();
int ev;
char **jobenv = NULL;
Debug(DPROC, ("[%ld] do_command(%s, (%s,%ld,%ld))\n",
(long) pid, e->cmd, u->name,
(long) e->pwd->pw_uid, (long) e->pwd->pw_gid));
/* fork to become asynchronous -- parent process is done immediately,
* and continues to run the normal cron code, which means return to
* tick(). the child and grandchild don't leave this function, alive.
*
* vfork() is unsuitable, since we have much to do, and the parent
* needs to be able to run off and fork other processes.
*/
switch (fork()) {
case -1:
log_it("CRON", pid, "CAN'T FORK", "do_command", errno);
break;
case 0:
/* child process */
acquire_daemonlock(1);
/* Set up the Red Hat security context for both mail/minder and job processes:
*/
if (cron_set_job_security_context(e, u, &jobenv) != 0) {
_exit(ERROR_EXIT);
}
ev = child_process(e, jobenv);
#ifdef WITH_PAM
cron_close_pam();
#endif
env_free(jobenv);
Debug(DPROC, ("[%ld] child process done, exiting\n", (long) getpid()));
_exit(ev);
break;
default:
/* parent process */
break;
}
Debug(DPROC, ("[%ld] main process returning to work\n", (long) pid));
}
static int child_process(entry * e, char **jobenv) {
int stdin_pipe[2], stdout_pipe[2];
char *input_data, *usernm, *mailto, *mailfrom;
char mailto_expanded[MAX_EMAILSTR];
char mailfrom_expanded[MAX_EMAILSTR];
int children = 0;
pid_t pid = getpid();
pid_t jobpid = -1;
struct sigaction sa;
/* Ignore SIGPIPE as we will be writing to pipes and do not want to terminate
prematurely */
memset(&sa, 0, sizeof(sa));
sa.sa_handler = SIG_IGN;
sigaction(SIGPIPE, &sa, NULL);
/* our parent is watching for our death by catching SIGCHLD. we
* do not care to watch for our children's deaths this way -- we
* use wait() explicitly. so we have to reset the signal (which
* was inherited from the parent).
*/
sa.sa_handler = SIG_DFL;
sigaction(SIGCHLD, &sa, NULL);
Debug(DPROC, ("[%ld] child_process('%s')\n", (long) getpid(), e->cmd));
#ifdef CAPITALIZE_FOR_PS
/* mark ourselves as different to PS command watchers by upshifting
* our program name. This has no effect on some kernels.
*/
/*local */ {
char *pch;
for (pch = ProgramName; *pch; pch++)
*pch = (char)MkUpper(*pch);
}
#endif /* CAPITALIZE_FOR_PS */
/* discover some useful and important environment settings
*/
usernm = e->pwd->pw_name;
mailto = env_get("MAILTO", jobenv);
mailfrom = env_get("MAILFROM", e->envp);
if (mailto != NULL) {
if (expand_envvar(mailto, mailto_expanded, sizeof(mailto_expanded))) {
mailto = mailto_expanded;
}
else {
log_it("CRON", pid, "WARNING", "The environment variable 'MAILTO' could not be expanded. The non-expanded value will be used." , 0);
}
}
if (mailfrom != NULL) {
if (expand_envvar(mailfrom, mailfrom_expanded, sizeof(mailfrom_expanded))) {
mailfrom = mailfrom_expanded;
}
else {
log_it("CRON", pid, "WARNING", "The environment variable 'MAILFROM' could not be expanded. The non-expanded value will be used." , 0);
}
}
/* create some pipes to talk to our future child
*/
if (pipe(stdin_pipe) == -1) { /* child's stdin */
log_it("CRON", pid, "PIPE() FAILED", "stdin_pipe", errno);
return ERROR_EXIT;
}
if (pipe(stdout_pipe) == -1) { /* child's stdout */
log_it("CRON", pid, "PIPE() FAILED", "stdout_pipe", errno);
return ERROR_EXIT;
}
/* since we are a forked process, we can diddle the command string
* we were passed -- nobody else is going to use it again, right?
*
* if a % is present in the command, previous characters are the
* command, and subsequent characters are the additional input to
* the command. An escaped % will have the escape character stripped
* from it. Subsequent %'s will be transformed into newlines,
* but that happens later.
*/
/*local */ {
int escaped = FALSE;
int ch;
char *p;
for (input_data = p = e->cmd;
(ch = *input_data) != '\0'; input_data++, p++) {
if (p != input_data)
*p = (char)ch;
if (escaped) {
if (ch == '%')
*--p = (char)ch;
escaped = FALSE;
continue;
}
if (ch == '\\') {
escaped = TRUE;
continue;
}
if (ch == '%') {
*input_data++ = '\0';
break;
}
}
*p = '\0';
}
/* fork again, this time so we can exec the user's command.
*/
switch (jobpid = fork()) {
case -1:
log_it("CRON", pid, "CAN'T FORK", "child_process", errno);
return ERROR_EXIT;
/*NOTREACHED*/
case 0:
Debug(DPROC, ("[%ld] grandchild process fork()'ed\n", (long) getpid()));
/* write a log message. we've waited this long to do it
* because it was not until now that we knew the PID that
* the actual user command shell was going to get and the
* PID is part of the log message.
*/
if ((e->flags & DONT_LOG) == 0) {
char *x = mkprints((u_char *) e->cmd, strlen(e->cmd));
if (x == NULL) /* out of memory, better exit */
_exit(ERROR_EXIT);
log_it(usernm, getpid(), "CMD", x, 0);
free(x);
}
if (cron_change_user_permanently(e->pwd, env_get("HOME", jobenv)) < 0)
_exit(ERROR_EXIT);
/* get new pgrp, void tty, etc.
*/
(void) setsid();
/* reset the SIGPIPE back to default so the child will terminate
* if it tries to write to a closed pipe
*/
sa.sa_handler = SIG_DFL;
sigaction(SIGPIPE, &sa, NULL);
/* close the pipe ends that we won't use. this doesn't affect
* the parent, who has to read and write them; it keeps the
* kernel from recording us as a potential client TWICE --
* which would keep it from sending SIGPIPE in otherwise
* appropriate circumstances.
*/
close(stdin_pipe[WRITE_PIPE]);
close(stdout_pipe[READ_PIPE]);
/* grandchild process. make std{in,out} be the ends of
* pipes opened by our daddy; make stderr go to stdout.
*/
if (stdin_pipe[READ_PIPE] != STDIN) {
dup2(stdin_pipe[READ_PIPE], STDIN);
close(stdin_pipe[READ_PIPE]);
}
if (stdout_pipe[WRITE_PIPE] != STDOUT) {
dup2(stdout_pipe[WRITE_PIPE], STDOUT);
close(stdout_pipe[WRITE_PIPE]);
}
dup2(STDOUT, STDERR);
/*
* Exec the command.
*/
{
char *shell = env_get("SHELL", jobenv);
int fd, fdmax = TMIN(getdtablesize(), MAX_CLOSE_FD);
DIR *dir;
struct dirent *dent;
/*
* if /proc is mounted, we can optimize what fd can be closed,
* but if it isn't available, fall back to the previous behavior.
*/
if ((dir = opendir("/proc/self/fd")) != NULL) {
while ((dent = readdir(dir)) != NULL) {
if (!strcmp(dent->d_name, ".") || !strcmp(dent->d_name, ".."))
continue;
fd = atoi(dent->d_name);
if (fd > STDERR_FILENO)
close(fd);
}
} else {
/* close all unwanted open file descriptors */
for (fd = STDERR + 1; fd < fdmax; fd++) {
close(fd);
}
}
#if DEBUGGING
if (DebugFlags & DTEST) {
fprintf(stderr, "debug DTEST is on, not exec'ing command.\n");
fprintf(stderr, "\tcmd='%s' shell='%s'\n", e->cmd, shell);
_exit(OK_EXIT);
}
#endif /*DEBUGGING*/
execle(shell, shell, "-c", e->cmd, (char *) 0, jobenv);
fprintf(stderr, "execl: couldn't exec `%s'\n", shell);
perror("execl");
_exit(ERROR_EXIT);
}
break;
default:
cron_restore_default_security_context();
/* parent process */
break;
}
children++;
/* middle process, child of original cron, parent of process running
* the user's command.
*/
Debug(DPROC, ("[%ld] child continues, closing pipes\n", (long) getpid()));
/* close the ends of the pipe that will only be referenced in the
* grandchild process...
*/
close(stdin_pipe[READ_PIPE]);
close(stdout_pipe[WRITE_PIPE]);
/*
* write, to the pipe connected to child's stdin, any input specified
* after a % in the crontab entry. while we copy, convert any
* additional %'s to newlines. when done, if some characters were
* written and the last one wasn't a newline, write a newline.
*
* Note that if the input data won't fit into one pipe buffer (2K
* or 4K on most BSD systems), and the child doesn't read its stdin,
* we would block here. thus we must fork again.
*/
if (*input_data && fork() == 0) {
FILE *out = fdopen(stdin_pipe[WRITE_PIPE], "w");
int need_newline = FALSE;
int escaped = FALSE;
int ch;
Debug(DPROC, ("[%ld] child2 sending data to grandchild\n",
(long) getpid()));
/* reset the SIGPIPE back to default so the child will terminate
* if it tries to write to a closed pipe
*/
sa.sa_handler = SIG_DFL;
sigaction(SIGPIPE, &sa, NULL);
/* close the pipe we don't use, since we inherited it and
* are part of its reference count now.
*/
close(stdout_pipe[READ_PIPE]);
if (cron_change_user_permanently(e->pwd, env_get("HOME", jobenv)) < 0)
_exit(ERROR_EXIT);
/* translation:
* \% -> %
* % -> \n
* \x -> \x for all x != %
*/
while ((ch = *input_data++) != '\0') {
if (escaped) {
if (ch != '%')
putc('\\', out);
}
else {
if (ch == '%')
ch = '\n';
}
if (!(escaped = (ch == '\\'))) {
putc(ch, out);
need_newline = (ch != '\n');
}
}
if (escaped)
putc('\\', out);
if (need_newline)
putc('\n', out);
/* close the pipe, causing an EOF condition. fclose causes
* stdin_pipe[WRITE_PIPE] to be closed, too.
*/
fclose(out);
Debug(DPROC, ("[%ld] child2 done sending to grandchild\n",
(long) getpid()));
_exit(0);
}
/* close the pipe to the grandkiddie's stdin, since its wicked uncle
* ernie back there has it open and will close it when he's done.
*/
close(stdin_pipe[WRITE_PIPE]);
children++;
/*
* read output from the grandchild. it's stderr has been redirected to
* it's stdout, which has been redirected to our pipe. if there is any
* output, we'll be mailing it to the user whose crontab this is...
* when the grandchild exits, we'll get EOF.
*/
Debug(DPROC, ("[%ld] child reading output from grandchild\n",
(long) getpid()));
/*local */ {
FILE *in = fdopen(stdout_pipe[READ_PIPE], "r");
int ch = getc(in);
if (ch != EOF) {
FILE *mail = NULL;
int bytes = 1;
int status = 0;
#if defined(SYSLOG)
char logbuf[1024];
int bufidx = 0;
if (SyslogOutput) {
if (ch != '\n')
logbuf[bufidx++] = (char)ch;
}
#endif
Debug(DPROC | DEXT,
("[%ld] got data (%x:%c) from grandchild\n",
(long) getpid(), ch, ch));
/* get name of recipient. this is MAILTO if set to a
* valid local username; USER otherwise.
*/
if (mailto) {
/* MAILTO was present in the environment
*/
if (!*mailto) {
/* ... but it's empty. set to NULL
*/
mailto = NULL;
}
}
else {
/* MAILTO not present, set to USER.
*/
mailto = usernm;
}
/* get sender address. this is MAILFROM if set (and safe),
* the user account name otherwise.
*/
if (!mailfrom || !*mailfrom || !safe_p(usernm, mailfrom)) {
mailfrom = e->pwd->pw_name;
}
/* if we are supposed to be mailing, MAILTO will
* be non-NULL. only in this case should we set
* up the mail command and subjects and stuff...
*/
/* Also skip it if MailCmd is set to "off" */
if (mailto && safe_p(usernm, mailto)
&& strncmp(MailCmd,"off",3) && !SyslogOutput) {
char **env;
char mailcmd[MAX_COMMAND+1]; /* +1 for terminator */
char hostname[MAXHOSTNAMELEN];
char *content_type = env_get("CONTENT_TYPE", jobenv),
*content_transfer_encoding =
env_get("CONTENT_TRANSFER_ENCODING", jobenv);
gethostname(hostname, MAXHOSTNAMELEN);
if (MailCmd[0] == '\0') {
int len;
len = snprintf(mailcmd, sizeof mailcmd, MAILFMT, MAILARG, mailfrom);
if (len < 0) {
fprintf(stderr, "mailcmd snprintf failed\n");
(void) _exit(ERROR_EXIT);
}
if (sizeof mailcmd <= (size_t) len) {
fprintf(stderr, "mailcmd too long\n");
(void) _exit(ERROR_EXIT);
}
}
else {
strncpy(mailcmd, MailCmd, MAX_COMMAND+1);
}
if (!(mail = cron_popen(mailcmd, "w", e->pwd, jobenv))) {
perror(mailcmd);
(void) _exit(ERROR_EXIT);
}
fprintf(mail, "From: \"(Cron Daemon)\" <%s>\n", mailfrom);
fprintf(mail, "To: %s\n", mailto);
fprintf(mail, "Subject: Cron <%s@%s> %s\n",
usernm, first_word(hostname, "."), e->cmd);
#ifdef MAIL_DATE
fprintf(mail, "Date: %s\n", arpadate(&StartTime));
#endif /*MAIL_DATE */
fprintf(mail, "MIME-Version: 1.0\n");
if (content_type == NULL) {
fprintf(mail, "Content-Type: text/plain; charset=%s\n",
cron_default_mail_charset);
}
else { /* user specified Content-Type header.
* disallow new-lines for security reasons
* (else users could specify arbitrary mail headers!)
*/
char *nl = content_type;
size_t ctlen = strlen(content_type);
while ((*nl != '\0')
&& ((nl = strchr(nl, '\n')) != NULL)
&& (nl < (content_type + ctlen))
)
*nl = ' ';
fprintf(mail, "Content-Type: %s\n", content_type);
}
if (content_transfer_encoding == NULL) {
fprintf(mail, "Content-Transfer-Encoding: 8bit\n");
}
else {
char *nl = content_transfer_encoding;
size_t ctlen = strlen(content_transfer_encoding);
while ((*nl != '\0')
&& ((nl = strchr(nl, '\n')) != NULL)
&& (nl < (content_transfer_encoding + ctlen))
)
*nl = ' ';
fprintf(mail, "Content-Transfer-Encoding: %s\n",
content_transfer_encoding);
}
/* The Auto-Submitted header is
* defined (and suggested by) RFC3834.
*/
fprintf(mail, "Auto-Submitted: auto-generated\n");
fprintf(mail, "Precedence: bulk\n");
for (env = jobenv; *env; env++)
fprintf(mail, "X-Cron-Env: <%s>\n", *env);
fprintf(mail, "\n");
/* this was the first char from the pipe
*/
putc(ch, mail);
}
/* we have to read the input pipe no matter whether
* we mail or not, but obviously we only write to
* mail pipe if we ARE mailing.
*/
while (EOF != (ch = getc(in))) {
if (ch == '\r')
continue;
bytes++;
if (mail)
putc(ch, mail);
#if defined(SYSLOG)
if (SyslogOutput) {
logbuf[bufidx++] = (char)ch;
if ((ch == '\n') || (bufidx == sizeof(logbuf)-1)) {
if (ch == '\n')
logbuf[bufidx-1] = '\0';
else
logbuf[bufidx] = '\0';
log_it(usernm, getpid(), "CMDOUT", logbuf, 0);
bufidx = 0;
}
}
#endif
}
/* if -n option was specified, abort the sending
* now when we read all of the command output
* and thus can wait for it's exit status
*/
if (mail && e->flags & MAIL_WHEN_ERR) {
int jobstatus = -1;
if (jobpid > 0) {
while (waitpid(jobpid, &jobstatus, 0) == -1) {
if (errno == EINTR) continue;
log_it("CRON", getpid(), "error", "invalid job pid", errno);
break;
}
} else {
log_it("CRON", getpid(), "error", "invalid job pid", 0);
}
/* if everything went well, -n is set, and we have mail,
* we won't be mailing so shoot the messenger!
*/
if (WIFEXITED(jobstatus) && WEXITSTATUS(jobstatus) == EXIT_SUCCESS) {
Debug(DPROC, ("[%ld] aborting pipe to mail\n", (long)getpid()));
status = cron_pabort(mail);
mail = NULL;
}
}
/* only close pipe if we opened it -- i.e., we're (still)
* mailing...
*/
if (mail) {
Debug(DPROC, ("[%ld] closing pipe to mail\n", (long) getpid()));
/* Note: the pclose will probably see
* the termination of the grandchild
* in addition to the mail process, since
* it (the grandchild) is likely to exit
* after closing its stdout.
*/
status = cron_pclose(mail);
}
#if defined(SYSLOG)
if (SyslogOutput) {
if (bufidx) {
logbuf[bufidx] = '\0';
log_it(usernm, getpid(), "CMDOUT", logbuf, 0);
}
}
#endif
/* if there was output and we could not mail it,
* log the facts so the poor user can figure out
* what's going on.
*/
if (mail && status && !SyslogOutput) {
char buf[MAX_TEMPSTR];
sprintf(buf,
"mailed %d byte%s of output but got status 0x%04x\n",
bytes, (bytes == 1) ? "" : "s", status);
log_it(usernm, getpid(), "MAIL", buf, 0);
}
} /*if data from grandchild */
Debug(DPROC, ("[%ld] got EOF from grandchild\n", (long) getpid()));
fclose(in); /* also closes stdout_pipe[READ_PIPE] */
}
/* wait for children to die.
*/
for (; children > 0; children--) {
WAIT_T waiter;
PID_T child;
Debug(DPROC, ("[%ld] waiting for grandchild #%d to finish\n",
(long) getpid(), children));
while ((child = wait(&waiter)) < OK && errno == EINTR) ;
if (child < OK) {
Debug(DPROC,
("[%ld] no more grandchildren--mail written?\n",
(long) getpid()));
break;
}
Debug(DPROC, ("[%ld] grandchild #%ld finished, status=%04x",
(long) getpid(), (long) child, WEXITSTATUS(waiter)));
if (WIFSIGNALED(waiter) && WCOREDUMP(waiter))
Debug(DPROC, (", dumped core"));
Debug(DPROC, ("\n"));
}
if ((e->flags & DONT_LOG) == 0) {
char *x = mkprints((u_char *) e->cmd, strlen(e->cmd));
log_it(usernm, getpid(), "CMDEND", x ? x : "**Unknown command**" , 0);
free(x);
}
return OK_EXIT;
}
static int safe_p(const char *usernm, const char *s) {
static const char safe_delim[] = "@!:%-.,_+"; /* conservative! */
const char *t;
int ch, first;
for (t = s, first = 1; (ch = *t++) != '\0'; first = 0) {
if (isascii(ch) && isprint(ch) &&
(isalnum(ch) || (!first && strchr(safe_delim, ch))))
continue;
log_it(usernm, getpid(), "UNSAFE", s, 0);
return (FALSE);
}
return (TRUE);
}