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