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 <files_changes.h>
26 #include <sequence.h>
27 #include <hash.h>
28 #include <string_lib.h>
29 #include <misc_lib.h>
30 #include <file_lib.h>
31 #include <dbm_api.h>
32 #include <promises.h>
33 #include <actuator.h>
34 #include <eval_context.h>
35 #include <known_dirs.h>
36 
37 /*
38   The format of the changes database is as follows:
39 
40          Key:   |            Value:
41   "D_<path>"    | "<basename>\0<basename>\0..." (SORTED!)
42                 |
43   "H_<hash_key> | "<hash>\0"
44                 |
45   "S_<path>     | "<struct stat>"
46 
47   Explanation:
48 
49   - The "D" entry contains all the filenames that have been recorded in that
50     directory, stored as the basename.
51   - The "H" entry records the hash of a file.
52   - The "S" entry records the stat information of a file.
53 */
54 
55 #define CHANGES_HASH_STRING_LEN 7
56 #define CHANGES_HASH_FILE_NAME_OFFSET  CHANGES_HASH_STRING_LEN+1
57 
58 typedef struct
59 {
60     unsigned char mess_digest[EVP_MAX_MD_SIZE + 1];     /* Content digest */
61 } ChecksumValue;
62 
63 static bool GetDirectoryListFromDatabase(CF_DB *db, const char * path, Seq *files);
64 static bool FileChangesSetDirectoryList(CF_DB *db, const char *path, const Seq *files, bool *change);
65 
66 /*
67  * Key format:
68  *
69  * 7 bytes    hash name, \0 padded at right
70  * 1 byte     \0
71  * N bytes    pathname
72  */
NewIndexKey(char type,const char * name,int * size)73 static char *NewIndexKey(char type, const char *name, int *size)
74 {
75     char *chk_key;
76 
77 // "H_" plus pathname plus index_str in one block + \0
78 
79     const size_t len = strlen(name);
80     *size = len + CHANGES_HASH_FILE_NAME_OFFSET + 3;
81 
82     chk_key = xcalloc(1, *size);
83 
84 // Data start after offset for index
85 
86     strlcpy(chk_key, "H_", 2);
87     strlcpy(chk_key + 2, HashNameFromId(type), CHANGES_HASH_STRING_LEN);
88     memcpy(chk_key + 2 + CHANGES_HASH_FILE_NAME_OFFSET, name, len);
89     return chk_key;
90 }
91 
DeleteIndexKey(char * key)92 static void DeleteIndexKey(char *key)
93 {
94     free(key);
95 }
96 
NewHashValue(unsigned char digest[EVP_MAX_MD_SIZE+1])97 static ChecksumValue *NewHashValue(unsigned char digest[EVP_MAX_MD_SIZE + 1])
98 {
99     ChecksumValue *chk_val;
100 
101     chk_val = xcalloc(1, sizeof(ChecksumValue));
102 
103     memcpy(chk_val->mess_digest, digest, EVP_MAX_MD_SIZE + 1);
104 
105     return chk_val;
106 }
107 
DeleteHashValue(ChecksumValue * chk_val)108 static void DeleteHashValue(ChecksumValue *chk_val)
109 {
110     free(chk_val);
111 }
112 
ReadHash(CF_DB * dbp,HashMethod type,const char * name,unsigned char digest[EVP_MAX_MD_SIZE+1])113 static bool ReadHash(CF_DB *dbp, HashMethod type, const char *name, unsigned char digest[EVP_MAX_MD_SIZE + 1])
114 {
115     char *key;
116     int size;
117     ChecksumValue chk_val;
118 
119     key = NewIndexKey(type, name, &size);
120 
121     if (ReadComplexKeyDB(dbp, key, size, (void *) &chk_val, sizeof(ChecksumValue)))
122     {
123         memcpy(digest, chk_val.mess_digest, EVP_MAX_MD_SIZE + 1);
124         DeleteIndexKey(key);
125         return true;
126     }
127     else
128     {
129         DeleteIndexKey(key);
130         return false;
131     }
132 }
133 
WriteHash(CF_DB * dbp,HashMethod type,const char * name,unsigned char digest[EVP_MAX_MD_SIZE+1])134 static int WriteHash(CF_DB *dbp, HashMethod type, const char *name, unsigned char digest[EVP_MAX_MD_SIZE + 1])
135 {
136     char *key;
137     ChecksumValue *value;
138     int ret, keysize;
139 
140     key = NewIndexKey(type, name, &keysize);
141     value = NewHashValue(digest);
142     ret = WriteComplexKeyDB(dbp, key, keysize, value, sizeof(ChecksumValue));
143     DeleteIndexKey(key);
144     DeleteHashValue(value);
145     return ret;
146 }
147 
DeleteHash(CF_DB * dbp,HashMethod type,const char * name)148 static void DeleteHash(CF_DB *dbp, HashMethod type, const char *name)
149 {
150     int size;
151     char *key;
152 
153     key = NewIndexKey(type, name, &size);
154     DeleteComplexKeyDB(dbp, key, size);
155     DeleteIndexKey(key);
156 }
157 
AddMigratedFileToDirectoryList(CF_DB * changes_db,const char * file,const char * common_msg)158 static void AddMigratedFileToDirectoryList(CF_DB *changes_db, const char *file, const char *common_msg)
159 {
160     // This is incredibly inefficient, since we add files to the list one by one,
161     // but the migration only ever needs to be done once for each host.
162     size_t file_len = strlen(file);
163     char dir[file_len + 1];
164     strcpy(dir, file);
165     const char *basefile;
166     char *last_slash = strrchr(dir, '/');
167 
168     if (last_slash == NULL)
169     {
170         Log(LOG_LEVEL_ERR, "%s: Invalid file entry: '%s'", common_msg, dir);
171         return;
172     }
173 
174     if (last_slash == dir)
175     {
176         // If we only have one slash, it is the root dir, so we need to have
177         // dir be equal to "/". We cannot both have that, and let basefile
178         // point to the the next component in dir (since the first character
179         // will then be '\0'), so point to the original file buffer instead.
180         dir[1] = '\0';
181         basefile = file + 1;
182     }
183     else
184     {
185         basefile = last_slash + 1;
186         *last_slash = '\0';
187     }
188 
189     Seq *files = SeqNew(1, free);
190     if (!GetDirectoryListFromDatabase(changes_db, dir, files))
191     {
192         Log(LOG_LEVEL_ERR, "%s: Not able to get directory index", common_msg);
193         SeqDestroy(files);
194         return;
195     }
196 
197     if (SeqBinaryIndexOf(files, basefile, StrCmpWrapper) == -1)
198     {
199         SeqAppend(files, xstrdup(basefile));
200         SeqSort(files, StrCmpWrapper, NULL);
201         bool changes;
202         if (!FileChangesSetDirectoryList(changes_db, dir, files, &changes))
203         {
204             Log(LOG_LEVEL_ERR, "%s: Not able to update directory index", common_msg);
205         }
206     }
207 
208     SeqDestroy(files);
209 }
210 
MigrateOldChecksumDatabase(CF_DB * changes_db)211 static bool MigrateOldChecksumDatabase(CF_DB *changes_db)
212 {
213     CF_DB *old_db;
214 
215     const char *common_msg = "While converting old checksum database to new format";
216 
217     if (!OpenDB(&old_db, dbid_checksums))
218     {
219         Log(LOG_LEVEL_ERR, "%s: Could not open database.", common_msg);
220         return false;
221     }
222 
223     CF_DBC *cursor;
224     if (!NewDBCursor(old_db, &cursor))
225     {
226         Log(LOG_LEVEL_ERR, "%s: Could not open database cursor.", common_msg);
227         CloseDB(old_db);
228         return false;
229     }
230 
231     char *key;
232     int ksize;
233     char *value;
234     int vsize;
235     while (NextDB(cursor, &key, &ksize, (void **)&value, &vsize))
236     {
237         char new_key[ksize + 2];
238         new_key[0] = 'H';
239         new_key[1] = '_';
240         memcpy(new_key + 2, key, ksize);
241         if (!WriteComplexKeyDB(changes_db, new_key, sizeof(new_key), value, vsize))
242         {
243             Log(LOG_LEVEL_ERR, "%s: Could not write file checksum to database", common_msg);
244             // Keep trying for other keys.
245         }
246         AddMigratedFileToDirectoryList(changes_db, key + CHANGES_HASH_FILE_NAME_OFFSET, common_msg);
247     }
248 
249     DeleteDBCursor(cursor);
250     CloseDB(old_db);
251 
252     return true;
253 }
254 
MigrateOldStatDatabase(CF_DB * changes_db)255 static bool MigrateOldStatDatabase(CF_DB *changes_db)
256 {
257     CF_DB *old_db;
258 
259     const char *common_msg = "While converting old filestat database to new format";
260 
261     if (!OpenDB(&old_db, dbid_filestats))
262     {
263         Log(LOG_LEVEL_ERR, "%s: Could not open database.", common_msg);
264         return false;
265     }
266 
267     CF_DBC *cursor;
268     if (!NewDBCursor(old_db, &cursor))
269     {
270         Log(LOG_LEVEL_ERR, "%s: Could not open database cursor.", common_msg);
271         CloseDB(old_db);
272         return false;
273     }
274 
275     char *key;
276     int ksize;
277     char *value;
278     int vsize;
279     while (NextDB(cursor, &key, &ksize, (void **)&value, &vsize))
280     {
281         char new_key[ksize + 2];
282         new_key[0] = 'S';
283         new_key[1] = '_';
284         memcpy(new_key + 2, key, ksize);
285         if (!WriteComplexKeyDB(changes_db, new_key, sizeof(new_key), value, vsize))
286         {
287             Log(LOG_LEVEL_ERR, "%s: Could not write filestat to database", common_msg);
288             // Keep trying for other keys.
289         }
290         AddMigratedFileToDirectoryList(changes_db, key, common_msg);
291     }
292 
293     DeleteDBCursor(cursor);
294     CloseDB(old_db);
295 
296     return true;
297 }
298 
OpenChangesDB(CF_DB ** db)299 static bool OpenChangesDB(CF_DB **db)
300 {
301     if (!OpenDB(db, dbid_changes))
302     {
303         Log(LOG_LEVEL_ERR, "Could not open changes database");
304         return false;
305     }
306 
307     struct stat statbuf;
308     char *old_checksums_db = DBIdToPath(dbid_checksums);
309     char *old_filestats_db = DBIdToPath(dbid_filestats);
310 
311     if (stat(old_checksums_db, &statbuf) != -1)
312     {
313         Log(LOG_LEVEL_INFO, "Migrating checksum database");
314         MigrateOldChecksumDatabase(*db);
315         char migrated_db_name[PATH_MAX];
316         snprintf(migrated_db_name, sizeof(migrated_db_name), "%s.cf-migrated", old_checksums_db);
317         Log(LOG_LEVEL_INFO, "After checksum database migration: Renaming '%s' to '%s'",
318             old_checksums_db, migrated_db_name);
319         if (rename(old_checksums_db, migrated_db_name) != 0)
320         {
321             Log(LOG_LEVEL_ERR, "Could not rename '%s' to '%s'", old_checksums_db, migrated_db_name);
322         }
323     }
324 
325     if (stat(old_filestats_db, &statbuf) != -1)
326     {
327         Log(LOG_LEVEL_INFO, "Migrating filestat database");
328         MigrateOldStatDatabase(*db);
329         char migrated_db_name[PATH_MAX];
330         snprintf(migrated_db_name, sizeof(migrated_db_name), "%s.cf-migrated", old_filestats_db);
331         Log(LOG_LEVEL_INFO, "After filestat database migration: Renaming '%s' to '%s'",
332             old_filestats_db, migrated_db_name);
333         if (rename(old_filestats_db, migrated_db_name) != 0)
334         {
335             Log(LOG_LEVEL_ERR, "Could not rename '%s' to '%s'", old_filestats_db, migrated_db_name);
336         }
337     }
338 
339     free(old_checksums_db);
340     free(old_filestats_db);
341 
342     return true;
343 }
344 
RemoveAllFileTraces(CF_DB * db,const char * path)345 static void RemoveAllFileTraces(CF_DB *db, const char *path)
346 {
347     for (int c = 0; c < HASH_METHOD_NONE; c++)
348     {
349         DeleteHash(db, c, path);
350     }
351     char key[strlen(path) + 3];
352     xsnprintf(key, sizeof(key), "S_%s", path);
353     DeleteDB(db, key);
354 }
355 
GetDirectoryListFromDatabase(CF_DB * db,const char * path,Seq * files)356 static bool GetDirectoryListFromDatabase(CF_DB *db, const char *path, Seq *files)
357 {
358     char key[strlen(path) + 3];
359     xsnprintf(key, sizeof(key), "D_%s", path);
360     if (!HasKeyDB(db, key, sizeof(key)))
361     {
362         // Not an error, so successful, but seq remains unchanged.
363         return true;
364     }
365     int size = ValueSizeDB(db, key, sizeof(key));
366     if (size <= 0)
367     {
368         // Shouldn't happen, since we don't store empty lists, but play it safe
369         // and return empty seq.
370         return true;
371     }
372 
373     char raw_entries[size];
374     if (!ReadDB(db, key, raw_entries, size))
375     {
376         Log(LOG_LEVEL_ERR, "Could not read changes database entry");
377         return false;
378     }
379 
380     char *raw_entries_end = raw_entries + size;
381     for (char *pos = raw_entries; pos < raw_entries_end;)
382     {
383         char *null_pos = memchr(pos, '\0', raw_entries_end - pos);
384         if (!null_pos)
385         {
386             Log(LOG_LEVEL_ERR, "Unexpected end of value in changes database");
387             return false;
388         }
389 
390         SeqAppend(files, xstrdup(pos));
391         pos = null_pos + 1;
392     }
393 
394     return true;
395 }
396 
FileChangesGetDirectoryList(const char * path,Seq * files)397 bool FileChangesGetDirectoryList(const char *path, Seq *files)
398 {
399     CF_DB *db;
400     if (!OpenChangesDB(&db))
401     {
402         Log(LOG_LEVEL_ERR, "Could not open changes database");
403         return false;
404     }
405 
406     bool result = GetDirectoryListFromDatabase(db, path, files);
407     CloseDB(db);
408     return result;
409 }
410 
FileChangesSetDirectoryList(CF_DB * db,const char * path,const Seq * files,bool * change)411 static bool FileChangesSetDirectoryList(CF_DB *db, const char *path, const Seq *files, bool *change)
412 {
413     assert(change != NULL);
414 
415     int size = 0;
416     int n_files = SeqLength(files);
417 
418     char key[strlen(path) + 3];
419     xsnprintf(key, sizeof(key), "D_%s", path);
420 
421     if (n_files == 0)
422     {
423         *change = DeleteDB(db, key);
424         return true;
425     }
426 
427     for (int c = 0; c < n_files; c++)
428     {
429         size += strlen(SeqAt(files, c)) + 1;
430     }
431 
432     char raw_entries[size];
433     char *pos = raw_entries;
434     for (int c = 0; c < n_files; c++)
435     {
436         strcpy(pos, SeqAt(files, c));
437         pos += strlen(pos) + 1;
438     }
439 
440     if (HasKeyDB(db, key, sizeof(key)))
441     {
442         char old_entries[MAX(size, 2 * CF_BUFSIZE)];
443         if (ReadDB(db, key, old_entries, sizeof(old_entries)) &&
444             (memcmp(old_entries, raw_entries, size) == 0))
445         {
446             Log(LOG_LEVEL_VERBOSE, "No changes in directory list");
447             *change = false;
448             return true;
449         }
450     }
451 
452     if (!WriteDB(db, key, raw_entries, size))
453     {
454         Log(LOG_LEVEL_ERR, "Could not write to changes database");
455         return false;
456     }
457 
458     *change = true;
459     return true;
460 }
461 
462 /**
463  * @return %false if #filename never seen before, and adds a checksum to the
464  *         database; %true if hashes do not match and also updates database to
465  *         the new value if #update is true.
466  */
FileChangesCheckAndUpdateHash(EvalContext * ctx,const char * filename,unsigned char digest[EVP_MAX_MD_SIZE+1],HashMethod type,const Attributes * attr,const Promise * pp,PromiseResult * result)467 bool FileChangesCheckAndUpdateHash(EvalContext *ctx,
468                                    const char *filename,
469                                    unsigned char digest[EVP_MAX_MD_SIZE + 1],
470                                    HashMethod type,
471                                    const Attributes *attr,
472                                    const Promise *pp,
473                                    PromiseResult *result)
474 {
475     assert(attr != NULL);
476 
477     const int size = HashSizeFromId(type);
478     unsigned char dbdigest[EVP_MAX_MD_SIZE + 1];
479     CF_DB *dbp;
480     bool found;
481     bool different;
482     bool ret = false;
483     bool update = attr->change.update;
484 
485     if (!OpenChangesDB(&dbp))
486     {
487         RecordFailure(ctx, pp, attr, "Unable to open the hash database!");
488         *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
489         return false;
490     }
491 
492     if (ReadHash(dbp, type, filename, dbdigest))
493     {
494         found = true;
495         different = (memcmp(digest, dbdigest, size) != 0);
496         if (different)
497         {
498             Log(LOG_LEVEL_INFO, "Hash '%s' for '%s' changed!", HashNameFromId(type), filename);
499             if (pp->comment)
500             {
501                 Log(LOG_LEVEL_VERBOSE, "Preceding promise '%s'", pp->comment);
502             }
503         }
504     }
505     else
506     {
507         found = false;
508         different = true;
509     }
510 
511     if (different)
512     {
513         /* TODO: Should we compare the stored hash with the digest of the file
514          *       in the changes chroot in case of ChrootChanges()?  */
515         if (!MakingInternalChanges(ctx, pp, attr, result, "record change of hash for file '%s'",
516                                    filename))
517         {
518             ret = true;
519         }
520         else if (!found || update)
521         {
522             const char *action = found ? "Updated" : "Stored";
523             char buffer[CF_HOSTKEY_STRING_SIZE];
524             RecordChange(ctx, pp, attr, "%s %s hash for '%s' (%s)",
525                          action, HashNameFromId(type), filename,
526                          HashPrintSafe(buffer, sizeof(buffer), digest, type, true));
527             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
528 
529             WriteHash(dbp, type, filename, digest);
530             ret = found;
531         }
532         else
533         {
534             /* FIXME: FAIL if found?!?!?! */
535             RecordFailure(ctx, pp, attr, "Hash for file '%s' changed, but not updating the records", filename);
536             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
537             ret = true;
538         }
539     }
540     else
541     {
542         RecordNoChange(ctx, pp, attr, "File hash for %s is correct", filename);
543         *result = PromiseResultUpdate(*result, PROMISE_RESULT_NOOP);
544         ret = false;
545     }
546 
547     CloseDB(dbp);
548     return ret;
549 }
550 
FileChangesLogNewFile(const char * path,const Promise * pp)551 bool FileChangesLogNewFile(const char *path, const Promise *pp)
552 {
553     Log(LOG_LEVEL_NOTICE, "New file '%s' found", path);
554     return FileChangesLogChange(path, FILE_STATE_NEW, "New file found", pp);
555 }
556 
557 // db_file_set should already be sorted.
FileChangesCheckAndUpdateDirectory(EvalContext * ctx,const Attributes * attr,const char * name,const Seq * file_set,const Seq * db_file_set,bool update,const Promise * pp,PromiseResult * result)558 void FileChangesCheckAndUpdateDirectory(EvalContext *ctx, const Attributes *attr,
559                                         const char *name, const Seq *file_set, const Seq *db_file_set,
560                                         bool update, const Promise *pp, PromiseResult *result)
561 {
562     CF_DB *db;
563     if (!OpenChangesDB(&db))
564     {
565         RecordFailure(ctx, pp, attr, "Could not open changes database");
566         *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
567         return;
568     }
569 
570     Seq *disk_file_set = SeqSoftSort(file_set, StrCmpWrapper, NULL);
571 
572     // We'll traverse the union of disk_file_set and db_file_set in merged order.
573 
574     int num_files = SeqLength(disk_file_set);
575     int num_db_files = SeqLength(db_file_set);
576     for (int disk_pos = 0, db_pos = 0; disk_pos < num_files || db_pos < num_db_files;)
577     {
578         int compare_result;
579         if (disk_pos >= num_files)
580         {
581             compare_result = 1;
582         }
583         else if (db_pos >= num_db_files)
584         {
585             compare_result = -1;
586         }
587         else
588         {
589             compare_result = strcmp(SeqAt(disk_file_set, disk_pos), SeqAt(db_file_set, db_pos));
590         }
591 
592         if (compare_result < 0)
593         {
594             /*
595               We would have called this here, but we assume that DepthSearch()
596               has already done it for us. The reason is that calling it here
597               produces a very unnatural order, with all stat and content
598               changes, as well as all subdirectories, appearing in the log
599               before the message about a new file. This is because we save the
600               list for last and *then* compare it to the saved directory list,
601               *after* traversing the tree. So we let DepthSearch() do it while
602               traversing instead. Removed files will still be listed last.
603             */
604 #if 0
605             char *file = SeqAt(disk_file_set, disk_pos);
606             char path[strlen(name) + strlen(file) + 2];
607             xsnprintf(path, sizeof(path), "%s/%s", name, file);
608             FileChangesLogNewFile(path, pp);
609 #endif
610 
611             /* just make sure the change is reflected in the promise result */
612             *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
613             disk_pos++;
614         }
615         else if (compare_result > 0)
616         {
617             char *db_file = SeqAt(db_file_set, db_pos);
618             char path[strlen(name) + strlen(db_file) + 2];
619             xsnprintf(path, sizeof(path), "%s/%s", name, db_file);
620 
621             Log(LOG_LEVEL_NOTICE, "File '%s' no longer exists", path);
622             if (MakingInternalChanges(ctx, pp, attr, result,
623                                       "record removal of '%s'", path))
624             {
625                 if (FileChangesLogChange(path, FILE_STATE_REMOVED, "File removed", pp))
626                 {
627                     RecordChange(ctx, pp, attr, "Removal of '%s' recorded", path);
628                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
629                 }
630                 else
631                 {
632                     RecordFailure(ctx, pp, attr, "Failed to record removal of '%s'", path);
633                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
634                 }
635             }
636 
637             RemoveAllFileTraces(db, path);
638 
639             db_pos++;
640         }
641         else
642         {
643             // DB file entry and filesystem file entry matched.
644             disk_pos++;
645             db_pos++;
646         }
647     }
648 
649     if (MakingInternalChanges(ctx, pp, attr, result,
650                               "record directory listing for '%s'", name))
651     {
652         bool changes = false;
653         if (update && FileChangesSetDirectoryList(db, name, disk_file_set, &changes))
654         {
655             if (changes)
656             {
657                 RecordChange(ctx, pp, attr, "Recorded directory listing for '%s'", name);
658                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
659             }
660         }
661         else
662         {
663             RecordChange(ctx, pp, attr, "Failed to record directory listing for '%s'", name);
664             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
665         }
666     }
667 
668     SeqSoftDestroy(disk_file_set);
669     CloseDB(db);
670 }
671 
FileChangesCheckAndUpdateStats(EvalContext * ctx,const char * file,const struct stat * sb,bool update,const Attributes * attr,const Promise * pp,PromiseResult * result)672 void FileChangesCheckAndUpdateStats(EvalContext *ctx,
673                                     const char *file,
674                                     const struct stat *sb,
675                                     bool update,
676                                     const Attributes *attr,
677                                     const Promise *pp,
678                                     PromiseResult *result)
679 {
680     struct stat cmpsb;
681     CF_DB *dbp;
682 
683     if (!OpenChangesDB(&dbp))
684     {
685         RecordFailure(ctx, pp, attr, "Could not open changes database");
686         *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
687         return;
688     }
689 
690     char key[strlen(file) + 3];
691     xsnprintf(key, sizeof(key), "S_%s", file);
692 
693     if (!ReadDB(dbp, key, &cmpsb, sizeof(struct stat)))
694     {
695         if (MakingInternalChanges(ctx, pp, attr, result,
696                                   "write stat information for '%s' to database", file))
697         {
698             if (!WriteDB(dbp, key, sb, sizeof(struct stat)))
699             {
700                 RecordFailure(ctx, pp, attr, "Could not write stat information for '%s' to database", file);
701                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
702             }
703             else
704             {
705                 RecordChange(ctx, pp, attr, "Wrote stat information for '%s' to database", file);
706                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
707             }
708         }
709 
710         CloseDB(dbp);
711         return;
712     }
713 
714     if (cmpsb.st_mode == sb->st_mode
715         && cmpsb.st_uid == sb->st_uid
716         && cmpsb.st_gid == sb->st_gid
717         && cmpsb.st_dev == sb->st_dev
718         && cmpsb.st_ino == sb->st_ino
719         && cmpsb.st_mtime == sb->st_mtime)
720     {
721         RecordNoChange(ctx, pp, attr, "No stat information change for '%s'", file);
722         CloseDB(dbp);
723         return;
724     }
725 
726     if (cmpsb.st_mode != sb->st_mode)
727     {
728         Log(LOG_LEVEL_NOTICE, "Permissions for '%s' changed %04jo -> %04jo",
729                  file, (uintmax_t)cmpsb.st_mode, (uintmax_t)sb->st_mode);
730 
731         char msg_temp[CF_MAXVARSIZE];
732         snprintf(msg_temp, sizeof(msg_temp), "Permission: %04jo -> %04jo",
733                  (uintmax_t)cmpsb.st_mode, (uintmax_t)sb->st_mode);
734 
735         if (MakingInternalChanges(ctx, pp, attr, result, "record permissions changes in '%s'", file))
736         {
737             if (FileChangesLogChange(file, FILE_STATE_STATS_CHANGED, msg_temp, pp))
738             {
739                 RecordChange(ctx, pp, attr, "Recorded permissions changes in '%s'", file);
740                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
741             }
742             else
743             {
744                 RecordFailure(ctx, pp, attr, "Failed to record permissions changes in '%s'", file);
745                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
746             }
747         }
748     }
749 
750     if (cmpsb.st_uid != sb->st_uid)
751     {
752         Log(LOG_LEVEL_NOTICE, "Owner for '%s' changed %ju -> %ju",
753             file, (uintmax_t) cmpsb.st_uid, (uintmax_t) sb->st_uid);
754 
755         char msg_temp[CF_MAXVARSIZE];
756         snprintf(msg_temp, sizeof(msg_temp), "Owner: %ju -> %ju",
757                  (uintmax_t) cmpsb.st_uid, (uintmax_t) sb->st_uid);
758 
759         if (MakingInternalChanges(ctx, pp, attr, result,
760                                   "record ownership changes in '%s'", file))
761         {
762             if (FileChangesLogChange(file, FILE_STATE_STATS_CHANGED, msg_temp, pp))
763             {
764                 RecordChange(ctx, pp, attr, "Recorded ownership changes in '%s'", file);
765                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
766             }
767             else
768             {
769                 RecordFailure(ctx, pp, attr, "Failed to record ownership changes in '%s'", file);
770                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
771             }
772         }
773     }
774 
775     if (cmpsb.st_gid != sb->st_gid)
776     {
777         Log(LOG_LEVEL_NOTICE, "Group for '%s' changed %ju -> %ju",
778             file, (uintmax_t) cmpsb.st_gid, (uintmax_t) sb->st_gid);
779 
780         char msg_temp[CF_MAXVARSIZE];
781         snprintf(msg_temp, sizeof(msg_temp), "Group: %ju -> %ju",
782                  (uintmax_t)cmpsb.st_gid, (uintmax_t)sb->st_gid);
783 
784         if (MakingInternalChanges(ctx, pp, attr, result,
785                                   "record group changes in '%s'", file))
786         {
787             if (FileChangesLogChange(file, FILE_STATE_STATS_CHANGED, msg_temp, pp))
788             {
789                 RecordChange(ctx, pp, attr, "Recorded group changes in '%s'", file);
790                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
791             }
792             else
793             {
794                 RecordFailure(ctx, pp, attr, "Failed to record group changes in '%s'", file);
795                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
796             }
797         }
798     }
799 
800     if (cmpsb.st_dev != sb->st_dev)
801     {
802         Log(LOG_LEVEL_NOTICE, "Device for '%s' changed %ju -> %ju",
803             file, (uintmax_t) cmpsb.st_dev, (uintmax_t) sb->st_dev);
804 
805         char msg_temp[CF_MAXVARSIZE];
806         snprintf(msg_temp, sizeof(msg_temp), "Device: %ju -> %ju",
807                  (uintmax_t)cmpsb.st_dev, (uintmax_t)sb->st_dev);
808 
809         if (MakingInternalChanges(ctx, pp, attr, result, "record device changes in '%s'", file))
810         {
811             if (FileChangesLogChange(file, FILE_STATE_STATS_CHANGED, msg_temp, pp))
812             {
813                 RecordChange(ctx, pp, attr, "Recorded device changes in '%s'", file);
814                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
815             }
816             else
817             {
818                 RecordFailure(ctx, pp, attr, "Failed to record device changes in '%s'", file);
819                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
820             }
821         }
822     }
823 
824     if (cmpsb.st_ino != sb->st_ino)
825     {
826         Log(LOG_LEVEL_NOTICE, "inode for '%s' changed %ju -> %ju",
827             file, (uintmax_t) cmpsb.st_ino, (uintmax_t) sb->st_ino);
828     }
829 
830     if (cmpsb.st_mtime != sb->st_mtime)
831     {
832         char from[25]; // ctime() string is 26 bytes (incl NUL)
833         char to[25];   // we ignore the newline at the end
834         // Example: "Thu Nov 24 18:22:48 1986\n"
835 
836         // TODO: Should be possible using memcpy
837         //       but I ran into some weird issues when trying
838         //       to assert the contents of ctime()
839         StringCopy(ctime(&(cmpsb.st_mtime)), from, 25);
840         StringCopy(ctime(&(sb->st_mtime)), to, 25);
841 
842         assert(strlen(from) == 24);
843         assert(strlen(to) == 24);
844 
845         Log(LOG_LEVEL_NOTICE, "Last modified time for '%s' changed '%s' -> '%s'", file, from, to);
846 
847         char msg_temp[CF_MAXVARSIZE];
848         snprintf(msg_temp, sizeof(msg_temp), "Modified time: %s -> %s",
849                  from, to);
850 
851         if (MakingInternalChanges(ctx, pp, attr, result, "record mtime changes in '%s'", file))
852         {
853             if (FileChangesLogChange(file, FILE_STATE_STATS_CHANGED, msg_temp, pp))
854             {
855                 RecordChange(ctx, pp, attr, "Recorded mtime changes in '%s'", file);
856                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
857             }
858             else
859             {
860                 RecordFailure(ctx, pp, attr, "Failed to record mtime changes in '%s'", file);
861                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
862             }
863         }
864     }
865 
866     if (pp->comment)
867     {
868         Log(LOG_LEVEL_VERBOSE, "Preceding promise '%s'", pp->comment);
869     }
870 
871     if (update)
872     {
873         if (MakingInternalChanges(ctx, pp, attr, result,
874                                   "write stat information for '%s' to database", file))
875         {
876             if (!DeleteDB(dbp, key) || !WriteDB(dbp, key, sb, sizeof(struct stat)))
877             {
878                 RecordFailure(ctx, pp, attr, "Failed to write stat information for '%s' to database", file);
879                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
880             }
881             else
882             {
883                 RecordChange(ctx, pp, attr, "Wrote stat information changes for '%s' to database", file);
884                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
885             }
886         }
887     }
888 
889     CloseDB(dbp);
890 }
891 
FileStateToChar(FileState status)892 static char FileStateToChar(FileState status)
893 {
894     switch(status)
895     {
896     case FILE_STATE_NEW:
897         return 'N';
898 
899     case FILE_STATE_REMOVED:
900         return 'R';
901 
902     case FILE_STATE_CONTENT_CHANGED:
903         return 'C';
904 
905     case FILE_STATE_STATS_CHANGED:
906         return 'S';
907 
908     default:
909         ProgrammingError("Unhandled file status in switch: %d", status);
910     }
911 }
912 
FileChangesLogChange(const char * file,FileState status,char * msg,const Promise * pp)913 bool FileChangesLogChange(const char *file, FileState status, char *msg, const Promise *pp)
914 {
915     char fname[CF_BUFSIZE];
916     time_t now = time(NULL);
917 
918 /* This is inefficient but we don't want to lose any data */
919 
920     snprintf(fname, CF_BUFSIZE, "%s/%s", GetStateDir(), CF_FILECHANGE_NEW);
921     MapName(fname);
922 
923 #ifndef __MINGW32__
924     struct stat sb;
925     if (stat(fname, &sb) != -1)
926     {
927         if (sb.st_mode & (S_IWGRP | S_IWOTH))
928         {
929             Log(LOG_LEVEL_ERR, "File '%s' (owner %ju) was writable by others (security exception)", fname, (uintmax_t)sb.st_uid);
930         }
931     }
932 #endif /* !__MINGW32__ */
933 
934     FILE *fp = safe_fopen(fname, "a");
935     if (fp == NULL)
936     {
937         Log(LOG_LEVEL_ERR, "Could not write to the hash change log. (fopen: %s)", GetErrorStr());
938         return false;
939     }
940 
941     const char *handle = PromiseID(pp);
942 
943     fprintf(fp, "%lld,%s,%s,%c,%s\n", (long long) now, handle, file, FileStateToChar(status), msg);
944     fclose(fp);
945     return true;
946 }
947