1 /*
2 Copyright 2020 Northern.tech AS
3
4 This file is part of CFEngine 3 - written and maintained by Northern.tech AS.
5
6 This program is free software; you can redistribute it and/or modify it
7 under the terms of the GNU General Public License as published by the
8 Free Software Foundation; version 3.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
18
19 To the extent this program is licensed as part of the Enterprise
20 versions of CFEngine, the applicable Commercial Open Source License
21 (COSL) may apply to this file if you as a licensee so wish it. See
22 included file COSL.txt.
23 */
24
25 #include <verify_exec.h>
26
27 #include <actuator.h>
28 #include <promises.h>
29 #include <files_lib.h>
30 #include <files_interfaces.h>
31 #include <vars.h>
32 #include <conversion.h>
33 #include <instrumentation.h>
34 #include <attributes.h>
35 #include <pipes.h>
36 #include <locks.h>
37 #include <evalfunction.h>
38 #include <exec_tools.h>
39 #include <misc_lib.h>
40 #include <writer.h>
41 #include <policy.h>
42 #include <string_lib.h>
43 #include <scope.h>
44 #include <ornaments.h>
45 #include <eval_context.h>
46 #include <retcode.h>
47 #include <timeout.h>
48
49 typedef enum
50 {
51 ACTION_RESULT_OK,
52 ACTION_RESULT_TIMEOUT,
53 ACTION_RESULT_FAILED
54 } ActionResult;
55
56 static bool SyntaxCheckExec(const Attributes *attr, const Promise *pp);
57 static bool PromiseKeptExec(const Attributes *a, const Promise *pp);
58 static char *GetLockNameExec(const Attributes *a, const Promise *pp);
59 static ActionResult RepairExec(EvalContext *ctx, const Attributes *a, const Promise *pp, PromiseResult *result);
60
61 static void PreviewProtocolLine(char *line, char *comm);
62
BuildCommandLine(const Attributes * a,const Promise * pp)63 char* BuildCommandLine(const Attributes *a, const Promise *pp)
64 {
65 assert(a != NULL);
66 Writer *w = StringWriter();
67 WriterWriteF(w, "%s", pp->promiser);
68
69 if (a->args)
70 {
71 WriterWrite(w, " ");
72 WriterWrite(w, a->args);
73 }
74
75 if (a->arglist)
76 {
77 for (const Rlist *rp = a->arglist; rp != NULL; rp = rp->next)
78 {
79 switch (rp->val.type)
80 {
81 case RVAL_TYPE_SCALAR:
82 WriterWrite(w, " ");
83 WriterWrite(w, RlistScalarValue(rp));
84 break;
85
86 default:
87 Log(LOG_LEVEL_INFO, "GetLockNameExec: invalid rval (not a scalar) in arglist of commands promise '%s'", pp->promiser);
88 break;
89 }
90 }
91 }
92
93 return StringWriterClose(w);
94 }
95
VerifyExecPromise(EvalContext * ctx,const Promise * pp)96 PromiseResult VerifyExecPromise(EvalContext *ctx, const Promise *pp)
97 {
98 Attributes a = GetExecAttributes(ctx, pp);
99
100 if (!SyntaxCheckExec(&a, pp))
101 {
102 return PROMISE_RESULT_FAIL;
103 }
104
105 if (PromiseKeptExec(&a, pp))
106 {
107 return PROMISE_RESULT_NOOP;
108 }
109
110 char *lock_name = GetLockNameExec(&a, pp);
111 CfLock thislock = AcquireLock(ctx, lock_name, VUQNAME, CFSTARTTIME, a.transaction.ifelapsed, a.transaction.expireafter, pp, false);
112 free(lock_name);
113 if (thislock.lock == NULL)
114 {
115 return PROMISE_RESULT_SKIPPED;
116 }
117
118 PromiseBanner(ctx,pp);
119
120 PromiseResult result = PROMISE_RESULT_NOOP;
121 /* See VerifyCommandRetcode for interpretation of return codes.
122 * Unless overridden by attributes in body classes, an exit code 0 means
123 * reparied (PROMISE_RESULT_CHANGE), an exit code != 0 means failure.
124 */
125 switch (RepairExec(ctx, &a, pp, &result))
126 {
127 case ACTION_RESULT_OK:
128 result = PromiseResultUpdate(result, PROMISE_RESULT_NOOP);
129 break;
130
131 case ACTION_RESULT_TIMEOUT:
132 result = PromiseResultUpdate(result, PROMISE_RESULT_TIMEOUT);
133 break;
134
135 case ACTION_RESULT_FAILED:
136 result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
137 break;
138
139 default:
140 ProgrammingError("Unexpected ActionResult value");
141 }
142
143 YieldCurrentLock(thislock);
144
145 return result;
146 }
147
148 /*****************************************************************************/
149 /* Level */
150 /*****************************************************************************/
151
SyntaxCheckExec(const Attributes * attr,const Promise * pp)152 static bool SyntaxCheckExec(const Attributes *attr, const Promise *pp)
153 {
154 assert(attr != NULL);
155 Attributes a = *attr; // TODO get rid of this, this function was probably
156 // intended to have side effects on the attr struct
157 if ((a.contain.nooutput) && (a.contain.preview))
158 {
159 Log(LOG_LEVEL_ERR, "no_output and preview are mutually exclusive (broken promise)");
160 PromiseRef(LOG_LEVEL_ERR, pp);
161 return false;
162 }
163
164 #ifdef __MINGW32__
165 if (a.contain.umask != (mode_t)CF_UNDEFINED)
166 {
167 Log(LOG_LEVEL_VERBOSE, "contain.umask is ignored on Windows");
168 }
169
170 if (a.contain.owner != CF_UNDEFINED)
171 {
172 Log(LOG_LEVEL_VERBOSE, "contain.exec_owner is ignored on Windows");
173 }
174
175 if (a.contain.group != CF_UNDEFINED)
176 {
177 Log(LOG_LEVEL_VERBOSE, "contain.exec_group is ignored on Windows");
178 }
179
180 if (a.contain.chroot != NULL)
181 {
182 Log(LOG_LEVEL_VERBOSE, "contain.chroot is ignored on Windows");
183 }
184
185 #else /* !__MINGW32__ */
186 if (a.contain.umask == (mode_t)CF_UNDEFINED)
187 {
188 a.contain.umask = 077; // FIXME: This has no effect!
189 }
190 #endif /* !__MINGW32__ */
191
192 return true;
193 }
194
PromiseKeptExec(ARG_UNUSED const Attributes * a,ARG_UNUSED const Promise * pp)195 static bool PromiseKeptExec(ARG_UNUSED const Attributes *a, ARG_UNUSED const Promise *pp)
196 {
197 return false;
198 }
199
GetLockNameExec(const Attributes * a,const Promise * pp)200 static char *GetLockNameExec(const Attributes *a, const Promise *pp)
201 {
202 return BuildCommandLine(a, pp);
203 }
204
205 /*****************************************************************************/
206
RepairExec(EvalContext * ctx,const Attributes * a,const Promise * pp,PromiseResult * result)207 static ActionResult RepairExec(EvalContext *ctx, const Attributes *a,
208 const Promise *pp, PromiseResult *result)
209 {
210 assert(a != NULL);
211 assert(pp != NULL);
212 char eventname[CF_BUFSIZE];
213 char cmdline[CF_BUFSIZE];
214 char comm[20];
215 int outsourced, count = 0;
216 #if !defined(__MINGW32__)
217 mode_t maskval = 0;
218 #endif
219 FILE *pfp;
220 char cmdOutBuf[CF_BUFSIZE];
221 int cmdOutBufPos = 0;
222 int lineOutLen;
223 char module_context[CF_BUFSIZE];
224
225 module_context[0] = '\0';
226
227 if (IsAbsoluteFileName(CommandArg0(pp->promiser)) || a->contain.shelltype == SHELL_TYPE_NONE)
228 {
229 if (!IsExecutable(CommandArg0(pp->promiser)))
230 {
231 cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a, "'%s' promises to be executable but isn't", pp->promiser);
232 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
233
234 if (strchr(pp->promiser, ' '))
235 {
236 Log(LOG_LEVEL_VERBOSE, "Paths with spaces must be inside escaped quoutes (e.g. \\\"%s\\\")", pp->promiser);
237 }
238
239 return ACTION_RESULT_FAILED;
240 }
241 else
242 {
243 Log(LOG_LEVEL_VERBOSE, "Promiser string contains a valid executable '%s' - ok", CommandArg0(pp->promiser));
244 }
245 }
246
247 char timeout_str[CF_BUFSIZE];
248 if (a->contain.timeout == CF_NOINT)
249 {
250 snprintf(timeout_str, CF_BUFSIZE, "no timeout");
251 }
252 else
253 {
254 snprintf(timeout_str, CF_BUFSIZE, "timeout=%ds", a->contain.timeout);
255 }
256
257 char owner_str[CF_BUFSIZE] = "";
258 if (a->contain.owner != -1)
259 {
260 snprintf(owner_str, CF_BUFSIZE, ",uid=%ju", (uintmax_t)a->contain.owner);
261 }
262
263 char group_str[CF_BUFSIZE] = "";
264 if (a->contain.group != -1)
265 {
266 snprintf(group_str, CF_BUFSIZE, ",gid=%ju", (uintmax_t)a->contain.group);
267 }
268
269 char* temp = BuildCommandLine(a, pp);
270 snprintf(cmdline, CF_BUFSIZE, "%s", temp); // TODO: remove CF_BUFSIZE limitation
271 free(temp);
272
273 const LogLevel info_or_verbose = a->inform ? LOG_LEVEL_INFO : LOG_LEVEL_VERBOSE;
274 Log(info_or_verbose, "Executing '%s%s%s' ... '%s'", timeout_str, owner_str, group_str, cmdline);
275
276 BeginMeasure();
277
278 if (DONTDO && (!a->contain.preview))
279 {
280 cfPS(ctx, LOG_LEVEL_WARNING, PROMISE_RESULT_WARN, pp, a, "Would execute script '%s'", cmdline);
281 *result = PromiseResultUpdate(*result, PROMISE_RESULT_WARN);
282 return ACTION_RESULT_OK;
283 }
284
285 if (a->transaction.action != cfa_fix)
286 {
287 cfPS(ctx, LOG_LEVEL_WARNING, PROMISE_RESULT_WARN, pp, a, "Command '%s' needs to be executed, but only warning was promised", cmdline);
288 *result = PromiseResultUpdate(*result, PROMISE_RESULT_WARN);
289 return ACTION_RESULT_OK;
290 }
291
292 CommandPrefix(cmdline, comm);
293
294 if (a->transaction.background)
295 {
296 #ifdef __MINGW32__
297 outsourced = true;
298 #else
299 Log(LOG_LEVEL_VERBOSE, "Backgrounding job '%s'", cmdline);
300 outsourced = fork();
301 #endif
302 }
303 else
304 {
305 outsourced = false;
306 }
307
308 if (outsourced || (!a->transaction.background)) // work done here: either by child or non-background parent
309 {
310 if (a->contain.timeout != CF_NOINT)
311 {
312 SetTimeOut(a->contain.timeout);
313 }
314
315 #ifndef __MINGW32__
316 Log(LOG_LEVEL_VERBOSE, "Setting umask to %jo", (uintmax_t)a->contain.umask);
317 maskval = umask(a->contain.umask);
318
319 if (a->contain.umask == 0)
320 {
321 Log(LOG_LEVEL_VERBOSE, "Programming '%s' running with umask 0! Use umask= to set", cmdline);
322 }
323 #endif /* !__MINGW32__ */
324
325 const char *open_mode = a->module ? "rt" : "r";
326 if (a->contain.shelltype == SHELL_TYPE_POWERSHELL)
327 {
328 #ifdef __MINGW32__
329 pfp =
330 cf_popen_powershell_setuid(cmdline, open_mode, a->contain.owner, a->contain.group, a->contain.chdir, a->contain.chroot,
331 a->transaction.background);
332 #else // !__MINGW32__
333 Log(LOG_LEVEL_ERR, "Powershell is only supported on Windows");
334 return ACTION_RESULT_FAILED;
335 #endif // !__MINGW32__
336 }
337 else if (a->contain.shelltype == SHELL_TYPE_USE)
338 {
339 pfp =
340 cf_popen_shsetuid(cmdline, open_mode, a->contain.owner, a->contain.group, a->contain.chdir, a->contain.chroot,
341 a->transaction.background);
342 }
343 else
344 {
345 pfp =
346 cf_popensetuid(cmdline, open_mode, a->contain.owner, a->contain.group, a->contain.chdir, a->contain.chroot,
347 a->transaction.background);
348 }
349
350 if (pfp == NULL)
351 {
352 Log(LOG_LEVEL_ERR, "Couldn't open pipe to command '%s'. (cf_popen: %s)", cmdline, GetErrorStr());
353 return ACTION_RESULT_FAILED;
354 }
355
356 StringSet *module_tags = StringSetNew();
357 long persistence = 0;
358
359 size_t line_size = CF_BUFSIZE;
360 char *line = xmalloc(line_size);
361
362 for (;;)
363 {
364 ssize_t res = CfReadLine(&line, &line_size, pfp);
365 if (res == -1)
366 {
367 if (!feof(pfp))
368 {
369 Log(LOG_LEVEL_ERR, "Unable to read output from command '%s'. (fread: %s)", cmdline, GetErrorStr());
370 cf_pclose(pfp);
371 free(line);
372 return ACTION_RESULT_FAILED;
373 }
374 else
375 {
376 break;
377 }
378 }
379
380 if (strstr(line, "cfengine-die"))
381 {
382 break;
383 }
384
385 if (a->contain.preview)
386 {
387 PreviewProtocolLine(line, cmdline);
388 }
389
390 if (a->module)
391 {
392 ModuleProtocol(ctx, cmdline, line, !a->contain.nooutput, module_context, sizeof(module_context), module_tags, &persistence);
393 }
394
395 if (!a->contain.nooutput && !EmptyString(line))
396 {
397 lineOutLen = strlen(comm) + strlen(line) + 12;
398
399 // if buffer is to small for this line, output it directly
400 if (lineOutLen > sizeof(cmdOutBuf))
401 {
402 Log(LOG_LEVEL_NOTICE, "Q: '%s': %s", comm, line);
403 }
404 else
405 {
406 if (cmdOutBufPos + lineOutLen > sizeof(cmdOutBuf))
407 {
408 Log(LOG_LEVEL_NOTICE, "%s", cmdOutBuf);
409 cmdOutBufPos = 0;
410 }
411 snprintf(cmdOutBuf + cmdOutBufPos,
412 sizeof(cmdOutBuf) - cmdOutBufPos,
413 "Q: \"...%s\": %s\n", comm, line);
414 cmdOutBufPos += (lineOutLen - 1);
415 }
416 count++;
417 }
418 }
419
420 StringSetDestroy(module_tags);
421 free(line);
422
423 #ifdef __MINGW32__
424 if (outsourced) // only get return value if we waited for command execution
425 {
426 cf_pclose_nowait(pfp);
427 }
428 else
429 #endif /* __MINGW32__ */
430 {
431 int ret = cf_pclose(pfp);
432
433 if (ret == -1)
434 {
435 cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, a, "Finished script '%s' - failed (abnormal termination)", pp->promiser);
436 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
437 }
438 else
439 {
440 VerifyCommandRetcode(ctx, ret, a, pp, result);
441 }
442 }
443 }
444
445 if (count)
446 {
447 if (cmdOutBufPos)
448 {
449 Log(LOG_LEVEL_NOTICE, "%s", cmdOutBuf);
450 }
451
452 Log(LOG_LEVEL_INFO, "Last %d quoted lines were generated by promiser '%s'", count, cmdline);
453 }
454
455 if (a->contain.timeout != CF_NOINT)
456 {
457 alarm(0);
458 signal(SIGALRM, SIG_DFL);
459 }
460
461 Log(info_or_verbose, "Completed execution of '%s'", cmdline);
462
463 #ifndef __MINGW32__
464 umask(maskval);
465 #endif
466
467 snprintf(eventname, CF_BUFSIZE - 1, "Exec(%s)", cmdline);
468
469 #ifndef __MINGW32__
470 if ((a->transaction.background) && outsourced)
471 {
472 Log(LOG_LEVEL_VERBOSE, "Backgrounded command '%s' is done - exiting", cmdline);
473
474 /* exit() OK since this is a forked process and no functions are
475 registered for cleanup */
476 exit(EXIT_SUCCESS);
477 }
478 #endif /* !__MINGW32__ */
479
480 return ACTION_RESULT_OK;
481 }
482
483 /*************************************************************/
484 /* Level */
485 /*************************************************************/
486
PreviewProtocolLine(char * line,char * comm)487 void PreviewProtocolLine(char *line, char *comm)
488 {
489 int i;
490 char *message = line;
491
492 /*
493 * Table matching cfoutputlevel enums to log prefixes.
494 */
495
496 char *prefixes[] =
497 {
498 ":silent:",
499 ":inform:",
500 ":verbose:",
501 ":editverbose:",
502 ":error:",
503 ":logonly:",
504 };
505
506 int precount = sizeof(prefixes) / sizeof(char *);
507
508 if (line[0] == ':')
509 {
510 /*
511 * Line begins with colon - see if it matches a log prefix.
512 */
513
514 for (i = 0; i < precount; i++)
515 {
516 int prelen = 0;
517
518 prelen = strlen(prefixes[i]);
519
520 if (strncmp(line, prefixes[i], prelen) == 0)
521 {
522 /*
523 * Found log prefix - set logging level, and remove the
524 * prefix from the log message.
525 */
526
527 message += prelen;
528 break;
529 }
530 }
531 }
532
533 Log(LOG_LEVEL_VERBOSE, "'%s', preview of '%s'", message, comm);
534 }
535