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