1 /*
2   Copyright 2021 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_files_utils.h>
26 
27 #include <actuator.h>
28 #include <attributes.h>
29 #include <dir.h>
30 #include <files_names.h>
31 #include <files_links.h>
32 #include <files_copy.h>
33 #include <files_properties.h>
34 #include <locks.h>
35 #include <instrumentation.h>
36 #include <match_scope.h>
37 #include <files_interfaces.h>
38 #include <promises.h>
39 #include <files_operators.h>
40 #include <item_lib.h>
41 #include <client_code.h>
42 #include <hash.h>
43 #include <files_repository.h>
44 #include <files_select.h>
45 #include <files_changes.h>
46 #include <expand.h>
47 #include <conversion.h>
48 #include <pipes.h>
49 #include <verify_acl.h>
50 #include <eval_context.h>
51 #include <vars.h>
52 #include <exec_tools.h>
53 #include <comparray.h>
54 #include <string_lib.h>
55 #include <files_lib.h>
56 #include <rlist.h>
57 #include <policy.h>
58 #include <scope.h>
59 #include <misc_lib.h>
60 #include <abstract_dir.h>
61 #include <verify_files_hashes.h>
62 #include <audit.h>
63 #include <retcode.h>
64 #include <cf-agent-enterprise-stubs.h>
65 #include <conn_cache.h>
66 #include <stat_cache.h>                      /* remote_stat,StatCacheLookup */
67 #include <known_dirs.h>
68 #include <changes_chroot.h>     /* PrepareChangesChroot(), RecordFileChangedInChroot() */
69 
70 #include <cf-windows-functions.h>
71 
72 #define CF_RECURSION_LIMIT 100
73 
74 static const Rlist *AUTO_DEFINE_LIST = NULL; /* GLOBAL_P */
75 
76 static Item *VSETXIDLIST = NULL;
77 
78 const Rlist *SINGLE_COPY_LIST = NULL; /* GLOBAL_P */
79 StringSet *SINGLE_COPY_CACHE = NULL; /* GLOBAL_X */
80 
81 static bool TransformFile(EvalContext *ctx, char *file, const Attributes *attr, const Promise *pp, PromiseResult *result);
82 static PromiseResult VerifyName(EvalContext *ctx, char *path, const struct stat *sb, const Attributes *attr, const Promise *pp);
83 static PromiseResult VerifyDelete(EvalContext *ctx,
84                                   const char *path, const struct stat *sb,
85                                   const Attributes *attr, const Promise *pp);
86 static PromiseResult VerifyCopy(EvalContext *ctx, const char *source, char *destination, const Attributes *attr, const Promise *pp,
87                                 CompressedArray **inode_cache, AgentConnection *conn);
88 static PromiseResult TouchFile(EvalContext *ctx, char *path, const Attributes *attr, const Promise *pp);
89 static PromiseResult VerifyFileAttributes(EvalContext *ctx, const char *file, const struct stat *dstat, const Attributes *attr, const Promise *pp);
90 static bool PushDirState(EvalContext *ctx, const Promise *pp, const Attributes *attr, char *name, const struct stat *sb, PromiseResult *result);
91 static bool PopDirState(EvalContext *ctx, const Promise *pp, const Attributes *attr, int goback, char *name, const struct stat *sb,
92                         DirectoryRecursion r, PromiseResult *result);
93 static bool CheckLinkSecurity(const struct stat *sb, const char *name);
94 static bool CompareForFileCopy(char *sourcefile, char *destfile, const struct stat *ssb, const struct stat *dsb, const FileCopy *fc, AgentConnection *conn);
95 static void FileAutoDefine(EvalContext *ctx, char *destfile);
96 static void TruncateFile(const char *name);
97 static void RegisterAHardLink(int i, const char *value, EvalContext *ctx, const Promise *pp,
98                               const Attributes *attr, PromiseResult *result,
99                               CompressedArray **inode_cache);
100 static PromiseResult VerifyCopiedFileAttributes(EvalContext *ctx, const char *src, const char *dest, const struct stat *sstat, const struct stat *dstat, const Attributes *attr, const Promise *pp);
101 static int cf_stat(const char *file, struct stat *buf, const FileCopy *fc, AgentConnection *conn);
102 #ifndef __MINGW32__
103 static int cf_readlink(EvalContext *ctx, const char *sourcefile, char *linkbuf, size_t buffsize, const Attributes *attr, const Promise *pp, AgentConnection *conn, PromiseResult *result);
104 #endif
105 static bool SkipDirLinks(EvalContext *ctx, const char *path, const char *lastnode, DirectoryRecursion r);
106 static bool DeviceBoundary(const struct stat *sb, dev_t rootdevice);
107 static PromiseResult LinkCopy(EvalContext *ctx, char *sourcefile, char *destfile, const struct stat *sb, const Attributes *attr,
108                               const Promise *pp, CompressedArray **inode_cache, AgentConnection *conn);
109 
110 #ifndef __MINGW32__
111 static void LoadSetxid(void);
112 static void SaveSetxid(bool modified);
113 static PromiseResult VerifySetUidGid(EvalContext *ctx, const char *file, const struct stat *dstat, mode_t newperm, const Promise *pp, const Attributes *attr);
114 #endif
115 #ifdef __APPLE__
116 static int VerifyFinderType(EvalContext *ctx, const char *file, const Attributes *a, const Promise *pp, PromiseResult *result);
117 #endif
118 static void VerifyFileChanges(EvalContext *ctx, const char *file, const struct stat *sb,
119                               const Attributes *attr, const Promise *pp, PromiseResult *result);
120 static PromiseResult VerifyFileIntegrity(EvalContext *ctx, const char *file, const Attributes *attr, const Promise *pp);
121 
122 extern Attributes GetExpandedAttributes(EvalContext *ctx, const Promise *pp, const Attributes *attr);
123 extern void ClearExpandedAttributes(Attributes *a);
124 
SetFileAutoDefineList(const Rlist * auto_define_list)125 void SetFileAutoDefineList(const Rlist *auto_define_list)
126 {
127     AUTO_DEFINE_LIST = auto_define_list;
128 }
129 
VerifyFileLeaf(EvalContext * ctx,char * path,const struct stat * sb,ARG_UNUSED const Attributes * attr,const Promise * pp,PromiseResult * result)130 void VerifyFileLeaf(EvalContext *ctx, char *path, const struct stat *sb, ARG_UNUSED const Attributes *attr, const Promise *pp, PromiseResult *result)
131 {
132     // FIXME: This function completely ignores it's attr argument
133     assert(attr != NULL);
134 
135 /* Here we can assume that we are in the parent directory of the leaf */
136 
137     Log(LOG_LEVEL_VERBOSE, "Handling file existence constraints on '%s'", path);
138 
139     /* Update this.promiser again, and overwrite common attributes (classes, action) accordingly */
140 
141     EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_THIS, "promiser", path, CF_DATA_TYPE_STRING, "source=promise");        // Parameters may only be scalars
142     Attributes org_attr = GetFilesAttributes(ctx, pp);
143     Attributes new_attr = GetExpandedAttributes(ctx, pp, &org_attr);
144 
145     if (new_attr.transformer != NULL)
146     {
147         if (!TransformFile(ctx, path, &new_attr, pp, result))
148         {
149             /* NOP, changes and/or failures are recorded by TransformFile() */
150         }
151     }
152     else
153     {
154         if (new_attr.haverename)
155         {
156             *result = PromiseResultUpdate(*result, VerifyName(ctx, path, sb, &new_attr, pp));
157         }
158 
159         if (new_attr.havedelete)
160         {
161             *result = PromiseResultUpdate(*result, VerifyDelete(ctx, path, sb, &new_attr, pp));
162         }
163 
164         if (new_attr.touch)
165         {
166             *result = PromiseResultUpdate(*result, TouchFile(ctx, path, &new_attr, pp)); // intrinsically non-convergent op
167         }
168     }
169 
170     if (new_attr.haveperms || new_attr.havechange || new_attr.acl.acl_entries)
171     {
172         if (S_ISDIR(sb->st_mode) && new_attr.recursion.depth && !new_attr.recursion.include_basedir &&
173             (strcmp(path, pp->promiser) == 0))
174         {
175             Log(LOG_LEVEL_VERBOSE, "Promise to skip base directory '%s'", path);
176         }
177         else
178         {
179             *result = PromiseResultUpdate(*result, VerifyFileAttributes(ctx, path, sb, &new_attr, pp));
180         }
181     }
182     ClearExpandedAttributes(&new_attr);
183 }
184 
185 /* Checks whether item matches a list of wildcards */
MatchRlistItem(EvalContext * ctx,const Rlist * listofregex,const char * teststring)186 static bool MatchRlistItem(EvalContext *ctx, const Rlist *listofregex, const char *teststring)
187 {
188     for (const Rlist *rp = listofregex; rp != NULL; rp = rp->next)
189     {
190         /* Avoid using regex if possible, due to memory leak */
191 
192         if (strcmp(teststring, RlistScalarValue(rp)) == 0)
193         {
194             return true;
195         }
196 
197         /* Make it commutative */
198 
199         if (FullTextMatch(ctx, RlistScalarValue(rp), teststring))
200         {
201             return true;
202         }
203     }
204 
205     return false;
206 }
207 
208 /* (conn == NULL) then copy is from localhost. */
CfCopyFile(EvalContext * ctx,char * sourcefile,char * destfile,const struct stat * ssb,const Attributes * a,const Promise * pp,CompressedArray ** inode_cache,AgentConnection * conn)209 static PromiseResult CfCopyFile(EvalContext *ctx, char *sourcefile,
210                                 char *destfile, const struct stat *ssb,
211                                 const Attributes *a, const Promise *pp,
212                                 CompressedArray **inode_cache,
213                                 AgentConnection *conn)
214 {
215     assert(a != NULL);
216     const char *lastnode;
217     struct stat dsb;
218     const mode_t srcmode = ssb->st_mode;
219 
220     const char *server;
221     if (conn != NULL)
222     {
223         server = conn->this_server;
224     }
225     else
226     {
227         server = "localhost";
228     }
229 
230 #ifdef __MINGW32__
231     if (a->copy.copy_links != NULL)
232     {
233         Log(LOG_LEVEL_VERBOSE,
234             "copy_from.copylink_patterns is ignored on Windows "
235             "(source files cannot be symbolic links)");
236     }
237 #endif /* __MINGW32__ */
238 
239     Attributes attr = *a; // TODO: Avoid this copy
240     attr.link.when_no_file = cfa_force;
241 
242     if (strcmp(sourcefile, destfile) == 0 &&
243         strcmp(server, "localhost") == 0)
244     {
245         RecordNoChange(ctx, pp, &attr,
246                        "File copy promise loop: file/dir '%s' is its own source",
247                        sourcefile);
248         return PROMISE_RESULT_NOOP;
249     }
250 
251     if (attr.haveselect && !SelectLeaf(ctx, sourcefile, ssb, &(attr.select)))
252     {
253         RecordNoChange(ctx, pp, &attr, "Skipping non-selected file '%s'", sourcefile);
254         return PROMISE_RESULT_NOOP;
255     }
256 
257     if ((SINGLE_COPY_CACHE != NULL) && StringSetContains(SINGLE_COPY_CACHE, destfile))
258     {
259         RecordNoChange(ctx, pp, &attr, "Skipping single-copied file '%s'", destfile);
260         return PROMISE_RESULT_NOOP;
261     }
262 
263     if (attr.copy.link_type != FILE_LINK_TYPE_NONE)
264     {
265         lastnode = ReadLastNode(sourcefile);
266 
267         if (MatchRlistItem(ctx, attr.copy.link_instead, lastnode))
268         {
269             if (MatchRlistItem(ctx, attr.copy.copy_links, lastnode))
270             {
271                 RecordNoChange(ctx, pp, &attr,
272                                "File %s matches both copylink_patterns and linkcopy_patterns"
273                                " - promise loop (skipping)!",
274                                sourcefile);
275                 return PROMISE_RESULT_NOOP;
276             }
277             else
278             {
279                 Log(LOG_LEVEL_VERBOSE, "Copy item '%s' marked for linking",
280                     sourcefile);
281 #ifdef __MINGW32__
282                 Log(LOG_LEVEL_VERBOSE,
283                     "Links are not yet supported on Windows"
284                     " - copying '%s' instead", sourcefile);
285 #else
286                 return LinkCopy(ctx, sourcefile, destfile, ssb,
287                                 &attr, pp, inode_cache, conn);
288 #endif
289             }
290         }
291     }
292 
293     const char *changes_destfile = destfile;
294     if (ChrootChanges())
295     {
296         changes_destfile = ToChangesChroot(destfile);
297     }
298 
299     PromiseResult result = PROMISE_RESULT_NOOP;
300     bool destfile_exists = (lstat(changes_destfile, &dsb) != -1);
301     if (destfile_exists)
302     {
303         if ((S_ISLNK(dsb.st_mode) && attr.copy.link_type == FILE_LINK_TYPE_NONE)
304             || (S_ISLNK(dsb.st_mode) && !S_ISLNK(srcmode)))
305         {
306             if (!S_ISLNK(srcmode) &&
307                 attr.copy.type_check &&
308                 attr.copy.link_type != FILE_LINK_TYPE_NONE)
309             {
310                 RecordFailure(ctx, pp, &attr,
311                               "File image exists but destination type is silly "
312                               "(file/dir/link doesn't match)");
313                 PromiseRef(LOG_LEVEL_ERR, pp);
314                 return PROMISE_RESULT_FAIL;
315             }
316 
317             if (!MakingChanges(ctx, pp, &attr, NULL,
318                               "remove old symbolic link '%s' to make way for copy",
319                                destfile))
320             {
321                 return PROMISE_RESULT_WARN;
322             }
323             else
324             {
325                 if (unlink(changes_destfile) == -1)
326                 {
327                     RecordFailure(ctx, pp, &attr,
328                                   "Couldn't remove link '%s'. (unlink: %s)",
329                                   destfile, GetErrorStr());
330                     return PROMISE_RESULT_FAIL;
331                 }
332                 RecordChange(ctx, pp, &attr,
333                              "Removing old symbolic link '%s' to make way for copy",
334                              destfile);
335                 result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
336                 destfile_exists = false;
337             }
338         }
339     }
340     else
341     {
342         bool dir_created = false;
343         if (!MakeParentDirectoryForPromise(ctx, pp, &attr, &result,
344                                            destfile, true, &dir_created))
345         {
346             return result;
347         }
348         if (dir_created)
349         {
350             RecordChange(ctx, pp, &attr, "Created parent directory for '%s'", destfile);
351             result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
352         }
353     }
354 
355     if (attr.copy.min_size != (size_t) CF_NOINT)
356     {
357         if (((size_t) ssb->st_size < attr.copy.min_size)
358             || ((size_t) ssb->st_size > attr.copy.max_size))
359         {
360             RecordNoChange(ctx, pp, &attr,
361                            "Source file '%s' size is not in the permitted safety range, skipping",
362                            sourcefile);
363             return result;
364         }
365     }
366 
367     if (!destfile_exists)
368     {
369         if (S_ISREG(srcmode) ||
370             (S_ISLNK(srcmode) && attr.copy.link_type == FILE_LINK_TYPE_NONE))
371         {
372             if (!MakingChanges(ctx, pp, &attr, &result, "copy '%s' to '%s'", destfile, sourcefile))
373             {
374                 return result;
375             }
376 
377             Log(LOG_LEVEL_VERBOSE,
378                 "'%s' wasn't at destination, copying from '%s:%s'",
379                 destfile, server, sourcefile);
380 
381             if (S_ISLNK(srcmode) && attr.copy.link_type != FILE_LINK_TYPE_NONE)
382             {
383                 Log(LOG_LEVEL_VERBOSE, "'%s' is a symbolic link", sourcefile);
384                 result = PromiseResultUpdate(result,
385                                              LinkCopy(ctx, sourcefile, destfile, ssb,
386                                                       &attr, pp, inode_cache, conn));
387             }
388             else if (CopyRegularFile(ctx, sourcefile, destfile, ssb, &attr,
389                                      pp, inode_cache, conn, &result))
390             {
391                 if (stat(ToChangesPath(destfile), &dsb) == -1)
392                 {
393                     Log(LOG_LEVEL_ERR,
394                         "Can't stat destination file '%s'. (stat: %s)",
395                         destfile, GetErrorStr());
396                 }
397                 else
398                 {
399                     result = PromiseResultUpdate(result,
400                                                  VerifyCopiedFileAttributes(ctx, sourcefile,
401                                                                             destfile, ssb,
402                                                                             &dsb, &attr, pp));
403                 }
404 
405                 RecordChange(ctx, pp, &attr, "Updated file '%s' from '%s:%s'",
406                              destfile, server, sourcefile);
407                 result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
408 
409                 if (SINGLE_COPY_LIST)
410                 {
411                     StringSetAdd(SINGLE_COPY_CACHE, xstrdup(destfile));
412                 }
413 
414                 if (MatchRlistItem(ctx, AUTO_DEFINE_LIST, destfile))
415                 {
416                     FileAutoDefine(ctx, destfile);
417                 }
418             }
419             else
420             {
421                 RecordFailure(ctx, pp, &attr, "Copy from '%s:%s' failed",
422                               server, sourcefile);
423                 result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
424             }
425 
426             return result;
427         }
428 
429         if (S_ISFIFO(srcmode))
430         {
431 #ifdef HAVE_MKFIFO
432             if (!MakingChanges(ctx, pp, &attr, &result, "create FIFO '%s'", destfile))
433             {
434                 return result;
435             }
436             else if (mkfifo(ToChangesPath(destfile), srcmode) != 0)
437             {
438                 RecordFailure(ctx, pp, &attr, "Cannot create fifo '%s'. (mkfifo: %s)",
439                               destfile, GetErrorStr());
440                 result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
441                 return result;
442             }
443 
444             RecordChange(ctx, pp, &attr, "Created fifo '%s'", destfile);
445             result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
446 #else
447             RecordWarning(ctx, pp, &attr, "Should create FIFO '%s', but FIFO creation not supported",
448                           destfile);
449             result = PromiseResultUpdate(result, PROMISE_RESULT_WARN);
450             return result;
451 #endif
452         }
453         else
454         {
455 #ifndef __MINGW32__                   // only regular files on windows
456             if (S_ISBLK(srcmode) || S_ISCHR(srcmode) || S_ISSOCK(srcmode))
457             {
458                 if (!MakingChanges(ctx, pp, &attr, &result, "make special file/device '%s'", destfile))
459                 {
460                     return result;
461                 }
462                 else if (mknod(ToChangesPath(destfile),
463                                srcmode, ssb->st_rdev))
464                 {
465                     RecordFailure(ctx, pp, &attr, "Cannot create special file '%s'. (mknod: %s)",
466                                   destfile, GetErrorStr());
467                     result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
468                     return result;
469                 }
470                 RecordChange(ctx, pp, &attr, "Created special file/device '%s'.", destfile);
471                 result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
472             }
473 #endif /* !__MINGW32__ */
474         }
475 
476         if ((S_ISLNK(srcmode)) && (attr.copy.link_type != FILE_LINK_TYPE_NONE))
477         {
478             result = PromiseResultUpdate(result,
479                                          LinkCopy(ctx, sourcefile, destfile, ssb,
480                                                   &attr, pp, inode_cache, conn));
481         }
482     }
483     else
484     {
485         Log(LOG_LEVEL_VERBOSE, "Destination file '%s' already exists",
486             destfile);
487 
488         if (attr.copy.compare == FILE_COMPARATOR_EXISTS)
489         {
490             RecordNoChange(ctx, pp, &attr, "Existence only is promised, no copying required");
491             return result;
492         }
493 
494         bool should_copy = (attr.copy.force_update ||
495                             CompareForFileCopy(sourcefile, destfile, ssb,
496                                                &dsb, &attr.copy, conn));
497 
498         if (attr.copy.type_check &&
499             attr.copy.link_type != FILE_LINK_TYPE_NONE)
500         {
501             // Mask mode with S_IFMT to extract and compare file types
502             if ((dsb.st_mode & S_IFMT) != (srcmode & S_IFMT))
503             {
504                 RecordFailure(ctx, pp, &attr,
505                               "Promised file copy %s exists but type mismatch with source '%s'",
506                               destfile, sourcefile);
507                 result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
508                 return result;
509             }
510         }
511 
512         if (should_copy || S_ISLNK(srcmode))                     /* Always check links */
513         {
514             if (S_ISREG(srcmode) ||
515                 attr.copy.link_type == FILE_LINK_TYPE_NONE)
516             {
517                 if (!MakingChanges(ctx, pp, &attr, &result, "update file '%s' from '%s' on '%s'",
518                                    destfile, sourcefile, server))
519                 {
520                     return result;
521                 }
522 
523                 if (MatchRlistItem(ctx, AUTO_DEFINE_LIST, destfile))
524                 {
525                     FileAutoDefine(ctx, destfile);
526                 }
527 
528                 if (CopyRegularFile(ctx, sourcefile, destfile, ssb, &attr,
529                                     pp, inode_cache, conn, &result))
530                 {
531                     if (stat(ToChangesPath(destfile), &dsb) == -1)
532                     {
533                         RecordInterruption(ctx, pp, &attr,
534                                            "Can't stat destination '%s'. (stat: %s)",
535                                            destfile, GetErrorStr());
536                         result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
537                     }
538                     else
539                     {
540                         RecordChange(ctx, pp, &attr, "Updated '%s' from source '%s' on '%s'",
541                                      destfile, sourcefile, server);
542                         result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
543                         result = PromiseResultUpdate(result,
544                                                      VerifyCopiedFileAttributes(ctx, sourcefile,
545                                                                                 destfile, ssb,
546                                                                                 &dsb, &attr, pp));
547                     }
548 
549                     if (RlistIsInListOfRegex(SINGLE_COPY_LIST, destfile))
550                     {
551                         StringSetAdd(SINGLE_COPY_CACHE, xstrdup(destfile));
552                     }
553                 }
554                 else
555                 {
556                     RecordFailure(ctx, pp, &attr, "Was not able to copy '%s' on '%s' to '%s'",
557                                   sourcefile, server, destfile);
558                     result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
559                 }
560 
561                 return result;
562             }
563             else if (S_ISLNK(srcmode))
564             {
565                 result = PromiseResultUpdate(result,
566                                              LinkCopy(ctx, sourcefile, destfile, ssb,
567                                                       &attr, pp, inode_cache, conn));
568             }
569         }
570         else
571         {
572             result = PromiseResultUpdate(result,
573                                          VerifyCopiedFileAttributes(ctx, sourcefile, destfile,
574                                                                     ssb, &dsb, &attr, pp));
575 
576             /* Now we have to check for single copy, even though nothing was
577                copied otherwise we can get oscillations between multipe
578                versions if type is based on a checksum */
579 
580             if (RlistIsInListOfRegex(SINGLE_COPY_LIST, destfile))
581             {
582                 StringSetAdd(SINGLE_COPY_CACHE, xstrdup(destfile));
583             }
584 
585             RecordNoChange(ctx, pp, &attr, "File '%s' is an up to date copy of source",
586                            destfile);
587         }
588     }
589 
590     return result;
591 }
592 
PurgeLocalFiles(EvalContext * ctx,Item * filelist,const char * localdir,const Attributes * attr,const Promise * pp,AgentConnection * conn)593 static PromiseResult PurgeLocalFiles(EvalContext *ctx, Item *filelist, const char *localdir, const Attributes *attr,
594                                      const Promise *pp, AgentConnection *conn)
595 {
596     assert(attr != NULL);
597     Dir *dirh;
598     struct stat sb;
599     const struct dirent *dirp;
600     char filename[CF_BUFSIZE] = { 0 };
601 
602     if (strlen(localdir) < 2)
603     {
604         RecordFailure(ctx, pp, attr, "Purge of '%s' denied - too dangerous!", localdir);
605         return PROMISE_RESULT_FAIL;
606     }
607 
608     /* If we purge with no authentication we wipe out EVERYTHING ! */
609 
610     if (conn && (!conn->authenticated))
611     {
612         RecordDenial(ctx, pp, attr,
613                      "Not purge local files '%s' - no authenticated contact with a source",
614                      localdir);
615         return PROMISE_RESULT_DENIED;
616     }
617 
618     if (!attr->havedepthsearch)
619     {
620         RecordNoChange(ctx, pp, attr,
621                        "No depth search when copying '%s' so purging does not apply",
622                        localdir);
623         return PROMISE_RESULT_NOOP;
624     }
625 
626 /* chdir to minimize the risk of race exploits during copy (which is inherently dangerous) */
627 
628     const char *changes_localdir = localdir;
629     if (ChrootChanges())
630     {
631         changes_localdir = ToChangesChroot(localdir);
632     }
633 
634     if (safe_chdir(changes_localdir) == -1)
635     {
636         RecordFailure(ctx, pp, attr,
637                       "Can't chdir to local directory '%s'. (chdir: %s)",
638                       localdir, GetErrorStr());
639         return PROMISE_RESULT_FAIL;
640     }
641 
642     if ((dirh = DirOpen(".")) == NULL)
643     {
644         RecordFailure(ctx, pp, attr,
645                       "Can't open local directory '%s'. (opendir: %s)",
646                       localdir, GetErrorStr());
647         return PROMISE_RESULT_FAIL;
648     }
649 
650     PromiseResult result = PROMISE_RESULT_NOOP;
651     for (dirp = DirRead(dirh); dirp != NULL; dirp = DirRead(dirh))
652     {
653         if (!ConsiderLocalFile(dirp->d_name, changes_localdir))
654         {
655             Log(LOG_LEVEL_VERBOSE, "Skipping '%s'", dirp->d_name);
656             continue;
657         }
658 
659         if (!IsItemIn(filelist, dirp->d_name))
660         {
661             strncpy(filename, localdir, CF_BUFSIZE - 2);
662 
663             AddSlash(filename);
664 
665             if (strlcat(filename, dirp->d_name, CF_BUFSIZE) >= CF_BUFSIZE)
666             {
667                 RecordFailure(ctx, pp, attr,
668                               "Path name '%s%s' is too long in PurgeLocalFiles",
669                               filename, dirp->d_name);
670                 continue;
671             }
672 
673             const char *changes_filename = filename;
674             if (ChrootChanges())
675             {
676                 changes_filename = ToChangesChroot(filename);
677             }
678             if (MakingChanges(ctx, pp, attr, &result,
679                               "purge '%s' from copy dest directory", filename))
680             {
681                 if (lstat(changes_filename, &sb) == -1)
682                 {
683                     RecordInterruption(ctx, pp, attr,
684                                        "Couldn't stat '%s' while purging. (lstat: %s)",
685                                        filename, GetErrorStr());
686                     result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
687                 }
688                 else if (S_ISDIR(sb.st_mode))
689                 {
690                     if (!DeleteDirectoryTree(changes_filename))
691                     {
692                         RecordFailure(ctx, pp, attr,
693                                       "Unable to purge directory tree '%s'", filename);
694                         result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
695                     }
696                     else if (rmdir(changes_filename) == -1)
697                     {
698                         if (errno != ENOENT)
699                         {
700                             RecordFailure(ctx, pp, attr,
701                                           "Unable to purge directory '%s'", filename);
702                             result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
703                         }
704                     }
705                     else
706                     {
707                         RecordChange(ctx, pp, attr,
708                                      "Purged directory '%s' in copy dest directory", filename);
709                         result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
710                     }
711                 }
712                 else if (unlink(changes_filename) == -1)
713                 {
714                     RecordFailure(ctx, pp, attr, "Couldn't delete '%s' while purging", filename);
715                     result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
716                 }
717                 else
718                 {
719                     RecordChange(ctx, pp, attr, "Purged '%s' copy dest directory", filename);
720                     result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
721                 }
722             }
723         }
724     }
725 
726     DirClose(dirh);
727 
728     return result;
729 }
730 
SourceSearchAndCopy(EvalContext * ctx,const char * from,char * to,int maxrecurse,const Attributes * attr,const Promise * pp,dev_t rootdevice,CompressedArray ** inode_cache,AgentConnection * conn)731 static PromiseResult SourceSearchAndCopy(EvalContext *ctx, const char *from, char *to, int maxrecurse, const Attributes *attr,
732                                          const Promise *pp, dev_t rootdevice, CompressedArray **inode_cache, AgentConnection *conn)
733 {
734     /* TODO overflow check all these str*cpy()s in here! */
735     Item *namecache = NULL;
736 
737     if (maxrecurse == 0)        /* reached depth limit */
738     {
739         RecordFailure(ctx, pp, attr, "Maximum recursion level reached at '%s'", from);
740         return PROMISE_RESULT_FAIL;
741     }
742 
743     if (strlen(from) == 0)      /* Check for root dir */
744     {
745         from = "/";
746     }
747 
748     /* Check that dest dir exists before starting */
749 
750     char newto[CF_BUFSIZE];
751     strlcpy(newto, to, sizeof(newto) - 10);
752     AddSlash(newto);
753     strcat(newto, "dummy");
754 
755     PromiseResult result = PROMISE_RESULT_NOOP;
756     struct stat tostat;
757 
758     bool dir_created = false;
759     if (!MakeParentDirectoryForPromise(ctx, pp, attr, &result,
760                                        newto, attr->move_obstructions, &dir_created))
761     {
762         return result;
763     }
764     if (dir_created)
765     {
766         RecordChange(ctx, pp, attr, "Created parent directory for '%s'", to);
767         result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
768     }
769     /* remove "/dummy" from 'newto' */
770     ChopLastNode(newto);
771 
772     /* XXX: really delete slash from 'to'??? */
773     DeleteSlash(to);
774 
775     const char *changes_newto = newto;
776     if (ChrootChanges())
777     {
778         changes_newto = ToChangesChroot(newto);
779     }
780 
781     /* Set aside symlinks */
782     if (lstat(changes_newto, &tostat) != 0)
783     {
784         RecordFailure(ctx, pp, attr, "Unable to stat newly created directory '%s'. (lstat: %s)",
785                       to, GetErrorStr());
786         result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
787         return result;
788     }
789 
790     if (S_ISLNK(tostat.st_mode))
791     {
792         char backup[CF_BUFSIZE];
793         mode_t mask;
794 
795         if (!attr->move_obstructions)
796         {
797             RecordFailure(ctx, pp, attr,
798                           "Path '%s' is a symlink. Unable to move it aside without move_obstructions is set",
799                           to);
800             result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
801             return result;
802         }
803 
804         strcpy(backup, changes_newto);
805         DeleteSlash(to);        /* FIXME: really delete slash from *to*??? */
806         strcat(backup, ".cf-moved");
807 
808         if (MakingChanges(ctx, pp, attr, &result, "backup '%s'", to))
809         {
810             if (rename(changes_newto, backup) == -1)
811             {
812                 Log(LOG_LEVEL_ERR, "Unable to backup old '%s'", to);
813                 unlink(to);
814             }
815             else
816             {
817                 RecordChange(ctx, pp, attr, "Backed up '%s'", to);
818                 result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
819             }
820         }
821 
822         if (MakingChanges(ctx, pp, attr, &result, "create directory '%s'", to))
823         {
824             mask = umask(0);
825             if (mkdir(changes_newto, DEFAULTMODE) == -1)
826             {
827                 RecordFailure(ctx, pp, attr,
828                               "Failed to make directory '%s'. (mkdir: %s)", to, GetErrorStr());
829                 umask(mask);
830                 result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
831                 return result;
832             }
833             else
834             {
835                 RecordChange(ctx, pp, attr, "Created directory for '%s'", to);
836                 result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
837             }
838             umask(mask);
839         }
840     }
841 
842     /* (conn == NULL) means copy is from localhost. */
843     const char *changes_from = from;
844     bool local_changed_from = false;
845     if ((conn == NULL) && ChrootChanges())
846     {
847         const char *chrooted_from = ToChangesChroot(from);
848         struct stat sb;
849         if (lstat(changes_from, &sb) != -1)
850         {
851             changes_from = chrooted_from;
852             local_changed_from = true;
853         }
854     }
855 
856     /* Send OPENDIR command. */
857     AbstractDir *dirh;
858     if ((dirh = AbstractDirOpen(changes_from, &(attr->copy), conn)) == NULL)
859     {
860         RecordInterruption(ctx, pp, attr, "Can't open directory '%s' for copying", from);
861         result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
862         return result;
863     }
864 
865     /* No backslashes over the network. */
866     const char sep = (conn != NULL) ? '/' : FILE_SEPARATOR;
867 
868     struct stat sb, dsb;
869     char newfrom[CF_BUFSIZE];
870     const struct dirent *dirp;
871     for (dirp = AbstractDirRead(dirh); dirp != NULL; dirp = AbstractDirRead(dirh))
872     {
873         /* This sends 1st STAT command. */
874         if (!ConsiderAbstractFile(dirp->d_name, from, &(attr->copy), conn))
875         {
876             if (conn != NULL &&
877                 conn->conn_info->status != CONNECTIONINFO_STATUS_ESTABLISHED)
878             {
879                 RecordInterruption(ctx, pp, attr,
880                                    "Connection error when checking '%s'", dirp->d_name);
881                 AbstractDirClose(dirh);
882                 result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
883                 return result;
884             }
885             else
886             {
887                 Log(LOG_LEVEL_VERBOSE, "Skipping '%s'", dirp->d_name);
888                 continue;
889             }
890         }
891 
892         if (attr->copy.purge)
893         {
894             /* Append this file to the list of files not to purge */
895             AppendItem(&namecache, dirp->d_name, NULL);
896         }
897 
898         /* Assemble pathnames. TODO check overflow. */
899         strlcpy(newfrom, from, sizeof(newfrom));
900         strlcpy(newto, to, sizeof(newto));
901 
902         if (!PathAppend(newfrom, sizeof(newfrom), dirp->d_name, sep))
903         {
904             RecordFailure(ctx, pp, attr, "Internal limit reached in SourceSearchAndCopy(),"
905                           " source path too long: '%s' + '%s'",
906                           newfrom, dirp->d_name);
907             result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
908             AbstractDirClose(dirh);
909             return result;
910         }
911 
912         const char *changes_newfrom = newfrom;
913         if (ChrootChanges() && local_changed_from)
914         {
915             changes_newfrom = ToChangesChroot(newfrom);
916         }
917 
918         /* This issues a 2nd STAT command, hopefully served from cache. */
919 
920         if ((attr->recursion.travlinks) || (attr->copy.link_type == FILE_LINK_TYPE_NONE))
921         {
922             /* No point in checking if there are untrusted symlinks here,
923                since this is from a trusted source, by definition */
924 
925             if (cf_stat(changes_newfrom, &sb, &(attr->copy), conn) == -1)
926             {
927                 Log(LOG_LEVEL_VERBOSE, "Can't stat '%s'. (cf_stat: %s)", newfrom, GetErrorStr());
928                 if (conn != NULL &&
929                     conn->conn_info->status != CONNECTIONINFO_STATUS_ESTABLISHED)
930                 {
931                     RecordInterruption(ctx, pp, attr, "Connection error when checking '%s'", newfrom);
932                     AbstractDirClose(dirh);
933                     result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
934                     return result;
935                 }
936                 else
937                 {
938                     Log(LOG_LEVEL_VERBOSE, "Skipping '%s'", newfrom);
939                     continue;
940                 }
941             }
942         }
943         else
944         {
945             if (cf_lstat(changes_newfrom, &sb, &(attr->copy), conn) == -1)
946             {
947                 Log(LOG_LEVEL_VERBOSE, "Can't stat '%s'. (cf_stat: %s)", newfrom, GetErrorStr());
948                 if (conn != NULL &&
949                     conn->conn_info->status != CONNECTIONINFO_STATUS_ESTABLISHED)
950                 {
951                     RecordInterruption(ctx, pp, attr, "Connection error when checking '%s'", newfrom);
952                     AbstractDirClose(dirh);
953                     result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
954                     return result;
955                 }
956                 else
957                 {
958                     Log(LOG_LEVEL_VERBOSE, "Skipping '%s'", newfrom);
959                     continue;
960                 }
961             }
962         }
963 
964         /* If "collapse_destination_dir" is set, we skip subdirectories, which
965          * means we are not creating them in the destination folder. */
966 
967         if (!attr->copy.collapse ||
968             (attr->copy.collapse && !S_ISDIR(sb.st_mode)))
969         {
970             if (!PathAppend(newto, sizeof(newto), dirp->d_name,
971                             FILE_SEPARATOR))
972             {
973                 RecordFailure(ctx, pp, attr,
974                               "Internal limit reached in SourceSearchAndCopy(),"
975                               " dest path too long: '%s' + '%s'",
976                               newto, dirp->d_name);
977                 result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
978                 AbstractDirClose(dirh);
979                 return result;
980             }
981         }
982         if (ChrootChanges())
983         {
984             /* Need to refresh 'changes_newto' because 'newto' could have been
985              * changed above and also the above call of ToChangesChroot() for
986              * 'changes_newfrom' modifies the internal buffer returned by
987              * ToChangesChroot(). */
988             changes_newto = ToChangesChroot(newto);
989         }
990 
991         if ((attr->recursion.xdev) && (DeviceBoundary(&sb, rootdevice)))
992         {
993             Log(LOG_LEVEL_VERBOSE, "Skipping '%s' on different device", newfrom);
994             continue;
995         }
996 
997         if (S_ISDIR(sb.st_mode))
998         {
999             if (attr->recursion.travlinks)
1000             {
1001                 Log(LOG_LEVEL_VERBOSE, "Traversing directory links during copy is too dangerous, pruned");
1002                 continue;
1003             }
1004 
1005             if (SkipDirLinks(ctx, newfrom, dirp->d_name, attr->recursion))
1006             {
1007                 continue;
1008             }
1009 
1010             memset(&dsb, 0, sizeof(struct stat));
1011 
1012             /* Only copy dirs if we are tracking subdirs */
1013 
1014             if ((!attr->copy.collapse) && (stat(newto, &dsb) == -1))
1015             {
1016                 if (mkdir(changes_newto, 0700) == -1)
1017                 {
1018                     RecordInterruption(ctx, pp, attr, "Can't make directory '%s'. (mkdir: %s)",
1019                                        newto, GetErrorStr());
1020                     result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
1021                     /* XXX: return result?!?!?! */
1022                     continue;
1023                 }
1024                 else
1025                 {
1026                     RecordChange(ctx, pp, attr, "Created directory '%s'", newto);
1027                     result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
1028                 }
1029 
1030                 if (stat(changes_newto, &dsb) == -1)
1031                 {
1032                     RecordInterruption(ctx, pp, attr,
1033                                        "Can't stat local copy '%s' - failed to establish directory. (stat: %s)",
1034                                        newto, GetErrorStr());
1035                     result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
1036                     /* XXX: return result?!?!?! */
1037                     continue;
1038                 }
1039             }
1040 
1041             Log(LOG_LEVEL_VERBOSE, "Entering '%s'", newto);
1042 
1043             if (!attr->copy.collapse)
1044             {
1045                 VerifyCopiedFileAttributes(ctx, newfrom, newto, &sb, &dsb, attr, pp);
1046             }
1047 
1048             result = PromiseResultUpdate(result, SourceSearchAndCopy(ctx, newfrom, newto, maxrecurse - 1, attr,
1049                                                                      pp, rootdevice, inode_cache, conn));
1050         }
1051         else
1052         {
1053             result = PromiseResultUpdate(result, VerifyCopy(ctx, newfrom, newto, attr, pp, inode_cache, conn));
1054         }
1055 
1056         if (conn != NULL &&
1057             conn->conn_info->status != CONNECTIONINFO_STATUS_ESTABLISHED)
1058         {
1059             RecordInterruption(ctx, pp, attr, "connection error");
1060             AbstractDirClose(dirh);
1061             return PROMISE_RESULT_INTERRUPTED;
1062         }
1063     }
1064 
1065     if (attr->copy.purge)
1066     {
1067         PurgeLocalFiles(ctx, namecache, to, attr, pp, conn);
1068         DeleteItemList(namecache);
1069     }
1070 
1071     AbstractDirClose(dirh);
1072 
1073     return result;
1074 }
1075 
VerifyCopy(EvalContext * ctx,const char * source,char * destination,const Attributes * attr,const Promise * pp,CompressedArray ** inode_cache,AgentConnection * conn)1076 static PromiseResult VerifyCopy(EvalContext *ctx,
1077                                 const char *source, char *destination,
1078                                 const Attributes *attr, const Promise *pp,
1079                                 CompressedArray **inode_cache,
1080                                 AgentConnection *conn)
1081 {
1082     assert(attr != NULL);
1083 
1084     /* (conn == NULL) means copy is from localhost. */
1085     const char *changes_source = source;
1086     bool local_changed_src = false;
1087     if ((conn == NULL) && ChrootChanges())
1088     {
1089         const char *chrooted_source = ToChangesChroot(source);
1090         if (access(chrooted_source, F_OK) == 0)
1091         {
1092             local_changed_src = true;
1093             changes_source = chrooted_source;
1094         }
1095     }
1096 
1097     int found;
1098     struct stat ssb;
1099 
1100     if (attr->copy.link_type == FILE_LINK_TYPE_NONE)
1101     {
1102         Log(LOG_LEVEL_DEBUG, "Treating links as files for '%s'", source);
1103         found = cf_stat(changes_source, &ssb, &(attr->copy), conn);
1104     }
1105     else
1106     {
1107         found = cf_lstat(changes_source, &ssb, &(attr->copy), conn);
1108     }
1109 
1110     if (found == -1)
1111     {
1112         RecordFailure(ctx, pp, attr, "Can't stat '%s' in verify copy", source);
1113         return PROMISE_RESULT_FAIL;
1114     }
1115 
1116     PromiseResult result = PROMISE_RESULT_NOOP;
1117     if (ssb.st_nlink > 1)      /* Preserve hard link structure when copying */
1118     {
1119         RegisterAHardLink(ssb.st_ino, ToChangesPath(destination),
1120                           ctx, pp, attr, &result, inode_cache);
1121     }
1122 
1123     if (S_ISDIR(ssb.st_mode))
1124     {
1125         char sourcedir[CF_BUFSIZE];
1126         strcpy(sourcedir, source);
1127         AddSlash(sourcedir);
1128 
1129         const char *changes_sourcedir = sourcedir;
1130         if (local_changed_src)
1131         {
1132             changes_sourcedir = ToChangesChroot(sourcedir);
1133         }
1134 
1135         AbstractDir *dirh;
1136         if ((dirh = AbstractDirOpen(changes_sourcedir, &(attr->copy), conn)) == NULL)
1137         {
1138             RecordFailure(ctx, pp, attr, "Can't open directory '%s'. (opendir: %s)",
1139                           sourcedir, GetErrorStr());
1140             result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
1141             return result;
1142         }
1143 
1144         /* Now check any overrides */
1145 
1146         char destdir[CF_BUFSIZE];
1147         strcpy(destdir, destination);
1148         AddSlash(destdir);
1149 
1150         const char *changes_destdir = destdir;
1151         if (ChrootChanges())
1152         {
1153             changes_destdir = ToChangesChroot(destdir);
1154         }
1155 
1156         struct stat dsb;
1157         if (stat(changes_destdir, &dsb) == -1)
1158         {
1159             RecordFailure(ctx, pp, attr, "Can't stat directory '%s'. (stat: %s)",
1160                           destdir, GetErrorStr());
1161             result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
1162         }
1163         else
1164         {
1165             result = PromiseResultUpdate(result,
1166                                          VerifyCopiedFileAttributes(ctx, sourcedir, destdir,
1167                                                                     &ssb, &dsb, attr, pp));
1168         }
1169 
1170         /* No backslashes over the network. */
1171         const char sep = (conn != NULL) ? '/' : FILE_SEPARATOR;
1172 
1173         for (const struct dirent *dirp = AbstractDirRead(dirh);
1174              dirp != NULL;
1175              dirp = AbstractDirRead(dirh))
1176         {
1177             if (!ConsiderAbstractFile(dirp->d_name, sourcedir,
1178                                       &(attr->copy), conn))
1179             {
1180                 if (conn != NULL &&
1181                     conn->conn_info->status != CONNECTIONINFO_STATUS_ESTABLISHED)
1182                 {
1183                     RecordInterruption(ctx, pp, attr,
1184                                        "Connection error when checking '%s'", dirp->d_name);
1185                     result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
1186                     return result;
1187                 }
1188                 else
1189                 {
1190                     Log(LOG_LEVEL_VERBOSE, "Skipping '%s'", dirp->d_name);
1191                 }
1192             }
1193 
1194             char sourcefile[CF_BUFSIZE];
1195             strcpy(sourcefile, sourcedir);
1196 
1197             if (!PathAppend(sourcefile, sizeof(sourcefile), dirp->d_name,
1198                             sep))
1199             {
1200                 /* TODO return FAIL */
1201                 FatalError(ctx, "VerifyCopy sourcefile buffer limit");
1202             }
1203 
1204             char destfile[CF_BUFSIZE];
1205             strcpy(destfile, destdir);
1206 
1207             if (!PathAppend(destfile, sizeof(destfile), dirp->d_name,
1208                             FILE_SEPARATOR))
1209             {
1210                 /* TODO return FAIL */
1211                 FatalError(ctx, "VerifyCopy destfile buffer limit");
1212             }
1213 
1214             const char *changes_sourcefile = sourcefile;
1215             if (local_changed_src)
1216             {
1217                 changes_sourcefile = ToChangesChroot(sourcefile);
1218             }
1219             if (attr->copy.link_type == FILE_LINK_TYPE_NONE)
1220             {
1221                 if (cf_stat(changes_sourcefile, &ssb, &(attr->copy), conn) == -1)
1222                 {
1223                     RecordFailure(ctx, pp, attr,
1224                                   "Can't stat source file (notlinked) '%s'. (stat: %s)",
1225                                   sourcefile, GetErrorStr());
1226                     result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
1227                     return result;
1228                 }
1229             }
1230             else
1231             {
1232                 if (cf_lstat(changes_sourcefile, &ssb, &(attr->copy), conn) == -1)
1233                 {
1234                     RecordFailure(ctx, pp, attr, "Can't stat source file '%s'. (lstat: %s)",
1235                                   sourcefile, GetErrorStr());
1236                     result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
1237                     return result;
1238                 }
1239             }
1240 
1241             result = PromiseResultUpdate(result, CfCopyFile(ctx, sourcefile, destfile, &ssb,
1242                                                             attr, pp, inode_cache, conn));
1243         }
1244 
1245         AbstractDirClose(dirh);
1246         return result;
1247     }
1248     else
1249     {
1250         /* TODO: are these copies necessary??? */
1251         char sourcefile[CF_BUFSIZE];
1252         char destfile[CF_BUFSIZE];
1253         strcpy(sourcefile, source);
1254         strcpy(destfile, destination);
1255 
1256         return CfCopyFile(ctx, sourcefile, destfile, &ssb,
1257                           attr, pp, inode_cache, conn);
1258     }
1259 }
1260 
LinkCopy(EvalContext * ctx,char * sourcefile,char * destfile,const struct stat * sb,const Attributes * attr,const Promise * pp,CompressedArray ** inode_cache,AgentConnection * conn)1261 static PromiseResult LinkCopy(EvalContext *ctx, char *sourcefile, char *destfile, const struct stat *sb, const Attributes *attr, const Promise *pp,
1262                               CompressedArray **inode_cache, AgentConnection *conn)
1263 /* Link the file to the source, instead of copying */
1264 #ifdef __MINGW32__
1265 {
1266     RecordFailure(ctx, pp, attr,
1267                   "Can't link '%s' to '%s' (Windows does not support symbolic links)",
1268                   sourcefile, destfile);
1269     return PROMISE_RESULT_FAIL;
1270 }
1271 #else                           /* !__MINGW32__ */
1272 {
1273     assert(attr != NULL);
1274     char linkbuf[CF_BUFSIZE - 1];
1275     const char *lastnode;
1276     PromiseResult result = PROMISE_RESULT_NOOP;
1277 
1278     linkbuf[0] = '\0';
1279 
1280     /* (conn == NULL) means copy is from localhost. */
1281     const char *changes_sourcefile = sourcefile;
1282     bool local_changed_src = false;
1283     if ((conn == NULL) && ChrootChanges())
1284     {
1285         changes_sourcefile = ToChangesChroot(sourcefile);
1286         struct stat src_sb;
1287         local_changed_src = (lstat(changes_sourcefile, &src_sb) != -1);
1288     }
1289     char chrooted_linkbuf[sizeof(linkbuf)];
1290     chrooted_linkbuf[0] = '\0';
1291 
1292     if (S_ISLNK(sb->st_mode))
1293     {
1294         if (local_changed_src)
1295         {
1296             if (cf_readlink(ctx, changes_sourcefile, chrooted_linkbuf, sizeof(chrooted_linkbuf),
1297                             attr, pp, conn, &result) == -1)
1298             {
1299                 RecordFailure(ctx, pp, attr, "Can't read link '%s'", sourcefile);
1300                 return PROMISE_RESULT_FAIL;
1301             }
1302             if (IsAbsoluteFileName(chrooted_linkbuf))
1303             {
1304                 strlcpy(linkbuf, ToNormalRoot(chrooted_linkbuf), sizeof(linkbuf));
1305             }
1306             else
1307             {
1308                 strlcpy(linkbuf, chrooted_linkbuf, sizeof(linkbuf));
1309             }
1310         }
1311         else
1312         {
1313             if (cf_readlink(ctx, sourcefile, linkbuf, sizeof(linkbuf),
1314                             attr, pp, conn, &result) == -1)
1315             {
1316                 RecordFailure(ctx, pp, attr, "Can't read link '%s'", sourcefile);
1317                 return PROMISE_RESULT_FAIL;
1318             }
1319         }
1320 
1321         Log(LOG_LEVEL_VERBOSE, "Checking link from '%s' to '%s'", destfile, linkbuf);
1322 
1323         if ((attr->copy.link_type == FILE_LINK_TYPE_ABSOLUTE) && (!IsAbsoluteFileName(linkbuf)))        /* Not absolute path - must fix */
1324         {
1325             char vbuff[CF_BUFSIZE];
1326 
1327             if (local_changed_src)
1328             {
1329                 strlcpy(vbuff, changes_sourcefile, CF_BUFSIZE);
1330                 ChopLastNode(vbuff);
1331                 AddSlash(vbuff);
1332                 strncat(vbuff, chrooted_linkbuf, CF_BUFSIZE - 1);
1333                 strlcpy(chrooted_linkbuf, vbuff, CF_BUFSIZE - 1);
1334             }
1335             else
1336             {
1337                 strlcpy(vbuff, sourcefile, CF_BUFSIZE);
1338                 ChopLastNode(vbuff);
1339                 AddSlash(vbuff);
1340                 strncat(vbuff, linkbuf, CF_BUFSIZE - 1);
1341                 strlcpy(linkbuf, vbuff, CF_BUFSIZE - 1);
1342             }
1343         }
1344     }
1345     else
1346     {
1347         if (local_changed_src)
1348         {
1349             strlcpy(chrooted_linkbuf, changes_sourcefile, CF_BUFSIZE - 1);
1350         }
1351         else
1352         {
1353             strlcpy(linkbuf, sourcefile, CF_BUFSIZE - 1);
1354         }
1355     }
1356 
1357     lastnode = ReadLastNode(sourcefile);
1358 
1359     if (MatchRlistItem(ctx, attr->copy.copy_links, lastnode))
1360     {
1361         if (local_changed_src)
1362         {
1363             ExpandLinks(chrooted_linkbuf, changes_sourcefile, 0, CF_MAXLINKLEVEL);
1364             strlcpy(linkbuf, ToNormalRoot(chrooted_linkbuf), sizeof(linkbuf));
1365         }
1366         else
1367         {
1368             ExpandLinks(linkbuf, sourcefile, 0, CF_MAXLINKLEVEL);
1369         }
1370         Log(LOG_LEVEL_VERBOSE, "Link item in copy '%s' marked for copying from '%s' instead",
1371             sourcefile, linkbuf);
1372 
1373         struct stat ssb;
1374         if (local_changed_src)
1375         {
1376             stat(chrooted_linkbuf, &ssb);
1377         }
1378         else
1379         {
1380             stat(linkbuf, &ssb);
1381         }
1382 
1383         /* CfCopyFiles() does the chrooting (if necessary) on its second
1384          * argument so we need to always give it the original path. */
1385         return CfCopyFile(ctx, linkbuf, destfile, &ssb, attr, pp, inode_cache, conn);
1386     }
1387 
1388     int status;
1389     switch (attr->copy.link_type)
1390     {
1391     case FILE_LINK_TYPE_SYMLINK:
1392 
1393         if (*linkbuf == '.')
1394         {
1395             status = VerifyRelativeLink(ctx, destfile, linkbuf, attr, pp);
1396         }
1397         else
1398         {
1399             status = VerifyLink(ctx, destfile, linkbuf, attr, pp);
1400         }
1401         break;
1402 
1403     case FILE_LINK_TYPE_RELATIVE:
1404         status = VerifyRelativeLink(ctx, destfile, linkbuf, attr, pp);
1405         break;
1406 
1407     case FILE_LINK_TYPE_ABSOLUTE:
1408         status = VerifyAbsoluteLink(ctx, destfile, linkbuf, attr, pp);
1409         break;
1410 
1411     case FILE_LINK_TYPE_HARDLINK:
1412         status = VerifyHardLink(ctx, destfile, linkbuf, attr, pp);
1413         break;
1414 
1415     default:
1416         ProgrammingError("Unhandled link type in switch: %d", attr->copy.link_type);
1417     }
1418 
1419     if ((status == PROMISE_RESULT_CHANGE) || (status == PROMISE_RESULT_NOOP))
1420     {
1421         const char *changes_destfile = destfile;
1422         if (ChrootChanges())
1423         {
1424             changes_destfile = ToChangesChroot(destfile);
1425         }
1426 
1427         struct stat dsb;
1428         if (lstat(changes_destfile, &dsb) == -1)
1429         {
1430             RecordFailure(ctx, pp, attr, "Can't lstat '%s'. (lstat: %s)", destfile, GetErrorStr());
1431             result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
1432             return result;
1433         }
1434         else
1435         {
1436             result = PromiseResultUpdate(result,
1437                                          VerifyCopiedFileAttributes(ctx, sourcefile, destfile,
1438                                                                     sb, &dsb, attr, pp));
1439         }
1440     }
1441 
1442     return result;
1443 }
1444 #endif /* !__MINGW32__ */
1445 
CopyRegularFile(EvalContext * ctx,const char * source,const char * dest,const struct stat * sstat,const Attributes * attr,const Promise * pp,CompressedArray ** inode_cache,AgentConnection * conn,PromiseResult * result)1446 bool CopyRegularFile(EvalContext *ctx, const char *source, const char *dest, const struct stat *sstat,
1447                      const Attributes *attr, const Promise *pp, CompressedArray **inode_cache,
1448                      AgentConnection *conn, PromiseResult *result)
1449 {
1450     assert(attr != NULL);
1451     assert(sstat != NULL);
1452 
1453     char new[CF_BUFSIZE], *linkable;
1454     int remote = false, backupisdir = false, backupok = false, discardbackup;
1455 
1456 #ifdef HAVE_UTIME_H
1457     struct utimbuf timebuf;
1458 #endif
1459 
1460 #ifdef __APPLE__
1461 /* For later copy from new to dest */
1462     char *rsrcbuf;
1463     int rsrcbytesr;             /* read */
1464     int rsrcbytesw;             /* written */
1465     int rsrcbytesl;             /* to read */
1466     int rsrcrd;
1467     int rsrcwd;
1468 
1469 /* Keep track of if a resrouce fork */
1470     int rsrcfork = 0;
1471 #endif
1472 
1473     discardbackup = ((attr->copy.backup == BACKUP_OPTION_NO_BACKUP) || (attr->copy.backup == BACKUP_OPTION_REPOSITORY_STORE));
1474 
1475     if (!MakingChanges(ctx, pp, attr, result, "copy '%s' to '%s'", source, dest))
1476     {
1477         return false;
1478     }
1479 
1480     /* Make an assoc array of inodes used to preserve hard links */
1481 
1482     linkable = CompressedArrayValue(*inode_cache, sstat->st_ino);
1483 
1484     /* If making changes in chroot, we need to make sure the target is in the
1485      * changes chroot before we create the hardlink. */
1486     if (ChrootChanges() && (linkable != NULL) && (sstat->st_nlink > 1))
1487     {
1488         PrepareChangesChroot(linkable);
1489     }
1490 
1491     if (sstat->st_nlink > 1)     /* Preserve hard links, if possible */
1492     {
1493         if ((linkable != NULL) && (strcmp(dest, linkable) != 0))
1494         {
1495             if (unlink(ToChangesPath(dest)) == 0)
1496             {
1497                 RecordChange(ctx, pp, attr, "Removed '%s'", dest);
1498                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1499             }
1500             MakeHardLink(ctx, dest, linkable, attr, pp, result);
1501             return true;
1502         }
1503     }
1504 
1505     if (conn != NULL)
1506     {
1507         assert(attr->copy.servers && strcmp(RlistScalarValue(attr->copy.servers), "localhost"));
1508         Log(LOG_LEVEL_DEBUG, "This is a remote copy from server '%s'", RlistScalarValue(attr->copy.servers));
1509         remote = true;
1510     }
1511 
1512 #ifdef __APPLE__
1513     if (strstr(dest, _PATH_RSRCFORKSPEC))
1514     {
1515         char *tmpstr = xstrndup(dest, CF_BUFSIZE);
1516 
1517         rsrcfork = 1;
1518         /* Drop _PATH_RSRCFORKSPEC */
1519         char *forkpointer = strstr(tmpstr, _PATH_RSRCFORKSPEC);
1520         *forkpointer = '\0';
1521 
1522         strlcpy(new, tmpstr, CF_BUFSIZE);
1523 
1524         free(tmpstr);
1525     }
1526     else
1527 #endif
1528     {
1529         strlcpy(new, dest, CF_BUFSIZE);
1530 
1531         if (!JoinSuffix(new, sizeof(new), CF_NEW))
1532         {
1533             Log(LOG_LEVEL_ERR, "Unable to construct filename for copy");
1534             return false;
1535         }
1536     }
1537 
1538     struct stat dest_stat;
1539     int ret = stat(ToChangesPath(dest), &dest_stat);
1540     bool dest_exists = (ret == 0);
1541 
1542     if (remote)
1543     {
1544         if (conn->error)
1545         {
1546             RecordFailure(ctx, pp, attr, "Failed to copy file '%s' from '%s' (connection error)",
1547                           source, conn->remoteip);
1548             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1549             return false;
1550         }
1551 
1552         if (!CopyRegularFileNet(source, ToChangesPath(new),
1553                                 sstat->st_size, attr->copy.encrypt, conn))
1554         {
1555             RecordFailure(ctx, pp, attr, "Failed to copy file '%s' from '%s'",
1556                           source, conn->remoteip);
1557             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1558             return false;
1559         }
1560         RecordChange(ctx, pp, attr, "Copied file '%s' from '%s' to '%s'",
1561                      source, new, conn->remoteip);
1562         *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1563     }
1564     else
1565     {
1566         const char *changes_source = source;
1567         char *chrooted_source = NULL;
1568         if (ChrootChanges())
1569         {
1570             /* Need to create a copy because the next ToChangesChroot() call
1571              * will override the internal buffer. */
1572             chrooted_source = xstrdup(ToChangesChroot(source));
1573             struct stat sb;
1574             if (lstat(chrooted_source, &sb) != -1)
1575             {
1576                 changes_source = chrooted_source;
1577             }
1578         }
1579         // If preserve is true, retain permissions of source file
1580         if (attr->copy.preserve)
1581         {
1582             if (!CopyRegularFileDisk(changes_source, ToChangesPath(new)))
1583             {
1584                 RecordFailure(ctx, pp, attr, "Failed copying file '%s' to '%s'", source, new);
1585                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1586                 return false;
1587             }
1588             RecordChange(ctx, pp, attr, "Copied file '%s' to '%s' (permissions preserved)",
1589                          source, new);
1590             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1591         }
1592         else
1593         {
1594             // Never preserve SUID bit (0777)
1595             int mode = dest_exists ? (dest_stat.st_mode & 0777) : CF_PERMS_DEFAULT;
1596             if (!CopyRegularFileDiskPerms(changes_source,
1597                                           ToChangesPath(new),
1598                                           mode))
1599             {
1600                 RecordFailure(ctx, pp, attr, "Failed copying file '%s' to '%s'", source, new);
1601                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1602                 return false;
1603             }
1604             RecordChange(ctx, pp, attr, "Copied file '%s' to '%s' (mode '%jo')",
1605                          source, new, (uintmax_t) mode);
1606             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1607         }
1608         free(chrooted_source);
1609 
1610 #ifdef HAVE_UTIME_H
1611         if (attr->copy.stealth)
1612         {
1613             timebuf.actime = sstat->st_atime;
1614             timebuf.modtime = sstat->st_mtime;
1615             utime(source, &timebuf);
1616         }
1617 #endif
1618     }
1619 
1620     Log(LOG_LEVEL_VERBOSE, "Copy of regular file succeeded '%s' to '%s'", source, new);
1621 
1622     char backup[CF_BUFSIZE];
1623     char chrooted_backup[CF_BUFSIZE];
1624     const char *changes_backup = backup;
1625     backup[0] = '\0';
1626     chrooted_backup[0] = '\0';
1627 
1628     /* XXX: Do RecordChange() for the changes done below? They are just "behind
1629      *      the scenes" changes the user maybe doesn't care about if they just
1630      *      work? */
1631     if (dest_exists)
1632     {
1633         if (!discardbackup)
1634         {
1635             char stamp[CF_BUFSIZE];
1636             time_t stampnow;
1637 
1638             Log(LOG_LEVEL_DEBUG, "Backup file '%s'", source);
1639 
1640             strlcpy(backup, dest, CF_BUFSIZE);
1641             if (attr->copy.backup == BACKUP_OPTION_TIMESTAMP)
1642             {
1643                 stampnow = time((time_t *) NULL);
1644                 snprintf(stamp, CF_BUFSIZE - 1, "_%lu_%s",
1645                          CFSTARTTIME, CanonifyName(ctime(&stampnow)));
1646 
1647                 if (!JoinSuffix(backup, sizeof(backup), stamp))
1648                 {
1649                     RecordFailure(ctx, pp, attr,
1650                                   "Failed to determine file name for the backup of '%s'",
1651                                   dest);
1652                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1653                     return false;
1654                 }
1655             }
1656 
1657             if (!JoinSuffix(backup, sizeof(backup), CF_SAVED))
1658             {
1659                 RecordFailure(ctx, pp, attr,
1660                               "Failed to determine file name for the backup of '%s'",
1661                               dest);
1662                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1663                 return false;
1664             }
1665 
1666             if (ChrootChanges())
1667             {
1668                 strlcpy(chrooted_backup, ToChangesChroot(backup), CF_BUFSIZE);
1669                 changes_backup = chrooted_backup;
1670             }
1671 
1672             Log(LOG_LEVEL_VERBOSE, "Backup for '%s' is '%s'", dest, backup);
1673 
1674             /* Now in case of multiple copies of same object,
1675              * try to avoid overwriting original backup */
1676 
1677             if (lstat(changes_backup, &dest_stat) != -1)
1678             {
1679                 /* if there is a dir in the way */
1680                 if (S_ISDIR(dest_stat.st_mode))
1681                 {
1682                     backupisdir = true;
1683 
1684                     /* PurgeLocalFiles() does ToChangesChroot() internally (if
1685                      * needed). */
1686                     PurgeLocalFiles(ctx, NULL, backup, attr, pp, conn);
1687                     if (rmdir(changes_backup) == 0)
1688                     {
1689                         RecordChange(ctx, pp, attr, "Removed old backup directory '%s'", backup);
1690                         *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1691                     }
1692                     else
1693                     {
1694                         RecordFailure(ctx, pp, attr,
1695                                       "Failed to remove old backup directory '%s'", backup);
1696                         *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1697                     }
1698                 }
1699 
1700                 if (unlink(changes_backup) == 0)
1701                 {
1702                     RecordChange(ctx, pp, attr, "Removed old backup '%s'", backup);
1703                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1704                 }
1705                 else
1706                 {
1707                     RecordFailure(ctx, pp, attr,
1708                                   "Failed to remove old backup '%s'", backup);
1709                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1710                 }
1711             }
1712 
1713             if (rename(dest, changes_backup) == 0)
1714             {
1715                 RecordChange(ctx, pp, attr, "Backed up '%s' as '%s'", dest, backup);
1716                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1717             }
1718             else
1719             {
1720                 RecordFailure(ctx, pp, attr, "Failed to back up '%s' as '%s'", dest, backup);
1721                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1722             }
1723 
1724             /* Did the rename() succeed? NFS-safe */
1725             backupok = (lstat(changes_backup, &dest_stat) != -1);
1726         }
1727         else
1728         {
1729             /* Discarding backup */
1730 
1731             /* Mainly important if there is a dir in the way */
1732             if (S_ISDIR(dest_stat.st_mode))
1733             {
1734                 /* PurgeLocalFiles does ToChangesChroot(dest) internally. */
1735                 PurgeLocalFiles(ctx, NULL, dest, attr, pp, conn);
1736 
1737                 if (rmdir(ToChangesPath(dest)) == 0)
1738                 {
1739                     RecordChange(ctx, pp, attr, "Removed directory '%s'", dest);
1740                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1741                 }
1742                 else
1743                 {
1744                     RecordFailure(ctx, pp, attr,
1745                                   "Failed to remove directory '%s'", dest);
1746                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1747                 }
1748             }
1749         }
1750     }
1751 
1752     const char *changes_new = new;
1753     char *chrooted_new = NULL;
1754     if (ChrootChanges())
1755     {
1756         chrooted_new = xstrdup(ToChangesChroot(new));
1757         changes_new = chrooted_new;
1758     }
1759     const char *changes_dest = dest;
1760     if (ChrootChanges())
1761     {
1762         changes_dest = ToChangesChroot(dest);
1763     }
1764 
1765     struct stat new_stat;
1766     if (lstat(changes_new, &new_stat) == -1)
1767     {
1768         RecordFailure(ctx, pp, attr,
1769                       "Can't stat new file '%s' - another agent has picked it up?. (stat: %s)",
1770                       new, GetErrorStr());
1771         *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1772         free(chrooted_new);
1773         return false;
1774     }
1775 
1776     if ((S_ISREG(new_stat.st_mode)) && (new_stat.st_size != sstat->st_size))
1777     {
1778         RecordFailure(ctx, pp, attr,
1779                       "New file '%s' seems to have been corrupted in transit,"
1780                       " destination size (%zd) differs from the source size (%zd), aborting.",
1781                       new, (size_t) new_stat.st_size, (size_t) sstat->st_size);
1782         *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1783 
1784         if (backupok && (rename(changes_backup, changes_dest) == 0))
1785         {
1786             RecordChange(ctx, pp, attr, "Restored '%s' from '%s'", dest, backup);
1787             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1788         }
1789         else
1790         {
1791             RecordFailure(ctx, pp, attr, "Could not restore '%s' as '%s'", dest, backup);
1792             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1793         }
1794 
1795         free(chrooted_new);
1796         return false;
1797     }
1798 
1799     if (attr->copy.verify)
1800     {
1801         Log(LOG_LEVEL_VERBOSE, "Final verification of transmission ...");
1802 
1803         if (CompareFileHashes(source, changes_new, sstat, &new_stat, &(attr->copy), conn))
1804         {
1805             RecordFailure(ctx, pp, attr,
1806                           "New file '%s' seems to have been corrupted in transit, aborting.", new);
1807             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1808 
1809             if (backupok && (rename(changes_backup, changes_dest) == 0))
1810             {
1811                 RecordChange(ctx, pp, attr, "Restored '%s' from '%s'", dest, backup);
1812                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1813             }
1814             else
1815             {
1816                 RecordFailure(ctx, pp, attr, "Could not restore '%s' as '%s'", dest, backup);
1817                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1818             }
1819 
1820             free(chrooted_new);
1821             return false;
1822         }
1823         else
1824         {
1825             Log(LOG_LEVEL_VERBOSE, "New file '%s' transmitted correctly - verified", new);
1826         }
1827     }
1828 
1829 #ifdef __APPLE__
1830     if (rsrcfork)
1831     {                           /* Can't just "mv" the resource fork, unfortunately */
1832         rsrcrd = safe_open(changes_new, O_RDONLY | O_BINARY);
1833         rsrcwd = safe_open(changes_dest, O_WRONLY | O_BINARY | O_CREAT | O_TRUNC);
1834 
1835         if (rsrcrd == -1 || rsrcwd == -1)
1836         {
1837             Log(LOG_LEVEL_INFO, "Open of Darwin resource fork rsrcrd/rsrcwd failed. (open: %s)", GetErrorStr());
1838             close(rsrcrd);
1839             close(rsrcwd);
1840             free(chrooted_new);
1841             return (false);
1842         }
1843 
1844         rsrcbuf = xmalloc(CF_BUFSIZE);
1845 
1846         rsrcbytesr = 0;
1847 
1848         while (1)
1849         {
1850             rsrcbytesr = read(rsrcrd, rsrcbuf, CF_BUFSIZE);
1851 
1852             if (rsrcbytesr == -1)
1853             {                   /* Ck error */
1854                 if (errno == EINTR)
1855                 {
1856                     continue;
1857                 }
1858                 else
1859                 {
1860                     RecordFailure(ctx, pp, attr,
1861                                   "Read of Darwin resource fork rsrcrd '%s' failed (read: %s)",
1862                                   new, GetErrorStr());
1863                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1864                     close(rsrcrd);
1865                     close(rsrcwd);
1866                     free(rsrcbuf);
1867                     free(chrooted_new);
1868                     return (false);
1869                 }
1870             }
1871 
1872             else if (rsrcbytesr == 0)
1873             {
1874                 /* Reached EOF */
1875                 close(rsrcrd);
1876                 close(rsrcwd);
1877                 free(rsrcbuf);
1878 
1879                 if (unlink(changes_new) == 0)
1880                 {
1881                     RecordChange(ctx, pp, attr, "Moved resource fork '%s' to '%s'", new, dest);
1882                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1883                 }
1884                 else
1885                 {
1886                     RecordFailure(ctx, pp, attr, "Failed to remove resource fork '%s'", new);
1887                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1888                     /* not a fatal failure, just record it and keep moving */
1889                 }
1890                 break;
1891             }
1892 
1893             rsrcbytesl = rsrcbytesr;
1894             rsrcbytesw = 0;
1895 
1896             while (rsrcbytesl > 0)
1897             {
1898                 rsrcbytesw += write(rsrcwd, rsrcbuf, rsrcbytesl);
1899 
1900                 if (rsrcbytesw == -1)
1901                 {
1902                     if (errno == EINTR)
1903                     {
1904                         continue;
1905                     }
1906                     else
1907                     {
1908                         RecordFailure(ctx, pp, attr,
1909                                       "Write of Darwin resource fork rsrcwd '%s' failed (write: %s)",
1910                                       dest, GetErrorStr());
1911                         *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1912                         close(rsrcrd);
1913                         close(rsrcwd);
1914                         free(rsrcbuf);
1915                         free(chrooted_new);
1916                         return (false);
1917                     }
1918                 }
1919                 rsrcbytesl = rsrcbytesr - rsrcbytesw;
1920             }
1921         }
1922     }
1923     else
1924 #endif
1925     {
1926         if (rename(changes_new, changes_dest) == 0)
1927         {
1928             RecordChange(ctx, pp, attr, "Moved '%s' to '%s'", new, dest);
1929             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1930         }
1931         else
1932         {
1933             RecordFailure(ctx, pp, attr,
1934                           "Could not install copy file as '%s', directory in the way?. (rename: %s)",
1935                           dest, GetErrorStr());
1936             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1937 
1938             if (backupok && (rename(changes_backup, changes_dest) == 0))
1939             {
1940                 RecordChange(ctx, pp, attr, "Restored '%s' from '%s'", dest, backup);
1941                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1942             }
1943             else
1944             {
1945                 RecordFailure(ctx, pp, attr, "Could not restore '%s' as '%s'", dest, backup);
1946                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1947             }
1948 
1949             free(chrooted_new);
1950             return false;
1951         }
1952     }
1953     free(chrooted_new);
1954 
1955     if ((!discardbackup) && backupisdir)
1956     {
1957         Log(LOG_LEVEL_INFO, "Cannot move a directory to repository, leaving at '%s'", backup);
1958     }
1959     else if ((!discardbackup) && (ArchiveToRepository(changes_backup, attr)))
1960     {
1961         RecordChange(ctx, pp, attr, "Archived backup '%s'", backup);
1962         *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
1963 
1964         if (unlink(changes_backup) != 0)
1965         {
1966             RecordFailure(ctx, pp, attr, "Failed to clean backup '%s' after archiving", backup);
1967             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
1968         }
1969     }
1970 
1971 #ifdef HAVE_UTIME_H
1972     if (attr->copy.stealth)
1973     {
1974         timebuf.actime = sstat->st_atime;
1975         timebuf.modtime = sstat->st_mtime;
1976         utime(dest, &timebuf);
1977     }
1978 #endif
1979 
1980     return true;
1981 }
1982 
TransformFile(EvalContext * ctx,char * file,const Attributes * attr,const Promise * pp,PromiseResult * result)1983 static bool TransformFile(EvalContext *ctx, char *file, const Attributes *attr, const Promise *pp, PromiseResult *result)
1984 {
1985     assert(attr != NULL);
1986     FILE *pop = NULL;
1987     int transRetcode = 0;
1988 
1989     if (attr->transformer == NULL || file == NULL)
1990     {
1991         return false;
1992     }
1993 
1994     Buffer *command = BufferNew();
1995     ExpandScalar(ctx, PromiseGetBundle(pp)->ns, PromiseGetBundle(pp)->name, attr->transformer, command);
1996 
1997     if (!IsExecutable(CommandArg0(BufferData(command))))
1998     {
1999         RecordFailure(ctx, pp, attr, "Transformer '%s' for file '%s' failed", attr->transformer, file);
2000         *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
2001         BufferDestroy(command);
2002         return false;
2003     }
2004 
2005     const char * const command_str = BufferData(command);
2006     if (MakingChanges(ctx, pp, attr, result, "transform file '%s' with '%s'", file, command_str))
2007     {
2008         CfLock thislock = AcquireLock(ctx, command_str, VUQNAME, CFSTARTTIME, attr->transaction.ifelapsed, attr->transaction.expireafter, pp, false);
2009 
2010         if (thislock.lock == NULL)
2011         {
2012             BufferDestroy(command);
2013             return false;
2014         }
2015 
2016         const char *changes_command = command_str;
2017         char *chrooted_command = NULL;
2018         if (ChrootChanges())
2019         {
2020             size_t command_len = BufferSize(command);
2021             chrooted_command = xmalloc(command_len + PATH_MAX + 1);
2022             strncpy(chrooted_command, command_str, command_len + 1);
2023             ssize_t ret = StringReplace(chrooted_command, command_len + PATH_MAX + 1,
2024                                         file, ToChangesChroot(file));
2025             if ((ret <= 0) || ((size_t) ret == command_len + PATH_MAX + 1))
2026             {
2027                 RecordFailure(ctx, pp, attr,
2028                               "Failed to replace path '%s' in transformer command '%s' for changes chroot",
2029                               file, command_str);
2030                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
2031                 YieldCurrentLock(thislock);
2032                 BufferDestroy(command);
2033                 free(chrooted_command);
2034                 return false;
2035             }
2036             else
2037             {
2038                 changes_command = chrooted_command;
2039             }
2040         }
2041 
2042         Log(LOG_LEVEL_INFO, "Transforming '%s' with '%s'", file, command_str);
2043         if ((pop = cf_popen(changes_command, "r", true)) == NULL)
2044         {
2045             RecordFailure(ctx, pp, attr, "Transformer '%s' for file '%s' failed", attr->transformer, file);
2046             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
2047             YieldCurrentLock(thislock);
2048             BufferDestroy(command);
2049             free(chrooted_command);
2050             return false;
2051         }
2052         free(chrooted_command);
2053 
2054         size_t line_size = CF_BUFSIZE;
2055         char *line = xmalloc(line_size);
2056 
2057         for (;;)
2058         {
2059             ssize_t res = CfReadLine(&line, &line_size, pop);
2060             if (res == -1)
2061             {
2062                 if (!feof(pop))
2063                 {
2064                     cf_pclose(pop);
2065                     RecordFailure(ctx, pp, attr, "Transformer '%s' for file '%s' failed", attr->transformer, file);
2066                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
2067                     YieldCurrentLock(thislock);
2068                     free(line);
2069                     BufferDestroy(command);
2070                     return false;
2071                 }
2072                 else
2073                 {
2074                     break;
2075                 }
2076             }
2077 
2078             Log(LOG_LEVEL_INFO, "%s", line);
2079         }
2080         RecordChange(ctx, pp, attr, "Transformed '%s' with '%s' ", file, command_str);
2081         free(line);
2082 
2083         transRetcode = cf_pclose(pop);
2084 
2085         if (VerifyCommandRetcode(ctx, transRetcode, attr, pp, result))
2086         {
2087             Log(LOG_LEVEL_INFO, "Transformer '%s' => '%s' seemed to work ok", file, command_str);
2088         }
2089         else
2090         {
2091             Log(LOG_LEVEL_ERR, "Transformer '%s' => '%s' returned error", file, command_str);
2092         }
2093 
2094         YieldCurrentLock(thislock);
2095     }
2096 
2097     BufferDestroy(command);
2098     return true;
2099 }
2100 
VerifyName(EvalContext * ctx,char * path,const struct stat * sb,const Attributes * attr,const Promise * pp)2101 static PromiseResult VerifyName(EvalContext *ctx, char *path, const struct stat *sb, const Attributes *attr, const Promise *pp)
2102 {
2103     assert(attr != NULL);
2104     mode_t newperm;
2105     struct stat dsb;
2106 
2107     const char *changes_path = path;
2108     char *chrooted_path = NULL;
2109     if (ChrootChanges())
2110     {
2111         chrooted_path = xstrdup(ToChangesChroot(path));
2112         changes_path = chrooted_path;
2113     }
2114 
2115     if (lstat(changes_path, &dsb) == -1)
2116     {
2117         RecordNoChange(ctx, pp, attr, "File object named '%s' is not there (promise kept)", path);
2118         free(chrooted_path);
2119         return PROMISE_RESULT_NOOP;
2120     }
2121     else
2122     {
2123         if (attr->rename.disable)
2124         {
2125             /* XXX: Why should attr->rename.disable imply that the file doesn't exist? */
2126             Log(LOG_LEVEL_WARNING, "File object '%s' exists, contrary to promise", path);
2127         }
2128     }
2129 
2130     PromiseResult result = PROMISE_RESULT_NOOP;
2131     if (attr->rename.newname != NULL)
2132     {
2133         char newname[CF_BUFSIZE];
2134         if (IsAbsoluteFileName(attr->rename.newname))
2135         {
2136             size_t copied = strlcpy(newname, attr->rename.newname, sizeof(newname));
2137             if (copied > sizeof(newname))
2138             {
2139                 RecordFailure(ctx, pp, attr,
2140                               "Internal buffer limit in rename operation, new name too long: '%s'",
2141                               attr->rename.newname);
2142                 result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
2143                 free(chrooted_path);
2144                 return result;
2145             }
2146         }
2147         else
2148         {
2149             strncpy(newname, path, sizeof(newname));
2150             ChopLastNode(newname);
2151 
2152             if (!PathAppend(newname, sizeof(newname),
2153                             attr->rename.newname, FILE_SEPARATOR))
2154             {
2155                 RecordFailure(ctx, pp, attr,
2156                               "Internal buffer limit in rename operation, destination: '%s' + '%s'",
2157                               newname, attr->rename.newname);
2158                 result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
2159                 free(chrooted_path);
2160                 return result;
2161             }
2162         }
2163 
2164         const char *changes_newname = newname;
2165         if (ChrootChanges())
2166         {
2167             changes_newname = ToChangesChroot(newname);
2168         }
2169 
2170         if (MakingChanges(ctx, pp, attr, &result, "rename file '%s' to '%s'",
2171                           path, newname))
2172         {
2173             if (!FileInRepository(newname))
2174             {
2175                 if (rename(changes_path, changes_newname) == -1)
2176                 {
2177                     RecordFailure(ctx, pp, attr, "Error occurred while renaming '%s'. (rename: %s)",
2178                                   path, GetErrorStr());
2179                     result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
2180                 }
2181                 else
2182                 {
2183                     if (ChrootChanges())
2184                     {
2185                         RecordFileRenamedInChroot(path, newname);
2186                     }
2187                     RecordChange(ctx, pp, attr, "Renamed file '%s' to '%s'", path, newname);
2188                     result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
2189                 }
2190             }
2191             else
2192             {
2193                 RecordWarning(ctx, pp, attr,
2194                               "Renaming '%s' to same destination twice would overwrite saved copy - aborting",
2195                               path);
2196                 result = PromiseResultUpdate(result, PROMISE_RESULT_WARN);
2197             }
2198         }
2199 
2200         free(chrooted_path);
2201         return result;
2202     }
2203 
2204     if (S_ISLNK(dsb.st_mode))
2205     {
2206         if (attr->rename.disable)
2207         {
2208             if (MakingChanges(ctx, pp, attr, &result, "disable link '%s'", path))
2209             {
2210                 if (unlink(changes_path) == -1)
2211                 {
2212                     RecordFailure(ctx, pp, attr, "Unable to unlink '%s'. (unlink: %s)",
2213                                   path, GetErrorStr());
2214                     result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
2215                 }
2216                 else
2217                 {
2218                     RecordChange(ctx, pp, attr, "Disabled symbolic link '%s' by deleting it", path);
2219                     result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
2220                 }
2221             }
2222 
2223             free(chrooted_path);
2224             return result;
2225         }
2226     }
2227 
2228 /* Normal disable - has priority */
2229 
2230     if (attr->rename.disable)
2231     {
2232         char newname[CF_BUFSIZE];
2233 
2234         if (!MakingChanges(ctx, pp, attr, &result, "rename '%s' '%s'",
2235                            S_ISDIR(sb->st_mode) ? "directory" : "file", path))
2236         {
2237             free(chrooted_path);
2238             return result;
2239         }
2240 
2241         if (attr->rename.newname && strlen(attr->rename.newname) > 0)
2242         {
2243             if (IsAbsoluteFileName(attr->rename.newname))
2244             {
2245                 strlcpy(newname, attr->rename.newname, CF_BUFSIZE);
2246             }
2247             else
2248             {
2249                 strcpy(newname, path);
2250                 ChopLastNode(newname);
2251 
2252                 if (!PathAppend(newname, sizeof(newname),
2253                                 attr->rename.newname, FILE_SEPARATOR))
2254                 {
2255                     RecordFailure(ctx, pp, attr,
2256                                   "Internal buffer limit in rename operation, destination: '%s' + '%s'",
2257                                   newname, attr->rename.newname);
2258                     result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
2259                     free(chrooted_path);
2260                     return result;
2261                 }
2262             }
2263         }
2264         else
2265         {
2266             strcpy(newname, path);
2267 
2268             if (attr->rename.disable_suffix)
2269             {
2270                 if (!JoinSuffix(newname, sizeof(newname), attr->rename.disable_suffix))
2271                 {
2272                     RecordFailure(ctx, pp, attr,
2273                                   "Failed to append disable suffix '%s' in rename operation for '%s'",
2274                                   attr->rename.disable_suffix, path);
2275                     result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
2276                     free(chrooted_path);
2277                     return result;
2278                 }
2279             }
2280             else
2281             {
2282                 if (!JoinSuffix(newname, sizeof(newname), ".cfdisabled"))
2283                 {
2284                     RecordFailure(ctx, pp, attr,
2285                                   "Failed to append disable suffix '.cfdisabled' in rename operation for '%s'",
2286                                   path);
2287                     result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
2288                     free(chrooted_path);
2289                     return result;
2290                 }
2291             }
2292         }
2293 
2294         const char *changes_newname = newname;
2295         if (ChrootChanges())
2296         {
2297             changes_newname = ToChangesChroot(newname);
2298         }
2299 
2300         if ((attr->rename.plus != CF_SAMEMODE) && (attr->rename.minus != CF_SAMEMODE))
2301         {
2302             newperm = (sb->st_mode & 07777);
2303             newperm |= attr->rename.plus;
2304             newperm &= ~(attr->rename.minus);
2305         }
2306         else
2307         {
2308             newperm = (mode_t) 0600;
2309         }
2310 
2311         if (MakingChanges(ctx, pp, attr, &result, "rename file '%s' to '%s'", path, newname))
2312         {
2313             if (safe_chmod(changes_path, newperm) == 0)
2314             {
2315                 RecordChange(ctx, pp, attr, "Changed permissions of '%s' to 'mode %04jo'",
2316                              path, (uintmax_t)newperm);
2317                 result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
2318             }
2319             else
2320             {
2321                 RecordFailure(ctx, pp, attr, "Failed to change permissions of '%s' to 'mode %04jo' (%s)",
2322                               path, (uintmax_t)newperm, GetErrorStr());
2323                 result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
2324             }
2325 
2326             if (!FileInRepository(newname))
2327             {
2328                 if (rename(changes_path, changes_newname) == -1)
2329                 {
2330                     RecordFailure(ctx, pp, attr, "Error occurred while renaming '%s'. (rename: %s)",
2331                                   path, GetErrorStr());
2332                     result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
2333                     free(chrooted_path);
2334                     return result;
2335                 }
2336                 else
2337                 {
2338                     if (ChrootChanges())
2339                     {
2340                         RecordFileRenamedInChroot(path, newname);
2341                     }
2342                     RecordChange(ctx, pp, attr, "Disabled file '%s' by renaming to '%s' with mode %04jo",
2343                                  path, newname, (uintmax_t)newperm);
2344                     result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
2345                 }
2346 
2347                 if (ArchiveToRepository(newname, attr))
2348                 {
2349                     /* TODO: ArchiveToRepository() does
2350                      *       Log(LOG_LEVEL_INFO, "Moved '%s' to repository location '%s'", file, destination)
2351                      *       but we should do LogChange() instead. (maybe just add 'char **destination'
2352                      *       argument to the function?) */
2353                     unlink(changes_newname);
2354                 }
2355             }
2356             else
2357             {
2358                 RecordWarning(ctx, pp, attr,
2359                               "Disable required twice? Would overwrite saved copy - changing permissions only");
2360                 result = PromiseResultUpdate(result, PROMISE_RESULT_WARN);
2361             }
2362         }
2363 
2364         free(chrooted_path);
2365         return result;
2366     }
2367 
2368     if (attr->rename.rotate == 0)
2369     {
2370         if (MakingChanges(ctx, pp, attr, &result, "truncate '%s'", path))
2371         {
2372             TruncateFile(changes_path);
2373             RecordChange(ctx, pp, attr, "Truncated '%s'", path);
2374             result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
2375         }
2376         free(chrooted_path);
2377         return result;
2378     }
2379 
2380     if (attr->rename.rotate > 0)
2381     {
2382         if (MakingChanges(ctx, pp, attr, &result, "rotate file '%s' in %d fifo", path, attr->rename.rotate))
2383         {
2384             RotateFiles(changes_path, attr->rename.rotate);
2385             RecordChange(ctx, pp, attr, "Rotated file '%s' in %d fifo", path, attr->rename.rotate);
2386             result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
2387         }
2388 
2389         free(chrooted_path);
2390         return result;
2391     }
2392 
2393     free(chrooted_path);
2394     return result;
2395 }
2396 
VerifyDelete(EvalContext * ctx,const char * path,const struct stat * sb,const Attributes * attr,const Promise * pp)2397 static PromiseResult VerifyDelete(EvalContext *ctx,
2398                                   const char *path, const struct stat *sb,
2399                                   const Attributes *attr, const Promise *pp)
2400 {
2401     assert(attr != NULL);
2402     Log(LOG_LEVEL_VERBOSE, "Verifying file deletions for '%s'", path);
2403 
2404     const char *changes_path = path;
2405     if (ChrootChanges())
2406     {
2407         changes_path = ToChangesChroot(path);
2408     }
2409 
2410     const char *lastnode = ReadLastNode(changes_path);
2411 
2412     PromiseResult result = PROMISE_RESULT_NOOP;
2413     if (MakingChanges(ctx, pp, attr, &result, "delete %s '%s'",
2414                        S_ISDIR(sb->st_mode) ? "directory" : "file", path))
2415     {
2416         if (!S_ISDIR(sb->st_mode))                      /* file,symlink */
2417         {
2418             int ret = unlink(lastnode);
2419             if (ret == -1)
2420             {
2421                 RecordFailure(ctx, pp, attr, "Couldn't unlink '%s' tidying. (unlink: %s)",
2422                               path, GetErrorStr());
2423                 return PROMISE_RESULT_FAIL;
2424             }
2425             else
2426             {
2427                 RecordChange(ctx, pp, attr, "Deleted file '%s'", path);
2428                 return PROMISE_RESULT_CHANGE;
2429             }
2430         }
2431         else                                               /* directory */
2432         {
2433             if (!attr->delete.rmdirs)
2434             {
2435                 RecordNoChange(ctx, pp, attr,
2436                                "Keeping directory '%s' since 'rmdirs' attribute was not specified",
2437                                path);
2438                 return PROMISE_RESULT_NOOP;
2439             }
2440 
2441             if (attr->havedepthsearch && strcmp(path, pp->promiser) == 0)
2442             {
2443                 Log(LOG_LEVEL_DEBUG,
2444                     "Skipping deletion of parent directory for recursive promise '%s', "
2445                     "you must specify separate promise for deleting",
2446                     path);
2447                 return PROMISE_RESULT_NOOP;
2448             }
2449 
2450             int ret = rmdir(lastnode);
2451             if (ret == -1 && errno != EEXIST && errno != ENOTEMPTY)
2452             {
2453                 RecordFailure(ctx, pp, attr, "Delete directory '%s' failed (rmdir: %s)",
2454                               path, GetErrorStr());
2455                 return PROMISE_RESULT_FAIL;
2456             }
2457             else if (ret == -1 &&
2458                      (errno == EEXIST || errno == ENOTEMPTY))
2459             {
2460                 /* It's never allowed to delete non-empty directories, they
2461                  * are silently skipped. */
2462                 Log(LOG_LEVEL_VERBOSE,
2463                     "Delete directory '%s' not empty, skipping", path);
2464                 return PROMISE_RESULT_NOOP;
2465             }
2466             else
2467             {
2468                 assert(ret != -1);
2469                 RecordChange(ctx, pp, attr, "Deleted directory '%s'", path);
2470                 return PROMISE_RESULT_CHANGE;
2471             }
2472         }
2473     }
2474 
2475     return result;
2476 }
2477 
TouchFile(EvalContext * ctx,char * path,const Attributes * attr,const Promise * pp)2478 static PromiseResult TouchFile(EvalContext *ctx, char *path, const Attributes *attr, const Promise *pp)
2479 {
2480     PromiseResult result = PROMISE_RESULT_NOOP;
2481     if (MakingChanges(ctx, pp, attr, &result, "update time stamps for '%s'", path))
2482     {
2483         if (utime(ToChangesPath(path), NULL) != -1)
2484         {
2485             RecordChange(ctx, pp, attr, "Touched (updated time stamps) for path '%s'", path);
2486             result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
2487         }
2488         else
2489         {
2490             RecordFailure(ctx, pp, attr, "Touch '%s' failed to update timestamps. (utime: %s)",
2491                           path, GetErrorStr());
2492             result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
2493         }
2494     }
2495 
2496     return result;
2497 }
2498 
VerifyFileAttributes(EvalContext * ctx,const char * file,const struct stat * dstat,const Attributes * attr,const Promise * pp)2499 static PromiseResult VerifyFileAttributes(EvalContext *ctx, const char *file, const struct stat *dstat, const Attributes *attr, const Promise *pp)
2500 {
2501     PromiseResult result = PROMISE_RESULT_NOOP;
2502 
2503 #ifndef __MINGW32__
2504     mode_t newperm = dstat->st_mode, maskvalue;
2505 
2506 # if defined HAVE_CHFLAGS
2507     u_long newflags;
2508 # endif
2509 
2510     maskvalue = umask(0);       /* This makes the DEFAULT modes absolute */
2511 
2512     newperm = (dstat->st_mode & 07777);
2513 
2514     if ((attr->perms.plus != CF_SAMEMODE) && (attr->perms.minus != CF_SAMEMODE))
2515     {
2516         newperm |= attr->perms.plus;
2517         newperm &= ~(attr->perms.minus);
2518         /* directories must have x set if r set, regardless  */
2519 
2520         if (S_ISDIR(dstat->st_mode))
2521         {
2522             if (attr->perms.rxdirs)
2523             {
2524                 Log(LOG_LEVEL_DEBUG, "Directory...fixing x bits");
2525 
2526                 if (newperm & S_IRUSR)
2527                 {
2528                     newperm |= S_IXUSR;
2529                 }
2530 
2531                 if (newperm & S_IRGRP)
2532                 {
2533                     newperm |= S_IXGRP;
2534                 }
2535 
2536                 if (newperm & S_IROTH)
2537                 {
2538                     newperm |= S_IXOTH;
2539                 }
2540             }
2541             else
2542             {
2543                 Log(LOG_LEVEL_VERBOSE, "NB: rxdirs is set to false - x for r bits not checked");
2544             }
2545         }
2546     }
2547 
2548     result = PromiseResultUpdate(result, VerifySetUidGid(ctx, file, dstat, newperm, pp, attr));
2549 
2550 # ifdef __APPLE__
2551     if (VerifyFinderType(ctx, file, attr, pp, &result))
2552     {
2553         /* nop */
2554     }
2555 # endif
2556 #endif
2557 
2558     if (VerifyOwner(ctx, file, pp, attr, dstat, &result))
2559     {
2560         /* nop */
2561     }
2562 
2563 #ifdef __MINGW32__
2564     if (NovaWin_FileExists(file) && !NovaWin_IsDir(file))
2565 #else
2566     if (attr->havechange && S_ISREG(dstat->st_mode))
2567 #endif
2568     {
2569         result = PromiseResultUpdate(result, VerifyFileIntegrity(ctx, file, attr, pp));
2570     }
2571 
2572     if (attr->havechange)
2573     {
2574         VerifyFileChanges(ctx, file, dstat, attr, pp, &result);
2575     }
2576 
2577 #ifndef __MINGW32__
2578     if (S_ISLNK(dstat->st_mode))        /* No point in checking permission on a link */
2579     {
2580         KillGhostLink(ctx, file, attr, pp, &result);
2581         umask(maskvalue);
2582         return result;
2583     }
2584 #endif
2585 
2586     const char *changes_file = file;
2587 
2588 #ifndef __MINGW32__
2589     result = PromiseResultUpdate(result, VerifySetUidGid(ctx, file, dstat, dstat->st_mode, pp, attr));
2590 
2591     if (ChrootChanges())
2592     {
2593         changes_file = ToChangesChroot(file);
2594     }
2595 
2596     if ((newperm & 07777) == (dstat->st_mode & 07777))  /* file okay */
2597     {
2598         Log(LOG_LEVEL_DEBUG, "File okay, newperm '%jo', stat '%jo'",
2599             (uintmax_t) (newperm & 07777), (uintmax_t) (dstat->st_mode & 07777));
2600         RecordNoChange(ctx, pp, attr, "File permissions on '%s' as promised", file);
2601         result = PromiseResultUpdate(result, PROMISE_RESULT_NOOP);
2602     }
2603     else
2604     {
2605         Log(LOG_LEVEL_DEBUG, "Trying to fix mode...newperm '%jo', stat '%jo'",
2606             (uintmax_t) (newperm & 07777), (uintmax_t) (dstat->st_mode & 07777));
2607 
2608         if (MakingChanges(ctx, pp, attr, &result, "change permissions of '%s' from %04jo to %04jo",
2609                           file, (uintmax_t)dstat->st_mode & 07777, (uintmax_t)newperm & 07777))
2610         {
2611             if (safe_chmod(changes_file, newperm & 07777) == -1)
2612             {
2613                 RecordFailure(ctx, pp, attr, "Failed to change permissions of '%s'. (chmod: %s)",
2614                               file, GetErrorStr());
2615                 result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
2616             }
2617             else
2618             {
2619                 RecordChange(ctx, pp, attr, "Object '%s' had permissions %04jo, changed it to %04jo",
2620                              file, (uintmax_t)dstat->st_mode & 07777, (uintmax_t)newperm & 07777);
2621                 result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
2622             }
2623         }
2624     }
2625 
2626 # if defined HAVE_CHFLAGS       /* BSD special flags */
2627 
2628     newflags = (dstat->st_flags & CHFLAGS_MASK);
2629     newflags |= attr->perms.plus_flags;
2630     newflags &= ~(attr->perms.minus_flags);
2631 
2632     if ((newflags & CHFLAGS_MASK) == (dstat->st_flags & CHFLAGS_MASK))
2633     {
2634         RecordNoChange(ctx, pp, attr,
2635                        "BSD flags of '%s' are as promised ('%jx')",
2636                        file, (uintmax_t) (dstat->st_flags & CHFLAGS_MASK));
2637     }
2638     else
2639     {
2640         Log(LOG_LEVEL_DEBUG, "BSD Fixing '%s', newflags '%jx', flags '%jx'",
2641                 file, (uintmax_t) (newflags & CHFLAGS_MASK),
2642                 (uintmax_t) (dstat->st_flags & CHFLAGS_MASK));
2643 
2644         if (MakingChanges(ctx, pp, attr, &result,
2645                           "change BSD flags of '%s' from %jo to %jo",
2646                           file, (uintmax_t) (dstat->st_mode & CHFLAGS_MASK),
2647                           (uintmax_t) (newflags & CHFLAGS_MASK)))
2648         {
2649             if (chflags(changes_file, newflags & CHFLAGS_MASK) == -1)
2650             {
2651                 RecordDenial(ctx, pp, attr,
2652                              "Failed setting BSD flags '%jx' on '%s'. (chflags: %s)",
2653                              (uintmax_t) newflags, file, GetErrorStr());
2654                 result = PromiseResultUpdate(result, PROMISE_RESULT_DENIED);
2655             }
2656             else
2657             {
2658                 RecordChange(ctx, pp, attr, "'%s' had flags %jo, changed it to %jo",
2659                              file,
2660                              (uintmax_t) (dstat->st_flags & CHFLAGS_MASK),
2661                              (uintmax_t) (newflags & CHFLAGS_MASK));
2662                 result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
2663             }
2664         }
2665     }
2666 # endif
2667 #endif
2668 
2669     if (attr->acl.acl_entries)
2670     {
2671         result = PromiseResultUpdate(result, VerifyACL(ctx, file, attr, pp));
2672     }
2673 
2674     /* Need to refresh 'changes_file' here because VerifyACL() above modifies
2675      * the internal buffer used by ToChangesChroot(). */
2676     if (ChrootChanges())
2677     {
2678         changes_file = ToChangesChroot(file);
2679     }
2680 
2681     if (attr->touch)
2682     {
2683         if (utime(changes_file, NULL) == -1)
2684         {
2685             RecordDenial(ctx, pp, attr, "Updating timestamps on '%s' failed. (utime: %s)",
2686                          file, GetErrorStr());
2687             result = PromiseResultUpdate(result, PROMISE_RESULT_DENIED);
2688         }
2689         else
2690         {
2691             RecordChange(ctx, pp, attr, "Updated timestamps on '%s'", file);
2692             result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
2693         }
2694     }
2695 
2696 #ifndef __MINGW32__
2697     umask(maskvalue);
2698 #endif
2699 
2700     return result;
2701 }
2702 
DepthSearch(EvalContext * ctx,char * name,const struct stat * sb,int rlevel,const Attributes * attr,const Promise * pp,dev_t rootdevice,PromiseResult * result)2703 bool DepthSearch(EvalContext *ctx, char *name, const struct stat *sb, int rlevel, const Attributes *attr,
2704                 const Promise *pp, dev_t rootdevice, PromiseResult *result)
2705 {
2706     assert(attr != NULL);
2707     Dir *dirh;
2708     int goback;
2709     const struct dirent *dirp;
2710     struct stat lsb;
2711     Seq *db_file_set = NULL;
2712     Seq *selected_files = NULL;
2713     bool retval = true;
2714 
2715     if (!attr->havedepthsearch)  /* if the search is trivial, make sure that we are in the parent dir of the leaf */
2716     {
2717         char basedir[CF_BUFSIZE];
2718 
2719         Log(LOG_LEVEL_DEBUG, "Direct file reference '%s', no search implied", name);
2720         strlcpy(basedir, ToChangesPath(name), sizeof(basedir));
2721         ChopLastNode(basedir);
2722         if (safe_chdir(basedir))
2723         {
2724             Log(LOG_LEVEL_ERR, "Failed to chdir into '%s'. (chdir: '%s')", basedir, GetErrorStr());
2725             return false;
2726         }
2727         if (!attr->haveselect || SelectLeaf(ctx, name, sb, &(attr->select)))
2728         {
2729             /* Renames are handled separately. */
2730             if ((EVAL_MODE == EVAL_MODE_SIMULATE_MANIFEST_FULL) && !attr->haverename)
2731             {
2732                 RecordFileEvaluatedInChroot(name);
2733             }
2734 
2735             VerifyFileLeaf(ctx, name, sb, attr, pp, result);
2736             return true;
2737         }
2738         else
2739         {
2740             return false;
2741         }
2742     }
2743 
2744     if (rlevel > CF_RECURSION_LIMIT)
2745     {
2746         RecordWarning(ctx, pp, attr,
2747                       "Very deep nesting of directories (>%d deep) for '%s' (Aborting files)",
2748                       rlevel, name);
2749         *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
2750         return false;
2751     }
2752 
2753     if (!PushDirState(ctx, pp, attr, name, sb, result))
2754     {
2755         /* PushDirState() reports errors and updates 'result' in case of failures */
2756         return false;
2757     }
2758 
2759     if ((dirh = DirOpen(".")) == NULL)
2760     {
2761         Log(LOG_LEVEL_INFO, "Could not open existing directory '%s'. (opendir: %s)", name, GetErrorStr());
2762         return false;
2763     }
2764 
2765     if (attr->havechange)
2766     {
2767         db_file_set = SeqNew(1, &free);
2768         if (!FileChangesGetDirectoryList(name, db_file_set))
2769         {
2770             RecordFailure(ctx, pp, attr,
2771                           "Failed to get directory listing for recording file changes in '%s'", name);
2772             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
2773             SeqDestroy(db_file_set);
2774             return false;
2775         }
2776         selected_files = SeqNew(1, &free);
2777     }
2778 
2779     char path[CF_BUFSIZE];
2780     for (dirp = DirRead(dirh); dirp != NULL; dirp = DirRead(dirh))
2781     {
2782         if (!ConsiderLocalFile(dirp->d_name, name))
2783         {
2784             continue;
2785         }
2786 
2787         size_t total_len = strlcpy(path, name, sizeof(path));
2788         if ((total_len >= sizeof(path)) || (JoinPaths(path, sizeof(path), dirp->d_name) == NULL))
2789         {
2790             RecordFailure(ctx, pp, attr,
2791                           "Internal limit reached in DepthSearch(), path too long: '%s' + '%s'",
2792                           path, dirp->d_name);
2793             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
2794             retval = false;
2795             goto end;
2796         }
2797 
2798         if (lstat(dirp->d_name, &lsb) == -1)
2799         {
2800             Log(LOG_LEVEL_VERBOSE, "Recurse was looking at '%s' when an error occurred. (lstat: %s)", path, GetErrorStr());
2801             continue;
2802         }
2803 
2804         if (S_ISLNK(lsb.st_mode))       /* should we ignore links? */
2805         {
2806             if (KillGhostLink(ctx, path, attr, pp, result))
2807             {
2808                 if (ChrootChanges())
2809                 {
2810                     RecordFileChangedInChroot(path);
2811                 }
2812                 continue;
2813             }
2814         }
2815 
2816         /* See if we are supposed to treat links to dirs as dirs and descend */
2817 
2818         if ((attr->recursion.travlinks) && (S_ISLNK(lsb.st_mode)))
2819         {
2820             if ((lsb.st_uid != 0) && (lsb.st_uid != getuid()))
2821             {
2822                 Log(LOG_LEVEL_INFO,
2823                     "File '%s' is an untrusted link: cfengine will not follow it with a destructive operation", path);
2824                 continue;
2825             }
2826 
2827             /* if so, hide the difference by replacing with actual object */
2828 
2829             if (stat(dirp->d_name, &lsb) == -1)
2830             {
2831                 RecordFailure(ctx, pp, attr,
2832                               "Recurse was working on '%s' when this failed. (stat: %s)",
2833                               path, GetErrorStr());
2834                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
2835                 continue;
2836             }
2837         }
2838 
2839         if ((attr->recursion.xdev) && (DeviceBoundary(&lsb, rootdevice)))
2840         {
2841             Log(LOG_LEVEL_VERBOSE, "Skipping '%s' on different device - use xdev option to change this. (stat: %s)", path, GetErrorStr());
2842             continue;
2843         }
2844 
2845         if (S_ISDIR(lsb.st_mode))
2846         {
2847             if (SkipDirLinks(ctx, path, dirp->d_name, attr->recursion))
2848             {
2849                 continue;
2850             }
2851 
2852             if ((attr->recursion.depth > 1) && (rlevel <= attr->recursion.depth))
2853             {
2854                 Log(LOG_LEVEL_VERBOSE, "Entering '%s', level %d", path, rlevel);
2855                 goback = DepthSearch(ctx, path, &lsb, rlevel + 1, attr, pp, rootdevice, result);
2856                 if (!PopDirState(ctx, pp, attr, goback, name, sb, attr->recursion, result))
2857                 {
2858                     FatalError(ctx, "Not safe to continue");
2859                 }
2860             }
2861         }
2862 
2863         if (!attr->haveselect || SelectLeaf(ctx, path, &lsb, &(attr->select)))
2864         {
2865             if (attr->havechange)
2866             {
2867                 if (!SeqBinaryLookup(db_file_set, dirp->d_name, StrCmpWrapper))
2868                 {
2869                     // See comments in FileChangesCheckAndUpdateDirectory(),
2870                     // regarding this function call.
2871                     FileChangesLogNewFile(path, pp);
2872                 }
2873                 SeqAppend(selected_files, xstrdup(dirp->d_name));
2874             }
2875 
2876             VerifyFileLeaf(ctx, path, &lsb, attr, pp, result);
2877 
2878             /* Renames are handled separately. */
2879             if ((EVAL_MODE == EVAL_MODE_SIMULATE_MANIFEST_FULL) && !attr->haverename)
2880             {
2881                 RecordFileEvaluatedInChroot(path);
2882             }
2883             if (ChrootChanges() && (*result == PROMISE_RESULT_CHANGE))
2884             {
2885                 RecordFileChangedInChroot(path);
2886             }
2887         }
2888         else
2889         {
2890             Log(LOG_LEVEL_DEBUG, "Skipping non-selected file '%s'", path);
2891         }
2892     }
2893 
2894     if (attr->havechange)
2895     {
2896         FileChangesCheckAndUpdateDirectory(ctx, attr, name, selected_files, db_file_set,
2897                                            attr->change.update, pp, result);
2898     }
2899 
2900 end:
2901     SeqDestroy(selected_files);
2902     SeqDestroy(db_file_set);
2903     DirClose(dirh);
2904     return retval;
2905 }
2906 
PushDirState(EvalContext * ctx,const Promise * pp,const Attributes * attr,char * name,const struct stat * sb,PromiseResult * result)2907 static bool PushDirState(EvalContext *ctx, const Promise *pp, const Attributes *attr, char *name, const struct stat *sb, PromiseResult *result)
2908 {
2909     const char *changes_name = (ToChangesPath(name));
2910     if (safe_chdir(changes_name) == -1)
2911     {
2912         RecordFailure(ctx, pp, attr, "Could not change to directory '%s' (mode '%04jo', chdir: %s)",
2913                       name, (uintmax_t)(sb->st_mode & 07777), GetErrorStr());
2914         *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
2915         return false;
2916     }
2917 
2918     if (!CheckLinkSecurity(sb, changes_name))
2919     {
2920         FatalError(ctx, "Not safe to continue");
2921     }
2922     return true;
2923 }
2924 
2925 /**
2926  * @return true if safe for agent to continue
2927  */
PopDirState(EvalContext * ctx,const Promise * pp,const Attributes * attr,int goback,char * name,const struct stat * sb,DirectoryRecursion r,PromiseResult * result)2928 static bool PopDirState(EvalContext *ctx, const Promise *pp, const Attributes *attr,
2929                         int goback, char *name, const struct stat *sb, DirectoryRecursion r,
2930                         PromiseResult *result)
2931 {
2932     const char *changes_name = (ToChangesPath(name));
2933     if (goback && (r.travlinks))
2934     {
2935         if (safe_chdir(changes_name) == -1)
2936         {
2937             RecordFailure(ctx, pp, attr,
2938                           "Error in backing out of recursive travlink descent securely to '%s'. (chdir: %s)",
2939                           name, GetErrorStr());
2940             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
2941             return false;
2942         }
2943 
2944         if (!CheckLinkSecurity(sb, changes_name))
2945         {
2946             return false;
2947         }
2948     }
2949     else if (goback)
2950     {
2951         if (safe_chdir("..") == -1)
2952         {
2953             RecordFailure(ctx, pp, attr,
2954                           "Error in backing out of recursive descent securely to '%s'. (chdir: %s)",
2955                           name, GetErrorStr());
2956             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
2957             return false;
2958         }
2959     }
2960 
2961     return true;
2962 }
2963 
2964 /**
2965  * @return true if it is safe for the agent to continue execution
2966  */
CheckLinkSecurity(const struct stat * sb,const char * name)2967 static bool CheckLinkSecurity(const struct stat *sb, const char *name)
2968 {
2969     struct stat security;
2970 
2971     Log(LOG_LEVEL_DEBUG, "Checking the inode and device to make sure we are where we think we are...");
2972 
2973     if (stat(".", &security) == -1)
2974     {
2975         Log(LOG_LEVEL_ERR, "Could not stat directory '%s' after entering. (stat: %s)",
2976             name, GetErrorStr());
2977         return true; // continue anyway
2978     }
2979 
2980     if ((sb->st_dev != security.st_dev) || (sb->st_ino != security.st_ino))
2981     {
2982         Log(LOG_LEVEL_ERR,
2983             "SERIOUS SECURITY ALERT: path race exploited in recursion to/from '%s'. Not safe for agent to continue - aborting",
2984               name);
2985         return false; // too dangerous
2986     }
2987 
2988     return true;
2989 }
2990 
VerifyCopiedFileAttributes(EvalContext * ctx,const char * src,const char * dest,const struct stat * sstat,const struct stat * dstat,const Attributes * a,const Promise * pp)2991 static PromiseResult VerifyCopiedFileAttributes(EvalContext *ctx, const char *src, const char *dest, const struct stat *sstat,
2992                                                 const struct stat *dstat, const Attributes *a, const Promise *pp)
2993 {
2994     assert(a != NULL);
2995     Attributes attr = *a; // TODO: try to remove this copy
2996 #ifndef __MINGW32__
2997     mode_t newplus, newminus;
2998     uid_t save_uid;
2999     gid_t save_gid;
3000 
3001 // If we get here, there is both a src and dest file
3002 
3003     save_uid = (attr.perms.owners)->uid;
3004     save_gid = (attr.perms.groups)->gid;
3005 
3006     if (attr.copy.preserve)
3007     {
3008         Log(LOG_LEVEL_VERBOSE, "Attempting to preserve file permissions from the source: %04jo",
3009               (uintmax_t)(sstat->st_mode & 07777));
3010 
3011         if ((attr.perms.owners)->uid == CF_SAME_OWNER)  /* Preserve uid and gid  */
3012         {
3013             (attr.perms.owners)->uid = sstat->st_uid;
3014         }
3015 
3016         if ((attr.perms.groups)->gid == CF_SAME_GROUP)
3017         {
3018             (attr.perms.groups)->gid = sstat->st_gid;
3019         }
3020 
3021 // Will this preserve if no mode set?
3022 
3023         newplus = (sstat->st_mode & 07777);
3024         newminus = ~newplus & 07777;
3025         attr.perms.plus = newplus;
3026         attr.perms.minus = newminus;
3027     }
3028     else
3029     {
3030         if ((attr.perms.owners)->uid == CF_SAME_OWNER)  /* Preserve uid and gid  */
3031         {
3032             (attr.perms.owners)->uid = dstat->st_uid;
3033         }
3034 
3035         if ((attr.perms.groups)->gid == CF_SAME_GROUP)
3036         {
3037             (attr.perms.groups)->gid = dstat->st_gid;
3038         }
3039 
3040         if (attr.haveperms)
3041         {
3042             newplus = (dstat->st_mode & 07777) | attr.perms.plus;
3043             newminus = ~(newplus & ~(attr.perms.minus)) & 07777;
3044             attr.perms.plus = newplus;
3045             attr.perms.minus = newminus;
3046         }
3047     }
3048 #endif
3049     PromiseResult result = VerifyFileAttributes(ctx, dest, dstat, &attr, pp);
3050 
3051 #ifndef __MINGW32__
3052     (attr.perms.owners)->uid = save_uid;
3053     (attr.perms.groups)->gid = save_gid;
3054 #endif
3055 
3056     const char *changes_src = src;
3057     char *chrooted_src = NULL;
3058     if (ChrootChanges())
3059     {
3060         /* Need to create a copy because the second call will override the
3061          * internal buffer used by ToChangesChroot(). */
3062         chrooted_src = xstrdup(ToChangesChroot(src));
3063         changes_src = chrooted_src;
3064     }
3065     const char *changes_dest = dest;
3066     if (ChrootChanges())
3067     {
3068         changes_dest = ToChangesChroot(dest);
3069     }
3070 
3071     if (attr.copy.preserve &&
3072         (   attr.copy.servers == NULL
3073          || strcmp(RlistScalarValue(attr.copy.servers), "localhost") == 0))
3074     {
3075         bool change = false;
3076         if (!CopyFileExtendedAttributesDisk(changes_src, changes_dest, &change))
3077         {
3078             RecordFailure(ctx, pp, &attr,
3079                           "Could not preserve extended attributes"
3080                           " (ACLs and security contexts) on file '%s'",
3081                           dest);
3082             result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
3083         }
3084         else if (change)
3085         {
3086             RecordChange(ctx, pp, &attr,
3087                          "Copied extended attributes from '%s' to '%s'",
3088                          src, dest);
3089             result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
3090         }
3091     }
3092     free(chrooted_src);
3093 
3094     return result;
3095 }
3096 
CopyFileSources(EvalContext * ctx,char * destination,const Attributes * attr,const Promise * pp,AgentConnection * conn)3097 static PromiseResult CopyFileSources(EvalContext *ctx, char *destination, const Attributes *attr, const Promise *pp, AgentConnection *conn)
3098 {
3099     assert(attr != NULL);
3100     Buffer *source_buf = BufferNew();
3101     // Expand this.promiser
3102     ExpandScalar(ctx,
3103                  PromiseGetBundle(pp)->ns, PromiseGetBundle(pp)->name,
3104                  attr->copy.source, source_buf);
3105     char vbuff[CF_BUFSIZE];
3106     struct stat ssb, dsb;
3107     struct timespec start;
3108 
3109     const char *source = BufferData(source_buf);
3110     const char *changes_source = source;
3111     if (ChrootChanges())
3112     {
3113         changes_source = ToChangesChroot(changes_source);
3114     }
3115     if (conn != NULL && (!conn->authenticated))
3116     {
3117         RecordFailure(ctx, pp, attr, "Source '%s' not authenticated in copy_from for '%s'",
3118                       source, destination);
3119         BufferDestroy(source_buf);
3120         return PROMISE_RESULT_FAIL;
3121     }
3122 
3123     if (cf_stat(changes_source, &ssb, &(attr->copy), conn) == -1)
3124     {
3125         if (attr->copy.missing_ok)
3126         {
3127             RecordNoChange(ctx, pp, attr,
3128                            "Can't stat file '%s' on '%s' but promise is kept because of"
3129                            " 'missing_ok' in files.copy_from promise",
3130                            source, conn ? conn->remoteip : "localhost");
3131             BufferDestroy(source_buf);
3132             return PROMISE_RESULT_NOOP;
3133         }
3134         else
3135         {
3136             RecordFailure(ctx, pp, attr,
3137                           "Can't stat file '%s' on '%s' in files.copy_from promise",
3138                           source, conn ? conn->remoteip : "localhost");
3139             BufferDestroy(source_buf);
3140             return PROMISE_RESULT_FAIL;
3141         }
3142     }
3143 
3144     start = BeginMeasure();
3145 
3146     strlcpy(vbuff, destination, CF_BUFSIZE - 3);
3147 
3148     if (S_ISDIR(ssb.st_mode))   /* could be depth_search */
3149     {
3150         AddSlash(vbuff);
3151         strcat(vbuff, ".");
3152     }
3153 
3154     PromiseResult result = PROMISE_RESULT_NOOP;
3155     bool dir_created = false;
3156     if (!MakeParentDirectoryForPromise(ctx, pp, attr, &result,
3157                                        vbuff, attr->move_obstructions, &dir_created))
3158     {
3159         BufferDestroy(source_buf);
3160         return result;
3161     }
3162     if (dir_created)
3163     {
3164         RecordChange(ctx, pp, attr, "Created parent directory for '%s'", destination);
3165         result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
3166     }
3167 
3168     CompressedArray *inode_cache = NULL;
3169     if (S_ISDIR(ssb.st_mode))   /* could be depth_search */
3170     {
3171         if (attr->copy.purge)
3172         {
3173             Log(LOG_LEVEL_VERBOSE, "Destination purging enabled");
3174         }
3175 
3176         Log(LOG_LEVEL_VERBOSE, "Entering directory '%s'", source);
3177 
3178         result = PromiseResultUpdate(
3179             result, SourceSearchAndCopy(ctx, source, destination,
3180                                         attr->recursion.depth, attr, pp,
3181                                         ssb.st_dev, &inode_cache, conn));
3182 
3183         if (stat(ToChangesPath(destination), &dsb) != -1)
3184         {
3185             if (attr->copy.check_root)
3186             {
3187                 result = PromiseResultUpdate(
3188                     result, VerifyCopiedFileAttributes(ctx, source,
3189                                                        destination, &ssb, &dsb,
3190                                                        attr, pp));
3191             }
3192         }
3193     }
3194     else
3195     {
3196         result = PromiseResultUpdate(
3197             result, VerifyCopy(ctx, source, destination,
3198                                attr, pp, &inode_cache, conn));
3199     }
3200 
3201     DeleteCompressedArray(inode_cache);
3202 
3203     const char *mid = PromiseGetConstraintAsRval(pp, "measurement_class", RVAL_TYPE_SCALAR);
3204     if (mid)
3205     {
3206         char eventname[CF_BUFSIZE];
3207         snprintf(eventname, CF_BUFSIZE - 1, "Copy(%s:%s > %s)",
3208                  conn ? conn->this_server : "localhost",
3209                  source, destination);
3210 
3211         EndMeasure(eventname, start);
3212     }
3213     else
3214     {
3215         EndMeasure(NULL, start);
3216     }
3217 
3218     BufferDestroy(source_buf);
3219     return result;
3220 }
3221 
3222 /* Decide the protocol version the agent will use to connect: If the user has
3223  * specified a copy_from attribute then follow that one, else use the body
3224  * common control setting. */
DecideProtocol(const EvalContext * ctx,ProtocolVersion copyfrom_setting)3225 static ProtocolVersion DecideProtocol(const EvalContext *ctx,
3226                                       ProtocolVersion copyfrom_setting)
3227 {
3228     if (copyfrom_setting == CF_PROTOCOL_UNDEFINED)
3229     {
3230         /* TODO we would like to get the common control setting from
3231          * GenericAgentConfig. Given that we have only access to EvalContext here,
3232          * we get the raw string and reparse it every time. */
3233         const char *s = EvalContextVariableControlCommonGet(
3234             ctx, COMMON_CONTROL_PROTOCOL_VERSION);
3235         ProtocolVersion common_setting = ProtocolVersionParse(s);
3236         return common_setting;
3237     }
3238     else
3239     {
3240         return copyfrom_setting;
3241     }
3242 }
3243 
FileCopyConnectionOpen(const EvalContext * ctx,const char * servername,const FileCopy * fc,bool background)3244 static AgentConnection *FileCopyConnectionOpen(const EvalContext *ctx,
3245                                                const char *servername,
3246                                                const FileCopy *fc, bool background)
3247 {
3248     ConnectionFlags flags = {
3249         .protocol_version = DecideProtocol(ctx, fc->protocol_version),
3250         .cache_connection = !background,
3251         .force_ipv4 = fc->force_ipv4,
3252         .trust_server = fc->trustkey,
3253         .off_the_record = false
3254     };
3255 
3256     unsigned int conntimeout = fc->timeout;
3257     if (fc->timeout == CF_NOINT || fc->timeout < 0)
3258     {
3259         conntimeout = CONNTIMEOUT;
3260     }
3261 
3262     const char *port = (fc->port != NULL) ? fc->port : CFENGINE_PORT_STR;
3263 
3264     AgentConnection *conn = NULL;
3265     if (flags.cache_connection)
3266     {
3267         conn = ConnCache_FindIdleMarkBusy(servername, port, flags);
3268 
3269         if (conn != NULL)                 /* found idle connection in cache */
3270         {
3271             return conn;
3272         }
3273         else                    /* not found, open and cache new connection */
3274         {
3275             int err = 0;
3276             conn = ServerConnection(servername, port, conntimeout,
3277                                     flags, &err);
3278 
3279             /* WARNING: if cache already has non-idle connections to that
3280              * host, here we add more so that we connect in parallel. */
3281 
3282             if (conn == NULL)                           /* Couldn't connect */
3283             {
3284                 /* Allocate and add to the cache as failure. */
3285                 conn = NewAgentConn(servername, port, flags);
3286                 conn->conn_info->status = CONNECTIONINFO_STATUS_NOT_ESTABLISHED;
3287 
3288                 ConnCache_Add(conn, CONNCACHE_STATUS_OFFLINE);
3289 
3290                 return NULL;
3291             }
3292             else
3293             {
3294                 /* Success! Put it in the cache as busy. */
3295                 ConnCache_Add(conn, CONNCACHE_STATUS_BUSY);
3296                 return conn;
3297             }
3298         }
3299     }
3300     else
3301     {
3302         int err = 0;
3303         conn = ServerConnection(servername, port, conntimeout,
3304                                 flags, &err);
3305         return conn;
3306     }
3307 }
3308 
FileCopyConnectionClose(AgentConnection * conn)3309 void FileCopyConnectionClose(AgentConnection *conn)
3310 {
3311     if (conn->flags.cache_connection)
3312     {
3313         /* Mark the connection as available in the cache. */
3314         ConnCache_MarkNotBusy(conn);
3315     }
3316     else
3317     {
3318         DisconnectServer(conn);
3319     }
3320 }
3321 
ScheduleCopyOperation(EvalContext * ctx,char * destination,const Attributes * attr,const Promise * pp)3322 PromiseResult ScheduleCopyOperation(EvalContext *ctx, char *destination, const Attributes *attr, const Promise *pp)
3323 {
3324     assert(attr != NULL);
3325     /* TODO currently parser allows body copy_from to have no source!
3326        See tests/acceptance/10_files/02_maintain/017.cf and
3327        https://cfengine.com/bugtracker/view.php?id=687 */
3328     if (attr->copy.source == NULL)
3329     {
3330         RecordFailure(ctx, pp, attr, "Body copy_from for '%s' has no source", destination);
3331         return PROMISE_RESULT_FAIL;
3332     }
3333 
3334     Log(LOG_LEVEL_VERBOSE, "File '%s' copy_from '%s'",
3335         destination, attr->copy.source);
3336 
3337     /* Empty attr->copy.servers means copy from localhost. */
3338     bool copyfrom_localhost = (attr->copy.servers == NULL);
3339     AgentConnection *conn = NULL;
3340     Rlist *rp = attr->copy.servers;
3341 
3342     /* Iterate over all copy_from servers until connection succeeds. */
3343     while (rp != NULL && conn == NULL)
3344     {
3345         const char *servername = RlistScalarValue(rp);
3346 
3347         if (strcmp(servername, "localhost") == 0)
3348         {
3349             copyfrom_localhost = true;
3350             break;
3351         }
3352 
3353         conn = FileCopyConnectionOpen(ctx, servername, &(attr->copy),
3354                                       attr->transaction.background);
3355         if (conn == NULL)
3356         {
3357             Log(LOG_LEVEL_INFO, "Unable to establish connection to '%s'",
3358                 servername);
3359         }
3360 
3361         rp = rp->next;
3362     }
3363 
3364     if (!copyfrom_localhost && conn == NULL)
3365     {
3366         RecordFailure(ctx, pp, attr, "No suitable server found for '%s'", destination);
3367         PromiseRef(LOG_LEVEL_ERR, pp);
3368         return PROMISE_RESULT_FAIL;
3369     }
3370 
3371     /* (conn == NULL) means local copy. */
3372     PromiseResult result = CopyFileSources(ctx, destination, attr, pp, conn);
3373 
3374     if (conn != NULL)
3375     {
3376         FileCopyConnectionClose(conn);
3377     }
3378 
3379     return result;
3380 }
3381 
ScheduleLinkOperation(EvalContext * ctx,char * destination,char * source,const Attributes * attr,const Promise * pp)3382 PromiseResult ScheduleLinkOperation(EvalContext *ctx, char *destination, char *source, const Attributes *attr, const Promise *pp)
3383 {
3384     assert(attr != NULL);
3385     const char *lastnode;
3386 
3387     lastnode = ReadLastNode(destination);
3388     PromiseResult result = PROMISE_RESULT_NOOP;
3389 
3390     if (MatchRlistItem(ctx, attr->link.copy_patterns, lastnode))
3391     {
3392         Log(LOG_LEVEL_VERBOSE, "Link '%s' matches copy_patterns", destination);
3393         CompressedArray *inode_cache = NULL;
3394         result = PromiseResultUpdate(result, VerifyCopy(ctx, attr->link.source, destination, attr, pp, &inode_cache, NULL));
3395         DeleteCompressedArray(inode_cache);
3396         return result;
3397     }
3398 
3399     switch (attr->link.link_type)
3400     {
3401     case FILE_LINK_TYPE_SYMLINK:
3402         result = VerifyLink(ctx, destination, source, attr, pp);
3403         break;
3404     case FILE_LINK_TYPE_HARDLINK:
3405         result = VerifyHardLink(ctx, destination, source, attr, pp);
3406         break;
3407     case FILE_LINK_TYPE_RELATIVE:
3408         result = VerifyRelativeLink(ctx, destination, source, attr, pp);
3409         break;
3410     case FILE_LINK_TYPE_ABSOLUTE:
3411         result = VerifyAbsoluteLink(ctx, destination, source, attr, pp);
3412         break;
3413     default:
3414         assert(false);
3415         Log(LOG_LEVEL_ERR, "Unknown link type - should not happen.");
3416         break;
3417     }
3418 
3419     return result;
3420 }
3421 
ScheduleLinkChildrenOperation(EvalContext * ctx,char * destination,char * source,int recurse,const Attributes * a,const Promise * pp)3422 PromiseResult ScheduleLinkChildrenOperation(EvalContext *ctx, char *destination, char *source, int recurse, const Attributes *a,
3423                                             const Promise *pp)
3424 {
3425     assert(a != NULL);
3426     Attributes attr = *a; // TODO: Remove this copy
3427     Dir *dirh;
3428     const struct dirent *dirp;
3429     char promiserpath[CF_BUFSIZE], sourcepath[CF_BUFSIZE];
3430     struct stat lsb;
3431     int ret;
3432 
3433     const char *changes_destination = destination;
3434     if (ChrootChanges())
3435     {
3436         changes_destination = ToChangesChroot(destination);
3437     }
3438 
3439     PromiseResult result = PROMISE_RESULT_NOOP;
3440     if ((ret = lstat(changes_destination, &lsb)) != -1)
3441     {
3442         if (attr.move_obstructions && S_ISLNK(lsb.st_mode))
3443         {
3444             if (unlink(changes_destination) == 0)
3445             {
3446                 RecordChange(ctx, pp, a, "Removed obstructing link '%s'", destination);
3447                 result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
3448             }
3449             else
3450             {
3451                 RecordFailure(ctx, pp, a, "Failed to remove obstructing link '%s'", destination);
3452                 result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
3453             }
3454         }
3455         else if (!S_ISDIR(lsb.st_mode))
3456         {
3457             RecordFailure(ctx, pp, a,
3458                           "Cannot promise to link files to children of '%s' as it is not a directory",
3459                           destination);
3460             result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
3461             return result;
3462         }
3463     }
3464 
3465     snprintf(promiserpath, sizeof(promiserpath), "%s/.", destination);
3466 
3467     if ((ret == -1) && !CfCreateFile(ctx, promiserpath, pp, &attr, &result))
3468     {
3469         RecordFailure(ctx, pp, a,
3470                       "Failed to create directory '%s' for linking files as its children",
3471                       destination);
3472         result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
3473         return result;
3474     }
3475 
3476     if (ChrootChanges())
3477     {
3478         /* If making changes in a chroot, we need to make sure 'source' is in
3479          * the chroot before we start. */
3480         PrepareChangesChroot(source);
3481     }
3482 
3483     if ((dirh = DirOpen(ToChangesPath(source))) == NULL)
3484     {
3485         RecordFailure(ctx, pp, a,
3486                       "Can't open source of children to link '%s'. (opendir: %s)",
3487                       attr.link.source, GetErrorStr());
3488         result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
3489         return result;
3490     }
3491 
3492     for (dirp = DirRead(dirh); dirp != NULL; dirp = DirRead(dirh))
3493     {
3494         if (!ConsiderLocalFile(dirp->d_name, source))
3495         {
3496             Log(LOG_LEVEL_VERBOSE, "Skipping '%s'", dirp->d_name);
3497             continue;
3498         }
3499 
3500         /* Assemble pathnames */
3501 
3502         strlcpy(promiserpath, destination, sizeof(promiserpath));
3503         if (JoinPaths(promiserpath, sizeof(promiserpath), dirp->d_name) == NULL)
3504         {
3505             RecordInterruption(ctx, pp, a,
3506                               "Internal buffer limit while verifying child links,"
3507                               " promiser: '%s' + '%s'",
3508                               promiserpath, dirp->d_name);
3509             result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
3510             DirClose(dirh);
3511             return result;
3512         }
3513 
3514         strlcpy(sourcepath, source, sizeof(sourcepath));
3515         if (JoinPaths(sourcepath, sizeof(sourcepath), dirp->d_name) == NULL)
3516         {
3517             RecordInterruption(ctx, pp, a,
3518                                "Internal buffer limit while verifying child links,"
3519                                " source filename: '%s' + '%s'",
3520                                sourcepath, dirp->d_name);
3521             result = PromiseResultUpdate(result, PROMISE_RESULT_INTERRUPTED);
3522             DirClose(dirh);
3523             return result;
3524         }
3525 
3526         if ((lstat(ToChangesPath(promiserpath), &lsb) != -1) && !S_ISLNK(lsb.st_mode) && !S_ISDIR(lsb.st_mode))
3527         {
3528             if (attr.link.when_linking_children == cfa_override)
3529             {
3530                 attr.move_obstructions = true;
3531             }
3532             else
3533             {
3534                 Log(LOG_LEVEL_VERBOSE, "Have promised not to disturb existing content belonging to '%s'", promiserpath);
3535                 continue;
3536             }
3537         }
3538 
3539         if ((attr.recursion.depth > recurse) && (lstat(ToChangesPath(sourcepath), &lsb) != -1) && S_ISDIR(lsb.st_mode))
3540         {
3541             result = PromiseResultUpdate(result,
3542                                          ScheduleLinkChildrenOperation(ctx, promiserpath, sourcepath,
3543                                                                        recurse + 1, &attr, pp));
3544         }
3545         else
3546         {
3547             result = PromiseResultUpdate(result, ScheduleLinkOperation(ctx, promiserpath, sourcepath,
3548                                                                        &attr, pp));
3549         }
3550         if (ChrootChanges() && (result == PROMISE_RESULT_CHANGE))
3551         {
3552             RecordFileChangedInChroot(promiserpath);
3553         }
3554     }
3555 
3556     DirClose(dirh);
3557     return result;
3558 }
3559 
VerifyFileIntegrity(EvalContext * ctx,const char * file,const Attributes * attr,const Promise * pp)3560 static PromiseResult VerifyFileIntegrity(EvalContext *ctx, const char *file, const Attributes *attr, const Promise *pp)
3561 {
3562     assert(attr != NULL);
3563     unsigned char digest1[EVP_MAX_MD_SIZE + 1];
3564     unsigned char digest2[EVP_MAX_MD_SIZE + 1];
3565 
3566     if ((attr->change.report_changes != FILE_CHANGE_REPORT_CONTENT_CHANGE) && (attr->change.report_changes != FILE_CHANGE_REPORT_ALL))
3567     {
3568         return PROMISE_RESULT_NOOP;
3569     }
3570 
3571     memset(digest1, 0, EVP_MAX_MD_SIZE + 1);
3572     memset(digest2, 0, EVP_MAX_MD_SIZE + 1);
3573 
3574     PromiseResult result = PROMISE_RESULT_NOOP;
3575     bool changed = false;
3576     if (attr->change.hash == HASH_METHOD_BEST)
3577     {
3578         HashFile(file, digest1, HASH_METHOD_MD5, false);
3579         HashFile(file, digest2, HASH_METHOD_SHA1, false);
3580 
3581         changed = (changed ||
3582                    FileChangesCheckAndUpdateHash(ctx, file, digest1, HASH_METHOD_MD5, attr, pp, &result));
3583         changed = (changed ||
3584                    FileChangesCheckAndUpdateHash(ctx, file, digest2, HASH_METHOD_SHA1, attr, pp, &result));
3585     }
3586     else
3587     {
3588         HashFile(file, digest1, attr->change.hash, false);
3589 
3590         changed = (changed ||
3591                    FileChangesCheckAndUpdateHash(ctx, file, digest1, attr->change.hash, attr, pp, &result));
3592     }
3593 
3594     if (changed && MakingInternalChanges(ctx, pp, attr, &result, "record integrity changes in '%s'", file))
3595     {
3596         EvalContextHeapPersistentSave(ctx, "checksum_alerts", CF_PERSISTENCE, CONTEXT_STATE_POLICY_PRESERVE, "");
3597         EvalContextClassPutSoft(ctx, "checksum_alerts", CONTEXT_SCOPE_NAMESPACE, "");
3598         if (FileChangesLogChange(file, FILE_STATE_CONTENT_CHANGED, "Content changed", pp))
3599         {
3600             RecordChange(ctx, pp, attr, "Recorded integrity changes in '%s'", file);
3601             result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
3602         }
3603         else
3604         {
3605             RecordFailure(ctx, pp, attr, "Failed to record integrity changes in '%s'", file);
3606             result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL);
3607         }
3608     }
3609 
3610     if (attr->change.report_diffs && MakingInternalChanges(ctx, pp, attr, &result, "report diffs in '%s'", file))
3611     {
3612         char destination[CF_BUFSIZE];
3613         if (!GetRepositoryPath(file, attr, destination))
3614         {
3615             destination[0] = '\0';
3616         }
3617         LogFileChange(ctx, file, changed, attr, pp, &CopyRegularFile, destination, &DeleteCompressedArray);
3618     }
3619 
3620     return result;
3621 }
3622 
CompareForFileCopy(char * sourcefile,char * destfile,const struct stat * ssb,const struct stat * dsb,const FileCopy * fc,AgentConnection * conn)3623 static bool CompareForFileCopy(char *sourcefile, char *destfile, const struct stat *ssb, const struct stat *dsb, const FileCopy *fc, AgentConnection *conn)
3624 {
3625     bool ok_to_copy;
3626 
3627     /* (conn == NULL) means copy is from localhost. */
3628     const char *changes_sourcefile = sourcefile;
3629     char *chrooted_sourcefile = NULL;
3630     if ((conn == NULL) && ChrootChanges())
3631     {
3632         /* Need to create a copy because the later call to ToChangesChroot()
3633          * overwrites the internal buffer. */
3634         chrooted_sourcefile = xstrdup(ToChangesChroot(sourcefile));
3635         struct stat sb;
3636         if (lstat(changes_sourcefile, &sb) != -1)
3637         {
3638             changes_sourcefile = chrooted_sourcefile;
3639         }
3640     }
3641     const char *changes_destfile = destfile;
3642     if (ChrootChanges())
3643     {
3644         changes_destfile = ToChangesChroot(destfile);
3645     }
3646 
3647     switch (fc->compare)
3648     {
3649     case FILE_COMPARATOR_CHECKSUM:
3650     case FILE_COMPARATOR_HASH:
3651 
3652         if (S_ISREG(dsb->st_mode) && S_ISREG(ssb->st_mode))
3653         {
3654             ok_to_copy = CompareFileHashes(changes_sourcefile, changes_destfile, ssb, dsb, fc, conn);
3655         }
3656         else
3657         {
3658             Log(LOG_LEVEL_VERBOSE, "Checksum comparison replaced by ctime: files not regular");
3659             ok_to_copy = (dsb->st_ctime < ssb->st_ctime) || (dsb->st_mtime < ssb->st_mtime);
3660         }
3661 
3662         if (ok_to_copy)
3663         {
3664             Log(LOG_LEVEL_VERBOSE, "Image file '%s' has a wrong digest/checksum, should be copy of '%s'", destfile,
3665                   sourcefile);
3666             free(chrooted_sourcefile);
3667             return ok_to_copy;
3668         }
3669         break;
3670 
3671     case FILE_COMPARATOR_BINARY:
3672 
3673         if (S_ISREG(dsb->st_mode) && S_ISREG(ssb->st_mode))
3674         {
3675             ok_to_copy = CompareBinaryFiles(changes_sourcefile, changes_destfile, ssb, dsb, fc, conn);
3676         }
3677         else
3678         {
3679             Log(LOG_LEVEL_VERBOSE, "Byte comparison replaced by ctime: files not regular");
3680             ok_to_copy = (dsb->st_ctime < ssb->st_ctime) || (dsb->st_mtime < ssb->st_mtime);
3681         }
3682 
3683         if (ok_to_copy)
3684         {
3685             Log(LOG_LEVEL_VERBOSE, "Image file %s has a wrong binary checksum, should be copy of '%s'", destfile,
3686                   sourcefile);
3687             free(chrooted_sourcefile);
3688             return ok_to_copy;
3689         }
3690         break;
3691 
3692     case FILE_COMPARATOR_MTIME:
3693 
3694         ok_to_copy = (dsb->st_mtime < ssb->st_mtime);
3695 
3696         if (ok_to_copy)
3697         {
3698             Log(LOG_LEVEL_VERBOSE, "Image file '%s' out of date, should be copy of '%s'", destfile, sourcefile);
3699             free(chrooted_sourcefile);
3700             return ok_to_copy;
3701         }
3702         break;
3703 
3704     case FILE_COMPARATOR_ATIME:
3705 
3706         ok_to_copy = (dsb->st_ctime < ssb->st_ctime) ||
3707             (dsb->st_mtime < ssb->st_mtime) || (CompareBinaryFiles(changes_sourcefile, changes_destfile, ssb, dsb, fc, conn));
3708 
3709         if (ok_to_copy)
3710         {
3711             Log(LOG_LEVEL_VERBOSE, "Image file '%s' seems out of date, should be copy of '%s'", destfile, sourcefile);
3712             free(chrooted_sourcefile);
3713             return ok_to_copy;
3714         }
3715         break;
3716 
3717     default:
3718         ok_to_copy = (dsb->st_ctime < ssb->st_ctime) || (dsb->st_mtime < ssb->st_mtime);
3719 
3720         if (ok_to_copy)
3721         {
3722             Log(LOG_LEVEL_VERBOSE, "Image file '%s' out of date, should be copy of '%s'", destfile, sourcefile);
3723             free(chrooted_sourcefile);
3724             return ok_to_copy;
3725         }
3726         break;
3727     }
3728 
3729     free(chrooted_sourcefile);
3730     return false;
3731 }
3732 
FileAutoDefine(EvalContext * ctx,char * destfile)3733 static void FileAutoDefine(EvalContext *ctx, char *destfile)
3734 {
3735     char context[CF_MAXVARSIZE];
3736 
3737     snprintf(context, CF_MAXVARSIZE, "auto_%s", CanonifyName(destfile));
3738     EvalContextClassPutSoft(ctx, context, CONTEXT_SCOPE_NAMESPACE, "source=promise");
3739     Log(LOG_LEVEL_INFO, "Auto defining class '%s'", context);
3740 }
3741 
3742 #ifndef __MINGW32__
LoadSetxid(void)3743 static void LoadSetxid(void)
3744 {
3745     assert(!VSETXIDLIST);
3746 
3747     char filename[CF_BUFSIZE];
3748     snprintf(filename, CF_BUFSIZE, "%s/cfagent.%s.log", GetLogDir(), VSYSNAME.nodename);
3749     ToLowerStrInplace(filename);
3750     MapName(filename);
3751 
3752     VSETXIDLIST = RawLoadItemList(filename);
3753 }
3754 
SaveSetxid(bool modified)3755 static void SaveSetxid(bool modified)
3756 {
3757     if (!VSETXIDLIST)
3758     {
3759         return;
3760     }
3761 
3762     if (modified)
3763     {
3764         char filename[CF_BUFSIZE];
3765         snprintf(filename, CF_BUFSIZE, "%s/cfagent.%s.log", GetLogDir(), VSYSNAME.nodename);
3766         ToLowerStrInplace(filename);
3767         MapName(filename);
3768 
3769         PurgeItemList(&VSETXIDLIST, "SETUID/SETGID");
3770 
3771         Item *current = RawLoadItemList(filename);
3772         if (!ListsCompare(VSETXIDLIST, current))
3773         {
3774             mode_t oldmode = umask(077); // This setxidlist file must only be accesible by root
3775             Log(LOG_LEVEL_DEBUG,
3776                 "Updating setxidlist at '%s', umask was %o, will create setxidlist using umask 0077, file perms should be 0600.",
3777                 filename,
3778                 oldmode);
3779             RawSaveItemList(VSETXIDLIST, filename, NewLineMode_Unix);
3780             umask(oldmode);
3781             Log(LOG_LEVEL_DEBUG, "Restored umask to %o", oldmode);
3782         }
3783         DeleteItemList(current);
3784     }
3785 
3786     DeleteItemList(VSETXIDLIST);
3787     VSETXIDLIST = NULL;
3788 }
3789 
IsInSetxidList(const char * file)3790 static bool IsInSetxidList(const char *file)
3791 {
3792     if (!VSETXIDLIST)
3793     {
3794        LoadSetxid();
3795     }
3796 
3797     return IsItemIn(VSETXIDLIST, file);
3798 }
3799 
VerifySetUidGid(EvalContext * ctx,const char * file,const struct stat * dstat,mode_t newperm,const Promise * pp,const Attributes * attr)3800 static PromiseResult VerifySetUidGid(EvalContext *ctx, const char *file, const struct stat *dstat, mode_t newperm,
3801                                      const Promise *pp, const Attributes *attr)
3802 {
3803     assert(attr != NULL);
3804     int amroot = true;
3805     PromiseResult result = PROMISE_RESULT_NOOP;
3806     bool setxid_modified = false;
3807 
3808 
3809     if (!IsPrivileged())
3810     {
3811         amroot = false;
3812     }
3813 
3814     if ((dstat->st_uid == 0) && (dstat->st_mode & S_ISUID))
3815     {
3816         if (newperm & S_ISUID)
3817         {
3818             if (!IsInSetxidList(file))
3819             {
3820                 if (amroot)
3821                 {
3822                     Log(LOG_LEVEL_NOTICE, "NEW SETUID root PROGRAM '%s' ", file);
3823                 }
3824 
3825                 PrependItem(&VSETXIDLIST, file, NULL);
3826                 setxid_modified = true;
3827             }
3828         }
3829         else if (MakingChanges(ctx, pp, attr, &result, "remove setuid (root) flag from '%s'", file))
3830         {
3831             RecordChange(ctx, pp, attr, "Removed setuid (root) flag from '%s'", file);
3832             result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
3833         }
3834     }
3835 
3836     if (dstat->st_uid == 0 && (dstat->st_mode & S_ISGID))
3837     {
3838         if (newperm & S_ISGID)
3839         {
3840             if (S_ISDIR(dstat->st_mode))
3841             {
3842                 /* setgid directory */
3843             }
3844             else if (!IsInSetxidList(file))
3845             {
3846                 if (amroot)
3847                 {
3848                     Log(LOG_LEVEL_NOTICE, "NEW SETGID root PROGRAM '%s' ", file);
3849                 }
3850 
3851                 PrependItem(&VSETXIDLIST, file, NULL);
3852                 setxid_modified = true;
3853             }
3854         }
3855         else if (MakingChanges(ctx, pp, attr, &result, "remove setgid (root) flag from '%s'", file))
3856         {
3857             RecordChange(ctx, pp, attr, "Removed setgid (root) flag from '%s'", file);
3858             result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE);
3859         }
3860     }
3861 
3862     SaveSetxid(setxid_modified);
3863 
3864     return result;
3865 }
3866 #endif
3867 
3868 #ifdef __APPLE__
3869 
VerifyFinderType(EvalContext * ctx,const char * file,const Attributes * a,const Promise * pp,PromiseResult * result)3870 static int VerifyFinderType(EvalContext *ctx, const char *file, const Attributes *a, const Promise *pp, PromiseResult *result)
3871 {                               /* Code modeled after hfstar's extract.c */
3872     assert(a != NULL);
3873     typedef struct
3874     {
3875         long fdType;
3876         long fdCreator;
3877         short fdFlags;
3878         short fdLocationV;
3879         short fdLocationH;
3880         short fdFldr;
3881         short fdIconID;
3882         short fdUnused[3];
3883         char fdScript;
3884         char fdXFlags;
3885         short fdComment;
3886         long fdPutAway;
3887     }
3888     FInfo;
3889 
3890     struct attrlist attrs;
3891     struct
3892     {
3893         long ssize;
3894         struct timespec created;
3895         struct timespec modified;
3896         struct timespec changed;
3897         struct timespec backup;
3898         FInfo fi;
3899     }
3900     fndrInfo;
3901     int retval;
3902 
3903     const char *changes_file = file;
3904     if (ChrootChanges())
3905     {
3906         changes_file = ToChangesChroot(file);
3907     }
3908 
3909     if (a->perms.findertype == NULL)
3910     {
3911         return 0;
3912     }
3913 
3914     Log(LOG_LEVEL_DEBUG, "VerifyFinderType of '%s' for '%s'", file, a->perms.findertype);
3915 
3916     if (strncmp(a->perms.findertype, "*", CF_BUFSIZE) == 0 || strncmp(a->perms.findertype, "", CF_BUFSIZE) == 0)
3917     {
3918         return 0;
3919     }
3920 
3921     attrs.bitmapcount = ATTR_BIT_MAP_COUNT;
3922     attrs.reserved = 0;
3923     attrs.commonattr = ATTR_CMN_CRTIME | ATTR_CMN_MODTIME | ATTR_CMN_CHGTIME | ATTR_CMN_BKUPTIME | ATTR_CMN_FNDRINFO;
3924     attrs.volattr = 0;
3925     attrs.dirattr = 0;
3926     attrs.fileattr = 0;
3927     attrs.forkattr = 0;
3928 
3929     memset(&fndrInfo, 0, sizeof(fndrInfo));
3930 
3931     getattrlist(changes_file, &attrs, &fndrInfo, sizeof(fndrInfo), 0);
3932 
3933     if (fndrInfo.fi.fdType != *(long *) a->perms.findertype)
3934     {
3935         fndrInfo.fi.fdType = *(long *) a->perms.findertype;
3936 
3937         if (MakingChanges(ctx, pp, a, result,
3938                           "set Finder Type code of '%s' to '%s'", file, a->perms.findertype))
3939         {
3940             /* setattrlist does not take back in the long ssize */
3941             retval = setattrlist(changes_file, &attrs, &fndrInfo.created, 4 * sizeof(struct timespec) + sizeof(FInfo), 0);
3942 
3943             Log(LOG_LEVEL_DEBUG, "CheckFinderType setattrlist returned '%d'", retval);
3944 
3945             if (retval >= 0)
3946             {
3947                 RecordChange(ctx, pp, a, "Set Finder Type code of '%s' to '%s'", file, a->perms.findertype);
3948                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
3949             }
3950             else
3951             {
3952                 RecordFailure(ctx, pp, a, "Setting Finder Type code of '%s' to '%s' failed",
3953                               file, a->perms.findertype);
3954                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
3955             }
3956 
3957             return retval;
3958         }
3959         else
3960         {
3961             return 0;
3962         }
3963     }
3964     else
3965     {
3966         RecordNoChange(ctx, pp, a, "Finder Type code of '%s' to '%s' is as promised", file, a->perms.findertype);
3967         *result = PromiseResultUpdate(*result, PROMISE_RESULT_NOOP);
3968         return 0;
3969     }
3970 }
3971 
3972 #endif
3973 
TruncateFile(const char * name)3974 static void TruncateFile(const char *name)
3975 {
3976     struct stat statbuf;
3977     int fd;
3978 
3979     if (stat(name, &statbuf) == -1)
3980     {
3981         Log(LOG_LEVEL_DEBUG, "Didn't find '%s' to truncate", name);
3982     }
3983     else
3984     {
3985         if ((fd = safe_creat(name, 000)) == -1)      /* dummy mode ignored */
3986         {
3987             Log(LOG_LEVEL_ERR, "Failed to create or truncate file '%s'. (create: %s)", name, GetErrorStr());
3988         }
3989         else
3990         {
3991             close(fd);
3992         }
3993     }
3994 }
3995 
RegisterAHardLink(int i,const char * value,EvalContext * ctx,const Promise * pp,const Attributes * attr,PromiseResult * result,CompressedArray ** inode_cache)3996 static void RegisterAHardLink(int i, const char *value, EvalContext *ctx, const Promise *pp,
3997                               const Attributes *attr, PromiseResult *result,
3998                               CompressedArray **inode_cache)
3999 {
4000     if (!FixCompressedArrayValue(i, value, inode_cache))
4001     {
4002         /* Not root hard link, remove to preserve consistency */
4003         if (MakingChanges(ctx, pp, attr, result, "remove old hard link '%s' to preserve structure",
4004                           value))
4005         {
4006             RecordChange(ctx, pp, attr, "Removed old hard link '%s' to preserve structure", value);
4007             unlink(value);
4008         }
4009     }
4010 }
4011 
cf_stat(const char * file,struct stat * buf,const FileCopy * fc,AgentConnection * conn)4012 static int cf_stat(const char *file, struct stat *buf, const FileCopy *fc, AgentConnection *conn)
4013 {
4014     if (!file)
4015     {
4016         return -1;
4017     }
4018 
4019     if (conn == NULL)
4020     {
4021         return stat(file, buf);
4022     }
4023     else
4024     {
4025         assert(fc->servers != NULL &&
4026                strcmp(RlistScalarValue(fc->servers), "localhost") != 0);
4027 
4028         return cf_remote_stat(conn, fc->encrypt, file, buf, "file");
4029     }
4030 }
4031 
4032 #ifndef __MINGW32__
4033 
cf_readlink(EvalContext * ctx,const char * sourcefile,char * linkbuf,size_t buffsize,const Attributes * attr,const Promise * pp,AgentConnection * conn,PromiseResult * result)4034 static int cf_readlink(EvalContext *ctx, const char *sourcefile, char *linkbuf, size_t buffsize,
4035                        const Attributes *attr, const Promise *pp, AgentConnection *conn, PromiseResult *result)
4036  /* wrapper for network access */
4037 {
4038     assert(buffsize > 0);
4039     assert(attr != NULL);
4040     memset(linkbuf, 0, buffsize);
4041 
4042     if (conn == NULL)
4043     {
4044         return readlink(sourcefile, linkbuf, buffsize - 1);
4045     }
4046     assert(attr->copy.servers &&
4047            strcmp(RlistScalarValue(attr->copy.servers), "localhost"));
4048 
4049     const Stat *sp = StatCacheLookup(conn, sourcefile,
4050                                      RlistScalarValue(attr->copy.servers));
4051 
4052     if (sp)
4053     {
4054         if (sp->cf_readlink != NULL)
4055         {
4056             if (strlen(sp->cf_readlink) + 1 > buffsize)
4057             {
4058                 cfPS(ctx, LOG_LEVEL_ERR, PROMISE_RESULT_FAIL, pp, attr, "readlink value is too large in cfreadlink");
4059                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
4060                 Log(LOG_LEVEL_ERR, "Contained '%s'", sp->cf_readlink);
4061                 return -1;
4062             }
4063             else
4064             {
4065                 memset(linkbuf, 0, buffsize);
4066                 strcpy(linkbuf, sp->cf_readlink);
4067                 return 0;
4068             }
4069         }
4070     }
4071 
4072     return -1;
4073 }
4074 
4075 #endif /* !__MINGW32__ */
4076 
SkipDirLinks(EvalContext * ctx,const char * path,const char * lastnode,DirectoryRecursion r)4077 static bool SkipDirLinks(EvalContext *ctx, const char *path, const char *lastnode, DirectoryRecursion r)
4078 {
4079     if (r.exclude_dirs)
4080     {
4081         if ((MatchRlistItem(ctx, r.exclude_dirs, path)) || (MatchRlistItem(ctx, r.exclude_dirs, lastnode)))
4082         {
4083             Log(LOG_LEVEL_VERBOSE, "Skipping matched excluded directory '%s'", path);
4084             return true;
4085         }
4086     }
4087 
4088     if (r.include_dirs)
4089     {
4090         if (!((MatchRlistItem(ctx, r.include_dirs, path)) || (MatchRlistItem(ctx, r.include_dirs, lastnode))))
4091         {
4092             Log(LOG_LEVEL_VERBOSE, "Skipping matched non-included directory '%s'", path);
4093             return true;
4094         }
4095     }
4096 
4097     return false;
4098 }
4099 
4100 #ifndef __MINGW32__
4101 
VerifyOwner(EvalContext * ctx,const char * file,const Promise * pp,const Attributes * attr,const struct stat * sb,PromiseResult * result)4102 bool VerifyOwner(EvalContext *ctx, const char *file, const Promise *pp, const Attributes *attr, const struct stat *sb, PromiseResult *result)
4103 {
4104     assert(sb != NULL);
4105 
4106     struct passwd *pw;
4107     struct group *gp;
4108     UidList *ulp;
4109     GidList *glp;
4110 
4111     /* The groups to change ownership to, using lchown(uid,gid). */
4112     uid_t uid = CF_UNKNOWN_OWNER;                       /* just init values */
4113     gid_t gid = CF_UNKNOWN_GROUP;
4114 
4115     /* SKIP if file is already owned by anyone of the promised owners. */
4116     for (ulp = attr->perms.owners; ulp != NULL; ulp = ulp->next)
4117     {
4118         if (ulp->uid == CF_SAME_OWNER)        /* "same" matches anything */
4119         {
4120             uid = CF_SAME_OWNER;
4121             break;
4122         }
4123         if (sb->st_uid == ulp->uid)
4124         {
4125             RecordNoChange(ctx, pp, attr, "Owner of '%s' as promised (%ju)", file, (uintmax_t) ulp->uid);
4126             uid = CF_SAME_OWNER;
4127             break;
4128         }
4129     }
4130 
4131     if (uid != CF_SAME_OWNER)
4132     {
4133         /* Change ownership to the first known user in the promised list. */
4134         for (ulp = attr->perms.owners; ulp != NULL; ulp = ulp->next)
4135         {
4136             if (ulp->uid != CF_UNKNOWN_OWNER)
4137             {
4138                 uid = ulp->uid;
4139                 break;
4140             }
4141         }
4142         if (ulp == NULL)
4143         {
4144             RecordFailure(ctx, pp, attr,
4145                           "None of the promised owners for '%s' exist -- see INFO logs for more",
4146                           file);
4147             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
4148             uid = CF_SAME_OWNER;      /* chown(-1) doesn't change ownership */
4149         }
4150     }
4151     assert(uid != CF_UNKNOWN_OWNER);
4152 
4153     /* SKIP if file is already group owned by anyone of the promised groups. */
4154     for (glp = attr->perms.groups; glp != NULL; glp = glp->next)
4155     {
4156         if (glp->gid == CF_SAME_GROUP)
4157         {
4158             gid = CF_SAME_GROUP;
4159             break;
4160         }
4161         if (sb->st_gid == glp->gid)
4162         {
4163             RecordNoChange(ctx, pp, attr, "Group of '%s' as promised (%ju)", file, (uintmax_t) glp->gid);
4164             gid = CF_SAME_GROUP;
4165             break;
4166         }
4167     }
4168 
4169     /* Change group ownership to the first known group in the promised list. */
4170     if (gid != CF_SAME_GROUP)
4171     {
4172         for (glp = attr->perms.groups; glp != NULL; glp = glp->next)
4173         {
4174             if (glp->gid != CF_UNKNOWN_GROUP)
4175             {
4176                 gid = glp->gid;
4177                 break;
4178             }
4179         }
4180         if (glp == NULL)
4181         {
4182             RecordFailure(ctx, pp, attr,
4183                           "None of the promised groups for '%s' exist -- see INFO logs for more",
4184                           file);
4185             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
4186             gid = CF_SAME_GROUP;      /* chown(-1) doesn't change ownership */
4187         }
4188     }
4189     assert(gid != CF_UNKNOWN_GROUP);
4190 
4191     if ((uid == CF_SAME_OWNER) && (gid == CF_SAME_GROUP))
4192     {
4193         /* Owner and group as promised or unspecified, nothing to do. */
4194         return false;
4195     }
4196 
4197     /* else */
4198     if ((pw = getpwuid(sb->st_uid)) == NULL)
4199     {
4200         RecordWarning(ctx, pp, attr,
4201                       "File '%s' is not owned by anybody in the passwd database (uid = %ju)",
4202                       file, (uintmax_t)sb->st_uid);
4203     }
4204 
4205     if ((gp = getgrgid(sb->st_gid)) == NULL)
4206     {
4207         RecordWarning(ctx, pp, attr, "File '%s' is not owned by any group in group database  (gid = %ju)",
4208                       file, (uintmax_t)sb->st_gid);
4209     }
4210 
4211     if (uid != CF_SAME_OWNER)
4212     {
4213         Log(LOG_LEVEL_DEBUG, "Change owner to uid '%ju' if possible",
4214             (uintmax_t) uid);
4215     }
4216 
4217     if (gid != CF_SAME_GROUP)
4218     {
4219         Log(LOG_LEVEL_DEBUG, "Change group to gid '%ju' if possible",
4220             (uintmax_t) gid);
4221     }
4222 
4223     if ((uid != CF_SAME_OWNER) && (gid != CF_SAME_GROUP) &&
4224         !MakingChanges(ctx, pp, attr, result, "change owner and group of '%s' to '%ju:%ju'",
4225                        file, (uintmax_t) uid, (uintmax_t) gid))
4226     {
4227         return false;
4228     }
4229     else if ((uid != CF_SAME_OWNER) &&
4230              !MakingChanges(ctx, pp, attr, result, "change owner of '%s' to '%ju'",
4231                             file, (uintmax_t) uid))
4232     {
4233         return false;
4234     }
4235     else if ((gid != CF_SAME_GROUP) &&
4236              !MakingChanges(ctx, pp, attr, result, "change group of '%s' to '%ju'",
4237                             file, (uintmax_t) gid))
4238     {
4239         return false;
4240     }
4241 
4242     const char *changes_file = file;
4243     if (ChrootChanges())
4244     {
4245         changes_file = ToChangesChroot(file);
4246     }
4247 
4248     if (S_ISLNK(sb->st_mode))
4249     {
4250 # ifdef HAVE_LCHOWN
4251         Log(LOG_LEVEL_DEBUG, "Using lchown function");
4252         if (safe_lchown(changes_file, uid, gid) == -1)
4253         {
4254             RecordFailure(ctx, pp, attr, "Cannot set ownership on link '%s'. (lchown: %s)",
4255                           file, GetErrorStr());
4256         }
4257         else
4258         {
4259             if (uid != CF_SAME_OWNER)
4260             {
4261                 RecordChange(ctx, pp, attr, "Owner of link '%s' was %ju, set to %ju",
4262                              file, (uintmax_t) sb->st_uid, (uintmax_t) uid);
4263                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
4264             }
4265 
4266             if (gid != CF_SAME_GROUP)
4267             {
4268                 RecordChange(ctx, pp, attr, "Group of link '%s' was %ju, set to %ju",
4269                              file, (uintmax_t)sb->st_gid, (uintmax_t)gid);
4270                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
4271             }
4272         }
4273 # endif
4274     }
4275     else
4276     {
4277         if (safe_chown(changes_file, uid, gid) == -1)
4278         {
4279             RecordDenial(ctx, pp, attr, "Cannot set ownership on file '%s'. (chown: %s)",
4280                          file, GetErrorStr());
4281             *result = PromiseResultUpdate(*result, PROMISE_RESULT_DENIED);
4282         }
4283         else
4284         {
4285             if (uid != CF_SAME_OWNER)
4286             {
4287                 RecordChange(ctx, pp, attr, "Owner of '%s' was %ju, set to %ju",
4288                              file, (uintmax_t) sb->st_uid, (uintmax_t) uid);
4289                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
4290             }
4291 
4292             if (gid != CF_SAME_GROUP)
4293             {
4294                 RecordChange(ctx, pp, attr, "Group of '%s' was %ju, set to %ju",
4295                              file, (uintmax_t)sb->st_gid, (uintmax_t)gid);
4296                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
4297             }
4298         }
4299     }
4300     return false;
4301 }
4302 
4303 #endif /* !__MINGW32__ */
4304 
VerifyFileChanges(EvalContext * ctx,const char * file,const struct stat * sb,const Attributes * attr,const Promise * pp,PromiseResult * result)4305 static void VerifyFileChanges(EvalContext *ctx, const char *file, const struct stat *sb,
4306                               const Attributes *attr, const Promise *pp, PromiseResult *result)
4307 {
4308     if ((attr->change.report_changes != FILE_CHANGE_REPORT_STATS_CHANGE) && (attr->change.report_changes != FILE_CHANGE_REPORT_ALL))
4309     {
4310         return;
4311     }
4312 
4313     FileChangesCheckAndUpdateStats(ctx, file, sb, attr->change.update, attr, pp, result);
4314 }
4315 
CfCreateFile(EvalContext * ctx,char * file,const Promise * pp,const Attributes * attr,PromiseResult * result)4316 bool CfCreateFile(EvalContext *ctx, char *file, const Promise *pp, const Attributes *attr, PromiseResult *result)
4317 {
4318     assert(attr != NULL);
4319     if (!IsAbsoluteFileName(file))
4320     {
4321         RecordFailure(ctx, pp, attr,
4322                       "Cannot create a relative filename '%s' - has no invariant meaning. (create: %s)",
4323                       file, GetErrorStr());
4324         *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
4325         return false;
4326     }
4327 
4328     const char *changes_file = file;
4329     if (ChrootChanges())
4330     {
4331         changes_file = ToChangesChroot(file);
4332     }
4333 
4334     /* If name ends in /., or depthsearch is set, then this is a directory */
4335     bool is_dir = attr->havedepthsearch || (strcmp(".", ReadLastNode(file)) == 0);
4336     if (is_dir)
4337     {
4338         Log(LOG_LEVEL_DEBUG, "File object '%s' seems to be a directory", file);
4339 
4340         if (MakingChanges(ctx, pp, attr, result, "create directory '%s'", file))
4341         {
4342             bool dir_created = false;
4343             if (!MakeParentDirectoryForPromise(ctx, pp, attr, result,
4344                                                file, attr->move_obstructions, &dir_created))
4345             {
4346                 return false;
4347             }
4348             if (dir_created)
4349             {
4350                 RecordChange(ctx, pp, attr, "Created directory '%s'", file);
4351                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
4352             }
4353         }
4354         else
4355         {
4356             return false;
4357         }
4358     }
4359     else if (attr->file_type && !strncmp(attr->file_type, "fifo", 5))
4360     {
4361 #ifndef _WIN32
4362         mode_t filemode = 0600;
4363         if (PromiseGetConstraintAsRval(pp, "mode", RVAL_TYPE_SCALAR) == NULL)
4364         {
4365             Log(LOG_LEVEL_VERBOSE, "No mode was set, choose plain file default %04jo", (uintmax_t)filemode);
4366         }
4367         else
4368         {
4369             filemode = attr->perms.plus & ~(attr->perms.minus);
4370         }
4371 
4372         bool dir_created = false;
4373         if (!MakeParentDirectoryForPromise(ctx, pp, attr, result,
4374                                            file, attr->move_obstructions, &dir_created))
4375         {
4376             return false;
4377         }
4378         if (dir_created)
4379         {
4380             RecordChange(ctx, pp, attr, "Created directory for '%s'", file);
4381             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
4382         }
4383 
4384         if (MakingChanges(ctx, pp, attr, result, "create named pipe '%s', mode %04jo",
4385                           file, (uintmax_t) filemode))
4386         {
4387             mode_t saveumask = umask(0);
4388             char errormsg[CF_BUFSIZE];
4389             if (mkfifo(changes_file, filemode) != 0)
4390             {
4391                 snprintf(errormsg, sizeof(errormsg), "(mkfifo: %s)", GetErrorStr());
4392                 RecordFailure(ctx, pp, attr, "Error creating file '%s', mode '%04jo'. %s",
4393                               file, (uintmax_t)filemode, errormsg);
4394                 umask(saveumask);
4395                 return false;
4396             }
4397             RecordChange(ctx, pp, attr, "Created named pipe '%s', mode %04jo", file, (uintmax_t) filemode);
4398             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
4399 
4400             umask(saveumask);
4401             return true;
4402         }
4403         else
4404         {
4405             return false;
4406         }
4407 #else
4408         RecordWarning(ctx, pp, attr, "Named pipe creation not supported on Windows");
4409         *result = PromiseResultUpdate(*result, PROMISE_RESULT_WARN);
4410         return false;
4411 #endif
4412     }
4413     else
4414     {
4415         mode_t filemode = 0600;     /* Decide the mode for filecreation */
4416         if (PromiseGetConstraintAsRval(pp, "mode", RVAL_TYPE_SCALAR) == NULL)
4417         {
4418             Log(LOG_LEVEL_VERBOSE, "No mode was set, choose plain file default %04jo", (uintmax_t)filemode);
4419         }
4420         else
4421         {
4422             filemode = attr->perms.plus & ~(attr->perms.minus);
4423         }
4424 
4425         bool dir_created = false;
4426         if (!MakeParentDirectoryForPromise(ctx, pp, attr, result,
4427                                            file, attr->move_obstructions, &dir_created))
4428         {
4429             return false;
4430         }
4431         if (dir_created)
4432         {
4433             RecordChange(ctx, pp, attr, "Created directory for '%s'", file);
4434             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
4435         }
4436 
4437         if (MakingChanges(ctx, pp, attr, result, "create file '%s', mode '%04jo'",
4438                           file, (uintmax_t)filemode))
4439         {
4440             mode_t saveumask = umask(0);
4441 
4442             int fd = safe_open_create_perms(changes_file, O_WRONLY | O_CREAT | O_EXCL, filemode);
4443             if (fd == -1)
4444             {
4445                 char errormsg[CF_BUFSIZE];
4446                 if (errno == EEXIST)
4447                 {
4448                     snprintf(errormsg, sizeof(errormsg), "(open: '%s'). "
4449                              "Most likely a dangling symlink is in the way. "
4450                              "Refusing to create the target file of dangling symlink (security risk).",
4451                              GetErrorStr());
4452                 }
4453                 else
4454                 {
4455                     snprintf(errormsg, sizeof(errormsg), "(open: %s)", GetErrorStr());
4456                 }
4457                 RecordFailure(ctx, pp, attr, "Error creating file '%s', mode '%04jo'. %s",
4458                               file, (uintmax_t)filemode, errormsg);
4459                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
4460                 umask(saveumask);
4461                 return false;
4462             }
4463             else
4464             {
4465                 RecordChange(ctx, pp, attr, "Created file '%s', mode %04jo", file, (uintmax_t)filemode);
4466                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
4467                 close(fd);
4468                 umask(saveumask);
4469                 return true;
4470             }
4471         }
4472         else
4473         {
4474             return false;
4475         }
4476     }
4477 
4478     return true;
4479 }
4480 
DeviceBoundary(const struct stat * sb,dev_t rootdevice)4481 static bool DeviceBoundary(const struct stat *sb, dev_t rootdevice)
4482 {
4483     if (sb->st_dev == rootdevice)
4484     {
4485         return false;
4486     }
4487     else
4488     {
4489         Log(LOG_LEVEL_VERBOSE, "Device change from %jd to %jd", (intmax_t) rootdevice, (intmax_t) sb->st_dev);
4490         return true;
4491     }
4492 }
4493