10 |
* Licensed under the GPL v2 or later, see the file LICENSE in this tarball. |
* Licensed under the GPL v2 or later, see the file LICENSE in this tarball. |
11 |
*/ |
*/ |
12 |
|
|
13 |
#include "busybox.h" |
#include "libbb.h" |
14 |
|
|
15 |
#ifndef CRONTABS |
#ifndef CRONTABS |
16 |
#define CRONTABS "/var/spool/cron/crontabs" |
#define CRONTABS "/var/spool/cron/crontabs" |
17 |
#endif |
#endif |
|
#ifndef TMPDIR |
|
|
#define TMPDIR "/var/spool/cron" |
|
|
#endif |
|
18 |
#ifndef CRONUPDATE |
#ifndef CRONUPDATE |
19 |
#define CRONUPDATE "cron.update" |
#define CRONUPDATE "cron.update" |
20 |
#endif |
#endif |
|
#ifndef PATH_VI |
|
|
#define PATH_VI "/bin/vi" /* location of vi */ |
|
|
#endif |
|
|
|
|
|
static const char *CDir = CRONTABS; |
|
21 |
|
|
22 |
static void EditFile(const char *user, const char *file); |
static void change_user(const struct passwd *pas) |
|
static int GetReplaceStream(const char *user, const char *file); |
|
|
static int ChangeUser(const char *user, short dochdir); |
|
|
|
|
|
int crontab_main(int ac, char **av) |
|
23 |
{ |
{ |
24 |
enum { NONE, EDIT, LIST, REPLACE, DELETE } option = NONE; |
xsetenv("USER", pas->pw_name); |
25 |
const struct passwd *pas; |
xsetenv("HOME", pas->pw_dir); |
26 |
const char *repFile = NULL; |
xsetenv("SHELL", DEFAULT_SHELL); |
|
int repFd = 0; |
|
|
int i; |
|
|
char caller[256]; /* user that ran program */ |
|
|
char buf[1024]; |
|
|
int UserId; |
|
|
|
|
|
UserId = getuid(); |
|
|
pas = getpwuid(UserId); |
|
|
if (pas == NULL) |
|
|
bb_perror_msg_and_die("getpwuid"); |
|
|
|
|
|
safe_strncpy(caller, pas->pw_name, sizeof(caller)); |
|
|
|
|
|
i = 1; |
|
|
if (ac > 1) { |
|
|
if (LONE_DASH(av[1])) { |
|
|
option = REPLACE; |
|
|
++i; |
|
|
} else if (av[1][0] != '-') { |
|
|
option = REPLACE; |
|
|
++i; |
|
|
repFile = av[1]; |
|
|
} |
|
|
} |
|
|
|
|
|
for (; i < ac; ++i) { |
|
|
char *ptr = av[i]; |
|
27 |
|
|
28 |
if (*ptr != '-') |
/* initgroups, setgid, setuid */ |
29 |
break; |
change_identity(pas); |
|
ptr += 2; |
|
30 |
|
|
31 |
switch (ptr[-1]) { |
if (chdir(pas->pw_dir) < 0) { |
32 |
case 'l': |
bb_perror_msg("chdir(%s) by %s failed", |
33 |
if (ptr[-1] == 'l') |
pas->pw_dir, pas->pw_name); |
34 |
option = LIST; |
xchdir("/tmp"); |
|
/* fall through */ |
|
|
case 'e': |
|
|
if (ptr[-1] == 'e') |
|
|
option = EDIT; |
|
|
/* fall through */ |
|
|
case 'd': |
|
|
if (ptr[-1] == 'd') |
|
|
option = DELETE; |
|
|
/* fall through */ |
|
|
case 'u': |
|
|
if (i + 1 < ac && av[i+1][0] != '-') { |
|
|
++i; |
|
|
if (getuid() == geteuid()) { |
|
|
pas = getpwnam(av[i]); |
|
|
if (pas) { |
|
|
UserId = pas->pw_uid; |
|
|
} else { |
|
|
bb_error_msg_and_die("user %s unknown", av[i]); |
|
|
} |
|
|
} else { |
|
|
bb_error_msg_and_die("only the superuser may specify a user"); |
|
|
} |
|
|
} |
|
|
break; |
|
|
case 'c': |
|
|
if (getuid() == geteuid()) { |
|
|
CDir = (*ptr) ? ptr : av[++i]; |
|
|
} else { |
|
|
bb_error_msg_and_die("-c option: superuser only"); |
|
|
} |
|
|
break; |
|
|
default: |
|
|
i = ac; |
|
|
break; |
|
|
} |
|
35 |
} |
} |
36 |
if (i != ac || option == NONE) |
} |
|
bb_show_usage(); |
|
|
|
|
|
/* |
|
|
* Get password entry |
|
|
*/ |
|
|
|
|
|
pas = getpwuid(UserId); |
|
|
if (pas == NULL) |
|
|
bb_perror_msg_and_die("getpwuid"); |
|
37 |
|
|
38 |
/* |
static void edit_file(const struct passwd *pas, const char *file) |
39 |
* If there is a replacement file, obtain a secure descriptor to it. |
{ |
40 |
*/ |
const char *ptr; |
41 |
|
int pid = vfork(); |
42 |
|
|
43 |
if (repFile) { |
if (pid < 0) /* failure */ |
44 |
repFd = GetReplaceStream(caller, repFile); |
bb_perror_msg_and_die("vfork"); |
45 |
if (repFd < 0) |
if (pid) { /* parent */ |
46 |
bb_error_msg_and_die("cannot read replacement file"); |
wait4pid(pid); |
47 |
|
return; |
48 |
|
} |
49 |
|
|
50 |
|
/* CHILD - change user and run editor */ |
51 |
|
change_user(pas); |
52 |
|
ptr = getenv("VISUAL"); |
53 |
|
if (!ptr) { |
54 |
|
ptr = getenv("EDITOR"); |
55 |
|
if (!ptr) |
56 |
|
ptr = "vi"; |
57 |
} |
} |
58 |
|
|
59 |
/* |
BB_EXECLP(ptr, ptr, file, NULL); |
60 |
* Change directory to our crontab directory |
bb_perror_msg_and_die("exec %s", ptr); |
61 |
*/ |
} |
|
|
|
|
xchdir(CDir); |
|
|
|
|
|
/* |
|
|
* Handle options as appropriate |
|
|
*/ |
|
62 |
|
|
63 |
switch (option) { |
static int open_as_user(const struct passwd *pas, const char *file) |
64 |
case LIST: |
{ |
65 |
{ |
pid_t pid; |
66 |
FILE *fi; |
char c; |
67 |
|
|
68 |
fi = fopen(pas->pw_name, "r"); |
pid = vfork(); |
69 |
if (fi) { |
if (pid < 0) /* ERROR */ |
70 |
while (fgets(buf, sizeof(buf), fi) != NULL) |
bb_perror_msg_and_die("vfork"); |
71 |
fputs(buf, stdout); |
if (pid) { /* PARENT */ |
72 |
fclose(fi); |
if (wait4pid(pid) == 0) { |
73 |
} else { |
/* exitcode 0: child says it can read */ |
74 |
bb_error_msg("no crontab for %s", pas->pw_name); |
return open(file, O_RDONLY); |
|
} |
|
75 |
} |
} |
76 |
break; |
return -1; |
|
case EDIT: |
|
|
{ |
|
|
/* FIXME: messy code here! we have file copying helpers for this! */ |
|
|
FILE *fi; |
|
|
int fd; |
|
|
int n; |
|
|
char tmp[128]; |
|
|
|
|
|
snprintf(tmp, sizeof(tmp), TMPDIR "/crontab.%d", getpid()); |
|
|
fd = xopen3(tmp, O_RDWR|O_CREAT|O_TRUNC|O_EXCL, 0600); |
|
|
/* race, use fchown */ |
|
|
chown(tmp, getuid(), getgid()); |
|
|
fi = fopen(pas->pw_name, "r"); |
|
|
if (fi) { |
|
|
while ((n = fread(buf, 1, sizeof(buf), fi)) > 0) |
|
|
full_write(fd, buf, n); |
|
|
} |
|
|
EditFile(caller, tmp); |
|
|
remove(tmp); |
|
|
lseek(fd, 0L, SEEK_SET); |
|
|
repFd = fd; |
|
|
} |
|
|
option = REPLACE; |
|
|
/* fall through */ |
|
|
case REPLACE: |
|
|
{ |
|
|
/* same here */ |
|
|
char path[1024]; |
|
|
int fd; |
|
|
int n; |
|
|
|
|
|
snprintf(path, sizeof(path), "%s.new", pas->pw_name); |
|
|
fd = open(path, O_CREAT|O_TRUNC|O_APPEND|O_WRONLY, 0600); |
|
|
if (fd >= 0) { |
|
|
while ((n = read(repFd, buf, sizeof(buf))) > 0) { |
|
|
full_write(fd, buf, n); |
|
|
} |
|
|
close(fd); |
|
|
rename(path, pas->pw_name); |
|
|
} else { |
|
|
bb_error_msg("cannot create %s/%s", CDir, path); |
|
|
} |
|
|
close(repFd); |
|
|
} |
|
|
break; |
|
|
case DELETE: |
|
|
remove(pas->pw_name); |
|
|
break; |
|
|
case NONE: |
|
|
default: |
|
|
break; |
|
77 |
} |
} |
78 |
|
|
79 |
/* |
/* CHILD */ |
80 |
* Bump notification file. Handle window where crond picks file up |
/* initgroups, setgid, setuid */ |
81 |
* before we can write our entry out. |
change_identity(pas); |
82 |
*/ |
/* We just try to read one byte. If it works, file is readable |
83 |
|
* under this user. We signal that by exiting with 0. */ |
84 |
if (option == REPLACE || option == DELETE) { |
_exit(safe_read(xopen(file, O_RDONLY), &c, 1) < 0); |
|
FILE *fo; |
|
|
struct stat st; |
|
|
|
|
|
while ((fo = fopen(CRONUPDATE, "a"))) { |
|
|
fprintf(fo, "%s\n", pas->pw_name); |
|
|
fflush(fo); |
|
|
if (fstat(fileno(fo), &st) != 0 || st.st_nlink != 0) { |
|
|
fclose(fo); |
|
|
break; |
|
|
} |
|
|
fclose(fo); |
|
|
/* loop */ |
|
|
} |
|
|
if (fo == NULL) { |
|
|
bb_error_msg("cannot append to %s/%s", CDir, CRONUPDATE); |
|
|
} |
|
|
} |
|
|
return 0; |
|
85 |
} |
} |
86 |
|
|
87 |
static int GetReplaceStream(const char *user, const char *file) |
int crontab_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; |
88 |
|
int crontab_main(int argc UNUSED_PARAM, char **argv) |
89 |
{ |
{ |
90 |
int filedes[2]; |
const struct passwd *pas; |
91 |
int pid; |
const char *crontab_dir = CRONTABS; |
92 |
|
char *tmp_fname; |
93 |
|
char *new_fname; |
94 |
|
char *user_name; /* -u USER */ |
95 |
int fd; |
int fd; |
96 |
int n; |
int src_fd; |
97 |
char buf[1024]; |
int opt_ler; |
98 |
|
|
99 |
if (pipe(filedes) < 0) { |
/* file [opts] Replace crontab from file |
100 |
perror("pipe"); |
* - [opts] Replace crontab from stdin |
101 |
return -1; |
* -u user User |
102 |
} |
* -c dir Crontab directory |
103 |
pid = fork(); |
* -l List crontab for user |
104 |
if (pid < 0) { |
* -e Edit crontab for user |
105 |
perror("fork"); |
* -r Delete crontab for user |
106 |
return -1; |
* bbox also supports -d == -r, but most other crontab |
107 |
} |
* implementations do not. Deprecated. |
108 |
if (pid > 0) { |
*/ |
109 |
/* |
enum { |
110 |
* PARENT |
OPT_u = (1 << 0), |
111 |
*/ |
OPT_c = (1 << 1), |
112 |
|
OPT_l = (1 << 2), |
113 |
close(filedes[1]); |
OPT_e = (1 << 3), |
114 |
if (read(filedes[0], buf, 1) != 1) { |
OPT_r = (1 << 4), |
115 |
close(filedes[0]); |
OPT_ler = OPT_l + OPT_e + OPT_r, |
116 |
filedes[0] = -1; |
}; |
117 |
|
|
118 |
|
opt_complementary = "?1:dr"; /* max one argument; -d implies -r */ |
119 |
|
opt_ler = getopt32(argv, "u:c:lerd", &user_name, &crontab_dir); |
120 |
|
argv += optind; |
121 |
|
|
122 |
|
if (sanitize_env_if_suid()) { /* Clears dangerous stuff, sets PATH */ |
123 |
|
/* run by non-root? */ |
124 |
|
if (opt_ler & (OPT_u|OPT_c)) |
125 |
|
bb_error_msg_and_die("only root can use -c or -u"); |
126 |
|
} |
127 |
|
|
128 |
|
if (opt_ler & OPT_u) { |
129 |
|
pas = getpwnam(user_name); |
130 |
|
if (!pas) |
131 |
|
bb_error_msg_and_die("user %s is not known", user_name); |
132 |
|
} else { |
133 |
|
/* XXX: xgetpwuid */ |
134 |
|
uid_t my_uid = getuid(); |
135 |
|
pas = getpwuid(my_uid); |
136 |
|
if (!pas) |
137 |
|
bb_perror_msg_and_die("unknown uid %d", (int)my_uid); |
138 |
|
} |
139 |
|
|
140 |
|
#define user_name DONT_USE_ME_BEYOND_THIS_POINT |
141 |
|
|
142 |
|
/* From now on, keep only -l, -e, -r bits */ |
143 |
|
opt_ler &= OPT_ler; |
144 |
|
if ((opt_ler - 1) & opt_ler) /* more than one bit set? */ |
145 |
|
bb_show_usage(); |
146 |
|
|
147 |
|
/* Read replacement file under user's UID/GID/group vector */ |
148 |
|
src_fd = STDIN_FILENO; |
149 |
|
if (!opt_ler) { /* Replace? */ |
150 |
|
if (!argv[0]) |
151 |
|
bb_show_usage(); |
152 |
|
if (NOT_LONE_DASH(argv[0])) { |
153 |
|
src_fd = open_as_user(pas, argv[0]); |
154 |
|
if (src_fd < 0) |
155 |
|
bb_error_msg_and_die("user %s cannot read %s", |
156 |
|
pas->pw_name, argv[0]); |
157 |
} |
} |
|
return filedes[0]; |
|
158 |
} |
} |
159 |
|
|
160 |
/* |
/* cd to our crontab directory */ |
161 |
* CHILD |
xchdir(crontab_dir); |
|
*/ |
|
162 |
|
|
163 |
close(filedes[0]); |
tmp_fname = NULL; |
164 |
|
|
165 |
if (ChangeUser(user, 0) < 0) |
/* Handle requested operation */ |
166 |
exit(0); |
switch (opt_ler) { |
167 |
|
|
168 |
xfunc_error_retval = 0; |
default: /* case OPT_r: Delete */ |
169 |
fd = xopen(file, O_RDONLY); |
unlink(pas->pw_name); |
170 |
buf[0] = 0; |
break; |
|
write(filedes[1], buf, 1); |
|
|
while ((n = read(fd, buf, sizeof(buf))) > 0) { |
|
|
write(filedes[1], buf, n); |
|
|
} |
|
|
exit(0); |
|
|
} |
|
|
|
|
|
static void EditFile(const char *user, const char *file) |
|
|
{ |
|
|
int pid = fork(); |
|
|
|
|
|
if (pid == 0) { |
|
|
/* |
|
|
* CHILD - change user and run editor |
|
|
*/ |
|
|
char *ptr; |
|
|
char visual[1024]; |
|
|
|
|
|
if (ChangeUser(user, 1) < 0) |
|
|
exit(0); |
|
|
ptr = getenv("VISUAL"); |
|
|
if (ptr == NULL || strlen(ptr) > 256) |
|
|
ptr = PATH_VI; |
|
|
|
|
|
snprintf(visual, sizeof(visual), "%s %s", ptr, file); |
|
|
execl(DEFAULT_SHELL, DEFAULT_SHELL, "-c", visual, NULL); |
|
|
perror("exec"); |
|
|
exit(0); |
|
|
} |
|
|
if (pid < 0) { |
|
|
/* |
|
|
* PARENT - failure |
|
|
*/ |
|
|
bb_perror_msg_and_die("fork"); |
|
|
} |
|
|
wait4(pid, NULL, 0, NULL); |
|
|
} |
|
171 |
|
|
172 |
static int ChangeUser(const char *user, short dochdir) |
case OPT_l: /* List */ |
173 |
{ |
{ |
174 |
struct passwd *pas; |
char *args[2] = { pas->pw_name, NULL }; |
175 |
|
return bb_cat(args); |
176 |
|
/* list exits, |
177 |
|
* the rest go play with cron update file */ |
178 |
|
} |
179 |
|
|
180 |
|
case OPT_e: /* Edit */ |
181 |
|
tmp_fname = xasprintf("%s.%u", crontab_dir, (unsigned)getpid()); |
182 |
|
/* No O_EXCL: we don't want to be stuck if earlier crontabs |
183 |
|
* were killed, leaving stale temp file behind */ |
184 |
|
src_fd = xopen3(tmp_fname, O_RDWR|O_CREAT|O_TRUNC, 0600); |
185 |
|
fchown(src_fd, pas->pw_uid, pas->pw_gid); |
186 |
|
fd = open(pas->pw_name, O_RDONLY); |
187 |
|
if (fd >= 0) { |
188 |
|
bb_copyfd_eof(fd, src_fd); |
189 |
|
close(fd); |
190 |
|
xlseek(src_fd, 0, SEEK_SET); |
191 |
|
} |
192 |
|
close_on_exec_on(src_fd); /* don't want editor to see this fd */ |
193 |
|
edit_file(pas, tmp_fname); |
194 |
|
/* fall through */ |
195 |
|
|
196 |
/* |
case 0: /* Replace (no -l, -e, or -r were given) */ |
197 |
* Obtain password entry and change privileges |
new_fname = xasprintf("%s.new", pas->pw_name); |
198 |
*/ |
fd = open(new_fname, O_WRONLY|O_CREAT|O_TRUNC|O_APPEND, 0600); |
199 |
|
if (fd >= 0) { |
200 |
|
bb_copyfd_eof(src_fd, fd); |
201 |
|
close(fd); |
202 |
|
xrename(new_fname, pas->pw_name); |
203 |
|
} else { |
204 |
|
bb_error_msg("cannot create %s/%s", |
205 |
|
crontab_dir, new_fname); |
206 |
|
} |
207 |
|
if (tmp_fname) |
208 |
|
unlink(tmp_fname); |
209 |
|
/*free(tmp_fname);*/ |
210 |
|
/*free(new_fname);*/ |
211 |
|
|
212 |
pas = getpwnam(user); |
} /* switch */ |
|
if (pas == NULL) { |
|
|
bb_perror_msg_and_die("failed to get uid for %s", user); |
|
|
} |
|
|
setenv("USER", pas->pw_name, 1); |
|
|
setenv("HOME", pas->pw_dir, 1); |
|
|
setenv("SHELL", DEFAULT_SHELL, 1); |
|
213 |
|
|
214 |
/* |
/* Bump notification file. Handle window where crond picks file up |
215 |
* Change running state to the user in question |
* before we can write our entry out. |
216 |
*/ |
*/ |
217 |
change_identity(pas); |
while ((fd = open(CRONUPDATE, O_WRONLY|O_CREAT|O_APPEND, 0600)) >= 0) { |
218 |
|
struct stat st; |
219 |
|
|
220 |
if (dochdir) { |
fdprintf(fd, "%s\n", pas->pw_name); |
221 |
if (chdir(pas->pw_dir) < 0) { |
if (fstat(fd, &st) != 0 || st.st_nlink != 0) { |
222 |
bb_perror_msg("chdir(%s) by %s failed", pas->pw_dir, user); |
/*close(fd);*/ |
223 |
xchdir(TMPDIR); |
break; |
224 |
} |
} |
225 |
|
/* st.st_nlink == 0: |
226 |
|
* file was deleted, maybe crond missed our notification */ |
227 |
|
close(fd); |
228 |
|
/* loop */ |
229 |
|
} |
230 |
|
if (fd < 0) { |
231 |
|
bb_error_msg("cannot append to %s/%s", |
232 |
|
crontab_dir, CRONUPDATE); |
233 |
} |
} |
234 |
return pas->pw_uid; |
return 0; |
235 |
} |
} |