From 117e21121e857b4b7d81949542e8dd257265970a Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Mon, 29 Nov 2021 10:24:44 +0100 Subject: [PATCH 1/3] journal: Add journal_file_read_object() --- src/libsystemd/sd-journal/journal-file.c | 59 ++++++++++++++++++++++++ src/libsystemd/sd-journal/journal-file.h | 1 + 2 files changed, 60 insertions(+) diff --git a/src/libsystemd/sd-journal/journal-file.c b/src/libsystemd/sd-journal/journal-file.c index 3722519dc4..7068b6bb17 100644 --- a/src/libsystemd/sd-journal/journal-file.c +++ b/src/libsystemd/sd-journal/journal-file.c @@ -990,6 +990,65 @@ int journal_file_move_to_object(JournalFile *f, ObjectType type, uint64_t offset return 0; } +int journal_file_read_object(JournalFile *f, ObjectType type, uint64_t offset, Object *ret) { + int r; + Object o; + uint64_t s; + + assert(f); + assert(ret); + + /* Objects may only be located at multiple of 64 bit */ + if (!VALID64(offset)) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), + "Attempt to read object at non-64bit boundary: %" PRIu64, + offset); + + /* Object may not be located in the file header */ + if (offset < le64toh(f->header->header_size)) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), + "Attempt to read object located in file header: %" PRIu64, + offset); + + /* This will likely read too much data but it avoids having to call pread() twice. */ + r = pread(f->fd, &o, sizeof(Object), offset); + if (r < 0) + return r; + + s = le64toh(o.object.size); + + if (s == 0) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), + "Attempt to read uninitialized object: %" PRIu64, + offset); + if (s < sizeof(ObjectHeader)) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), + "Attempt to read overly short object: %" PRIu64, + offset); + + if (o.object.type <= OBJECT_UNUSED) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), + "Attempt to read object with invalid type: %" PRIu64, + offset); + + if (s < minimum_header_size(&o)) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), + "Attempt to read truncated object: %" PRIu64, + offset); + + if (type > OBJECT_UNUSED && o.object.type != type) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), + "Attempt to read object of unexpected type: %" PRIu64, + offset); + + r = journal_file_check_object(f, offset, &o); + if (r < 0) + return r; + + *ret = o; + return 0; +} + static uint64_t journal_file_entry_seqnum( JournalFile *f, uint64_t *seqnum) { diff --git a/src/libsystemd/sd-journal/journal-file.h b/src/libsystemd/sd-journal/journal-file.h index 92784337c2..81b2ff23e1 100644 --- a/src/libsystemd/sd-journal/journal-file.h +++ b/src/libsystemd/sd-journal/journal-file.h @@ -202,6 +202,7 @@ static inline bool VALID_EPOCH(uint64_t u) { FLAGS_SET(le32toh((h)->incompatible_flags), HEADER_INCOMPATIBLE_KEYED_HASH) int journal_file_move_to_object(JournalFile *f, ObjectType type, uint64_t offset, Object **ret); +int journal_file_read_object(JournalFile *f, ObjectType type, uint64_t offset, Object *ret); uint64_t journal_file_entry_n_items(Object *o) _pure_; uint64_t journal_file_entry_array_n_items(Object *o) _pure_; From ab6e257b3e4e5b95f3750ed019bed6e89989e41b Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 16 Nov 2021 23:45:58 +0000 Subject: [PATCH 2/3] journal: truncate archived journals Journal files have space allocated in 8MiB-aligned increments. This can add up to substantial wasted space as many archived journals accumulate without using all the allocated space. Let's truncate journal files to their actually used size when archiving them to reclaim this unused space. As the mmap cache is not thread-safe, we can't call journal_file_move_to_object() from the offline thread. Instead, we use journal_file_read_object() which doesn't rely on the mmap cache. --- src/libsystemd/sd-journal/journal-file.c | 81 ++++++++++++++++++------ 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/src/libsystemd/sd-journal/journal-file.c b/src/libsystemd/sd-journal/journal-file.c index 7068b6bb17..655f9cf7e4 100644 --- a/src/libsystemd/sd-journal/journal-file.c +++ b/src/libsystemd/sd-journal/journal-file.c @@ -91,6 +91,59 @@ # pragma GCC diagnostic ignored "-Waddress-of-packed-member" #endif +static int journal_file_tail_end(JournalFile *f, uint64_t *ret_offset) { + Object tail; + uint64_t p; + int r; + + assert(f); + assert(f->header); + assert(ret_offset); + + p = le64toh(f->header->tail_object_offset); + if (p == 0) + p = le64toh(f->header->header_size); + else { + uint64_t sz; + + r = journal_file_read_object(f, OBJECT_UNUSED, p, &tail); + if (r < 0) + return r; + + sz = le64toh(tail.object.size); + if (sz > UINT64_MAX - sizeof(uint64_t) + 1) + return -EBADMSG; + + sz = ALIGN64(sz); + if (p > UINT64_MAX - sz) + return -EBADMSG; + + p += sz; + } + + *ret_offset = p; + + return 0; +} + +static int journal_file_truncate(JournalFile *f) { + uint64_t p; + int r; + + /* truncate excess from the end of archives */ + r = journal_file_tail_end(f, &p); + if (r < 0) + return log_debug_errno(r, "Failed to determine end of tail object: %m"); + + /* arena_size can't exceed the file size, ensure it's updated before truncating */ + f->header->arena_size = htole64(p - le64toh(f->header->header_size)); + + if (ftruncate(f->fd, p) < 0) + log_debug_errno(errno, "Failed to truncate %s: %m", f->path); + + return 0; +} + /* This may be called from a separate thread to prevent blocking the caller for the duration of fsync(). * As a result we use atomic operations on f->offline_state for inter-thread communications with * journal_file_set_offline() and journal_file_set_online(). */ @@ -117,6 +170,9 @@ static void journal_file_set_offline_internal(JournalFile *f) { break; case OFFLINE_SYNCING: + if (f->archive) + (void) journal_file_truncate(f); + (void) fsync(f->fd); if (!__sync_bool_compare_and_swap(&f->offline_state, OFFLINE_SYNCING, OFFLINE_OFFLINING)) @@ -1089,7 +1145,7 @@ int journal_file_append_object( int r; uint64_t p; - Object *tail, *o; + Object *o; void *t; assert(f); @@ -1101,26 +1157,9 @@ int journal_file_append_object( if (r < 0) return r; - p = le64toh(f->header->tail_object_offset); - if (p == 0) - p = le64toh(f->header->header_size); - else { - uint64_t sz; - - r = journal_file_move_to_object(f, OBJECT_UNUSED, p, &tail); - if (r < 0) - return r; - - sz = le64toh(READ_NOW(tail->object.size)); - if (sz > UINT64_MAX - sizeof(uint64_t) + 1) - return -EBADMSG; - - sz = ALIGN64(sz); - if (p > UINT64_MAX - sz) - return -EBADMSG; - - p += sz; - } + r = journal_file_tail_end(f, &p); + if (r < 0) + return r; r = journal_file_allocate(f, p, size); if (r < 0) From 6127ff987056ea3e80abc3724a4f397bb6314888 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 16 Nov 2021 23:54:16 +0000 Subject: [PATCH 3/3] journal: punch holes in unused parts of entry arrays when archiving Entry arrays grow exponentially, so when archiving a journal file is archived, it's very likely that the final entry array objects in each entry array chain aren't fully used. Let's punch holes in the unused parts so the filesystem can reclaim this unused space and use it for something else. --- src/libsystemd/sd-journal/journal-file.c | 89 +++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/src/libsystemd/sd-journal/journal-file.c b/src/libsystemd/sd-journal/journal-file.c index 655f9cf7e4..611c8c0d28 100644 --- a/src/libsystemd/sd-journal/journal-file.c +++ b/src/libsystemd/sd-journal/journal-file.c @@ -144,6 +144,91 @@ static int journal_file_truncate(JournalFile *f) { return 0; } +static int journal_file_entry_array_punch_hole(JournalFile *f, uint64_t p, uint64_t n_entries) { + Object o; + uint64_t offset, sz, n_items = 0, n_unused; + int r; + + if (n_entries == 0) + return 0; + + for (uint64_t q = p; q != 0; q = le64toh(o.entry_array.next_entry_array_offset)) { + r = journal_file_read_object(f, OBJECT_ENTRY_ARRAY, q, &o); + if (r < 0) + return r; + + n_items += journal_file_entry_array_n_items(&o); + p = q; + } + + if (p == 0) + return 0; + + if (n_entries > n_items) + return -EBADMSG; + + /* Amount of unused items in the final entry array. */ + n_unused = n_items - n_entries; + + if (n_unused == 0) + return 0; + + offset = p + offsetof(Object, entry_array.items) + + (journal_file_entry_array_n_items(&o) - n_unused) * sizeof(le64_t); + sz = p + le64toh(o.object.size) - offset; + + if (fallocate(f->fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, offset, sz) < 0) + return log_debug_errno(errno, "Failed to punch hole in entry array of %s: %m", f->path); + + return 0; +} + +static int journal_file_punch_holes(JournalFile *f) { + HashItem items[4096 / sizeof(HashItem)]; + uint64_t p, sz; + size_t to_read; + int r; + + r = journal_file_entry_array_punch_hole( + f, le64toh(f->header->entry_array_offset), le64toh(f->header->n_entries)); + if (r < 0) + return r; + + p = le64toh(f->header->data_hash_table_offset); + sz = le64toh(f->header->data_hash_table_size); + to_read = MIN((size_t) f->last_stat.st_blksize, sizeof(items)); + + for (uint64_t i = p; i < p + sz; i += sizeof(items)) { + ssize_t n_read; + + n_read = pread(f->fd, items, MIN(to_read, p + sz - i), i); + if (n_read < 0) + return n_read; + + for (size_t j = 0; j < (size_t) n_read / sizeof(HashItem); j++) { + Object o; + + for (uint64_t q = le64toh(items[j].head_hash_offset); q != 0; + q = le64toh(o.data.next_hash_offset)) { + + r = journal_file_read_object(f, OBJECT_DATA, q, &o); + if (r < 0) { + log_debug_errno(r, "Invalid data object: %m, ignoring"); + break; + } + + if (le64toh(o.data.n_entries) == 0) + continue; + + (void) journal_file_entry_array_punch_hole( + f, le64toh(o.data.entry_array_offset), le64toh(o.data.n_entries) - 1); + } + } + } + + return 0; +} + /* This may be called from a separate thread to prevent blocking the caller for the duration of fsync(). * As a result we use atomic operations on f->offline_state for inter-thread communications with * journal_file_set_offline() and journal_file_set_online(). */ @@ -170,8 +255,10 @@ static void journal_file_set_offline_internal(JournalFile *f) { break; case OFFLINE_SYNCING: - if (f->archive) + if (f->archive) { (void) journal_file_truncate(f); + (void) journal_file_punch_holes(f); + } (void) fsync(f->fd);