diff --git a/aide.spec b/aide.spec index ee5b4041dfda86008fd0fa04d80195b464a7cc04..7eedc06a786273ba6259bab948f365c9bb1c4d3a 100644 --- a/aide.spec +++ b/aide.spec @@ -2,7 +2,7 @@ Name: aide Version: 0.19.1 -Release: 1 +Release: 2 Summary: Advanced Intrusion Detection Environment License: GPLv2+ URL: https://sourceforge.net/projects/aide @@ -22,6 +22,8 @@ BuildRequires: check-devel Requires: libgcrypt-sm3 Patch0: Add-sm3-algorithm-for-aide.patch +Patch1: backport-CVE-2025-54389.patch +Patch2: backport-CVE-2025-54409.patch %description AIDE (Advanced Intrusion Detection Environment, [eyd]) is a file and directory integrity checker. @@ -75,6 +77,12 @@ make check %{_mandir}/*/* %changelog +* Fri Aug 15 2025 yujingbo - 0.19.1-2 +- Type:CVE +- ID:CVE-2025-54389 CVE-2025-54409 +- SUG:NA +- DESC: fix CVE-2025-54389 CVE-2025-54409 + * Fri Jul 11 2025 yixiangzhike - 0.19.1-1 - Type: enhancement - ID: NA diff --git a/backport-CVE-2025-54389.patch b/backport-CVE-2025-54389.patch new file mode 100644 index 0000000000000000000000000000000000000000..93faf685f49e2aed4e63b7735bf253de7943e99f --- /dev/null +++ b/backport-CVE-2025-54389.patch @@ -0,0 +1,432 @@ +From 64c8f32b0349c33fb8382784af468338078851f9 Mon Sep 17 00:00:00 2001 +From: Hannes von Haugwitz +Date: Thu, 7 Aug 2025 18:04:41 +0200 +Subject: [PATCH] Escape control characters in report and log output + +* this addresses CVE-2025-54389 +* thanks to Rajesh Pangare for reporting this issue +--- + ChangeLog | 4 ++++ + doc/aide.1 | 13 +++++++++++ + include/util.h | 3 +++ + src/aide.c | 13 +++++++---- + src/gen_list.c | 20 ++++++++++------- + src/log.c | 22 ++++++++++++++++-- + src/progress.c | 2 +- + src/report_json.c | 9 ++++++-- + src/report_plain.c | 56 +++++++++++++++++++++++++++++++--------------- + src/util.c | 34 ++++++++++++++++++++++++++++ + 10 files changed, 141 insertions(+), 35 deletions(-) + +diff --git a/ChangeLog b/ChangeLog +index 4f69d239..5242e8ff 100644 +--- a/ChangeLog ++++ b/ChangeLog +@@ -1,3 +1,7 @@ ++2025-08-07 Hannes von Haugwitz ++ * Escape control characters in report and log output (CVE-2025-54389), ++ thanks to Rajesh Pangare for reporting this issue ++ + 2025-07-06 Hannes von Haugwitz + * Release aide 0.19.1 + +diff --git a/doc/aide.1 b/doc/aide.1 +index c6b274c5..8572ee24 100644 +--- a/doc/aide.1 ++++ b/doc/aide.1 +@@ -158,12 +158,25 @@ Resize the progress bar (if enabled). + .PP + .SH NOTES + ++.IP "Checksum encoding" ++ + The checksums in the database and in the output are by default base64 + encoded (see also report_base16 option). + To decode them you can use the following shell command: + + echo | base64 \-d | hexdump \-v \-e '32/1 "%02x" "\\n"' + ++.IP "Control characters" ++ ++Control characters (00-31 and 127) are always escaped in log and plain report ++output. They are escaped by a literal backslash (\\) followed by exactly 3 ++digits representing the character in octal notation (e.g. a newline is output ++as "\fB\\012\fR"). A literal backslash is not escaped unless it is followed by ++3 digits (0-9), in this case the literal backslash is escaped as ++"\fB\\134\fR". Reports in JSON format are escaped according to the JSON specs ++(e.g. a newline is output as "\fB\\b\fR" or an escape (\fBESC\fR) is output as ++"\fB\\u001b\fR") ++ + .PP + .SH FILES + +diff --git a/include/util.h b/include/util.h +index d4c53fcc..4340562e 100644 +--- a/include/util.h ++++ b/include/util.h +@@ -89,6 +89,9 @@ int cmpurl(url_t*, url_t*); + + int contains_unsafe(const char*); + ++char *strnesc(const char *, size_t); ++char *stresc(const char *); ++ + void decode_string(char*); + + char* encode_string(const char*); +diff --git a/src/aide.c b/src/aide.c +index 2a5ec9c9..622e1085 100644 +--- a/src/aide.c ++++ b/src/aide.c +@@ -302,7 +302,8 @@ static void read_param(int argc,char**argv) + if((conf->limit_crx=pcre2_compile((PCRE2_SPTR) conf->limit, PCRE2_ZERO_TERMINATED, PCRE2_UTF|PCRE2_ANCHORED, &pcre2_errorcode, &pcre2_erroffset, NULL)) == NULL) { + PCRE2_UCHAR pcre2_error[128]; + pcre2_get_error_message(pcre2_errorcode, pcre2_error, 128); +- INVALID_ARGUMENT("--limit", error in regular expression '%s' at %zu: %s, conf->limit, pcre2_erroffset, pcre2_error) ++ char * limit_safe = stresc(conf->limit); ++ INVALID_ARGUMENT("--limit", error in regular expression '%s' at %zu: %s, limit_safe, pcre2_erroffset, pcre2_error) + + } + conf->limit_md = pcre2_match_data_create_from_pattern(conf->limit_crx, NULL); +@@ -649,14 +650,16 @@ static void list_attribute(db_line* entry, ATTRIBUTE attribute) { + + i = 0; + while (i= 0) { + c = k*(p-1); +- fprintf(stdout," %-*s%c %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p-1, olen-c>0?&value[i][c]:""); ++ fprintf(stdout," %-*s%c %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p-1, olen-c>0?&ovalue[c]:""); + k++; + } + ++i; ++ free(ovalue); + } + for(i=0; i < num; ++i) { free(value[i]); value[i]=NULL; } free(value); value=NULL; + } +@@ -810,7 +813,9 @@ int main(int argc,char**argv) + db_entry_t entry; + while((entry = db_readline(&(conf->database_in), false)).line != NULL) { + log_msg(LOG_LEVEL_RULE, "\u252c process '%s' (filetype: %c)", (entry.line)->filename, get_f_type_char_from_perm((entry.line)->perm)); +- fprintf(stdout, "%s\n", (entry.line)->filename); ++ char *entry_safe = stresc((entry.line)->filename); ++ fprintf(stdout, "%s\n", entry_safe); ++ free(entry_safe); + for (int j=0; j < report_attrs_order_length; ++j) { + switch(report_attrs_order[j]) { + case attr_allhashsums: +diff --git a/src/gen_list.c b/src/gen_list.c +index 7564fa8f..f8364409 100644 +--- a/src/gen_list.c ++++ b/src/gen_list.c +@@ -344,14 +344,14 @@ static DB_ATTR_TYPE get_different_attributes(db_line* l1, db_line* l2, DB_ATTR_T + #define PRINT_RULE_MATCH(format, c, ...) \ + if (file.fs_type) { \ + fs_type_str = get_fs_type_string_from_magic(file.fs_type); \ +- fprintf(stdout, "[%c] %c=%s:%s: " format "\n", c, file_type, fs_type_str, file.name, __VA_ARGS__); \ ++ fprintf(stdout, "[%c] %c=%s:%s: " format "\n", c, file_type, fs_type_str, filename_safe, __VA_ARGS__); \ + free(fs_type_str); \ + } else { \ +- fprintf(stdout, "[%c] %c:%s: " format "\n", c, file_type, file.name, __VA_ARGS__); \ ++ fprintf(stdout, "[%c] %c:%s: " format "\n", c, file_type, filename_safe, __VA_ARGS__); \ + } + #else + #define PRINT_RULE_MATCH(format, c, ...) \ +- fprintf(stdout, "[%c] %c:%s: " format "\n", c, file_type, file.name, __VA_ARGS__); ++ fprintf(stdout, "[%c] %c:%s: " format "\n", c, file_type, filename_safe, __VA_ARGS__); + #endif + + void print_match(file_t file, match_t match) { +@@ -362,6 +362,8 @@ void print_match(file_t file, match_t match) { + char *fs_type_str = NULL; + #endif + rx_rule *rule = match.rule; ++ char *filename_safe = stresc(file.name); ++ char *limit_safe = conf->limit?stresc(conf->limit):NULL; + switch (match.result) { + case RESULT_SELECTIVE_MATCH: + case RESULT_EQUAL_MATCH: +@@ -379,7 +381,7 @@ void print_match(file_t file, match_t match) { + break; + case RESULT_NEGATIVE_PARENT_MATCH: + str = get_restriction_string(rule->restriction); +- PRINT_RULE_MATCH("parent directory '%.*s' matches %s: '%s%s %s' (%s:%d: '%s%s%s')", ' ', match.length, file.name, get_rule_type_long_string(rule->type), get_rule_type_char(rule->type), rule->rx, str, rule->config_filename, rule->config_linenumber, rule->config_line, rule->prefix?"', prefix: '":"", rule->prefix?rule->prefix:"") ++ PRINT_RULE_MATCH("parent directory '%.*s' matches %s: '%s%s %s' (%s:%d: '%s%s%s')", ' ', match.length, filename_safe, get_rule_type_long_string(rule->type), get_rule_type_char(rule->type), rule->rx, str, rule->config_filename, rule->config_linenumber, rule->config_line, rule->prefix?"', prefix: '":"", rule->prefix?rule->prefix:"") + free(str); + break; + case RESULT_PARTIAL_MATCH: +@@ -387,21 +389,23 @@ void print_match(file_t file, match_t match) { + PRINT_RULE_MATCH("%s", ' ', "no matching rule") + break; + case RESULT_PARTIAL_LIMIT_MATCH: +- PRINT_RULE_MATCH("parital limit match (limit '%s')", ' ', conf->limit); ++ PRINT_RULE_MATCH("parital limit match (limit '%s')", ' ', limit_safe); + break; + case RESULT_PART_LIMIT_AND_NO_RECURSE_MATCH: + if (rule) { + str = get_restriction_string(rule->restriction); +- PRINT_RULE_MATCH("partial limit match (limit '%s') but %s: '%s%s %s' (%s:%d: '%s%s%s')", ' ', conf->limit, get_rule_type_long_string(rule->type), get_rule_type_char(rule->type), rule->rx, str, rule->config_filename, rule->config_linenumber, rule->config_line, rule->prefix?"', prefix: '":"", rule->prefix?rule->prefix:"") ++ PRINT_RULE_MATCH("partial limit match (limit '%s') but %s: '%s%s %s' (%s:%d: '%s%s%s')", ' ', limit_safe, get_rule_type_long_string(rule->type), get_rule_type_char(rule->type), rule->rx, str, rule->config_filename, rule->config_linenumber, rule->config_line, rule->prefix?"', prefix: '":"", rule->prefix?rule->prefix:"") + free(str); + } else { +- PRINT_RULE_MATCH("partial limit match (limit '%s') but no matching rule", ' ', conf->limit) ++ PRINT_RULE_MATCH("partial limit match (limit '%s') but no matching rule", ' ', limit_safe) + } + break; + case RESULT_NO_LIMIT_MATCH: +- PRINT_RULE_MATCH("outside of limit '%s'", ' ', conf->limit); ++ PRINT_RULE_MATCH("outside of limit '%s'", ' ', limit_safe); + break; + } ++ free(filename_safe); ++ free(limit_safe); + } + + /* +diff --git a/src/log.c b/src/log.c +index 9f4ea370..9bf55801 100644 +--- a/src/log.c ++++ b/src/log.c +@@ -117,7 +117,9 @@ static void log_cached_lines(void) { + for(int i = 0; i < ncachedlines; ++i) { + LOG_LEVEL level = cached_lines[i].level; + if (level == LOG_LEVEL_ERROR || level <= log_level) { +- stderr_msg("%s: %s\n", get_log_string(level), cached_lines[i].message); ++ char *msg_safe = stresc(cached_lines[i].message); ++ stderr_msg("%s: %s\n", get_log_string(level), msg_safe); ++ free(msg_safe); + } + free(cached_lines[i].message); + } +@@ -137,7 +139,23 @@ static void vlog_msg(LOG_LEVEL level,const char* format, va_list ap) { + cache_line(level, format, ap); + pthread_mutex_unlock(&log_mutex); + } else if (level == LOG_LEVEL_ERROR || level <= log_level) { +- vstderr_prefix_line(get_log_string(level), format, ap); ++ va_list aq; ++ va_copy(aq, ap); ++ int n = vsnprintf(NULL, 0, format, aq) + 1; ++ va_end(aq); ++ ++ int size = n * sizeof(char); ++ char *msg_unsafe = malloc(size); ++ if (msg_unsafe == NULL) { ++ stderr_msg("%s: malloc: failed to allocate %d bytes of memory\n", get_log_string(LOG_LEVEL_ERROR), size); ++ exit(MEMORY_ALLOCATION_FAILURE); ++ } ++ ++ vsnprintf(msg_unsafe, n, format, ap); ++ char *msg_safe = stresc(msg_unsafe); ++ free(msg_unsafe); ++ stderr_msg("%s: %s\n", get_log_string(level), msg_safe); ++ free(msg_safe); + } + } + +diff --git a/src/progress.c b/src/progress.c +index ea85a68d..940b84a6 100644 +--- a/src/progress.c ++++ b/src/progress.c +@@ -202,7 +202,7 @@ void progress_status(progress_state new_state, const char* data) { + free(path); + path = NULL; + if (data) { +- path = checked_strdup(data); ++ path = stresc(data); + } + break; + case PROGRESS_SKIPPED: +diff --git a/src/report_json.c b/src/report_json.c +index 4a4f4850..f9ed737c 100644 +--- a/src/report_json.c ++++ b/src/report_json.c +@@ -96,8 +96,13 @@ static int _escape_json_string(const char *src, char *escaped_string) { + n++; + break; + default: +- if (escaped_string) { escaped_string[n] = src[i]; } +- n++; ++ if (src[i] >= 0 && (src[i] < 0x1f || src[i] == 0x7f)) { ++ if (escaped_string) { snprintf(&escaped_string[n], 7, "\\u%04x", src[i]); } ++ n += 6; ++ } else { ++ if (escaped_string) { escaped_string[n] = src[i]; } ++ n++; ++ } + } + } + if (escaped_string) { escaped_string[n] = '\0'; } +diff --git a/src/report_plain.c b/src/report_plain.c +index 14f9b149..83cdd39b 100644 +--- a/src/report_plain.c ++++ b/src/report_plain.c +@@ -53,7 +53,9 @@ static char* _get_not_grouped_list_string(report_t *report) { + static void _print_config_option(report_t *report, config_option option, const char* value) { + if (first) { first=false; } + else { report_printf(report," | "); } +- report_printf(report, "%s: %s", config_options[option].report_string, value); ++ char *value_safe = stresc(value); ++ report_printf(report, "%s: %s", config_options[option].report_string, value_safe); ++ free(value_safe); + } + + static void _print_report_option(report_t *report, config_option option, const char* value) { +@@ -61,37 +63,49 @@ static void _print_report_option(report_t *report, config_option option, const c + } + + static void _print_attribute(report_t *report, db_line* oline, db_line* nline, ATTRIBUTE attribute) { +- char **ovalue = NULL; +- char **nvalue = NULL; ++ char **ovalues = NULL; ++ char **nvalues = NULL; + int onumber, nnumber, i, c; + int p = (conf->print_details_width-(4 + MAX_WIDTH_DETAILS_STRING))/2; + + DB_ATTR_TYPE attr = ATTR(attribute); + const char* name = attributes[attribute].details_string; + +- onumber=get_attribute_values(attr, oline, &ovalue, report); +- nnumber=get_attribute_values(attr, nline, &nvalue, report); ++ onumber=get_attribute_values(attr, oline, &ovalues, report); ++ nnumber=get_attribute_values(attr, nline, &nvalues, report); + + i = 0; + while (i= 0 || nlen-p*k >= 0) { + c = k*(p-1); + if (!onumber) { +- report_printf(report," %-*s%c %-*c %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p, ' ', p-1, nlen-c>0?&nvalue[i][c]:""); ++ report_printf(report," %-*s%c %-*c %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p, ' ', p-1, nlen-c>0?&nvalue[c]:""); + } else if (!nnumber) { +- report_printf(report," %-*s%c %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p-1, olen-c>0?&ovalue[i][c]:""); ++ report_printf(report," %-*s%c %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p-1, olen-c>0?&ovalue[c]:""); + } else { +- report_printf(report," %-*s%c %-*.*s| %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p, p-1, olen-c>0?&ovalue[i][c]:"", p-1, nlen-c>0?&nvalue[i][c]:""); ++ report_printf(report," %-*s%c %-*.*s| %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p, p-1, olen-c>0?&ovalue[c]:"", p-1, nlen-c>0?&nvalue[c]:""); + } + k++; + } + ++i; ++ free(ovalue); ++ free(nvalue); + } +- for(i=0; i < onumber; ++i) { free(ovalue[i]); ovalue[i]=NULL; } free(ovalue); ovalue=NULL; +- for(i=0; i < nnumber; ++i) { free(nvalue[i]); nvalue[i]=NULL; } free(nvalue); nvalue=NULL; ++ for(i=0; i < onumber; ++i) { free(ovalues[i]); ovalues[i]=NULL; } free(ovalues); ovalues=NULL; ++ for(i=0; i < nnumber; ++i) { free(nvalues[i]); nvalues[i]=NULL; } free(nvalues); nvalues=NULL; + } + + static void _print_database_attributes(report_t *report, db_line* db) { +@@ -134,19 +148,21 @@ static void print_report_summary_plain(report_t *report) { + } + + static void print_line_plain(report_t* report, char* filename, int node_checked, seltree* node) { ++ char *filename_safe = stresc(filename); + if(report->summarize_changes) { + char* summary = get_summarize_changes_string(report, node); +- report_printf(report, "\n%s: %s", summary, filename); ++ report_printf(report, "\n%s: %s", summary, filename_safe); + free(summary); summary=NULL; + } else { + if (node_checked&NODE_ADDED) { +- report_printf(report, _("\nadded: %s"), filename); ++ report_printf(report, _("\nadded: %s"), filename_safe); + } else if (node_checked&NODE_REMOVED) { +- report_printf(report, _("\nremoved: %s"), filename); ++ report_printf(report, _("\nremoved: %s"), filename_safe); + } else if (node_checked&NODE_CHANGED) { +- report_printf(report, _("\nchanged: %s"), filename); ++ report_printf(report, _("\nchanged: %s"), filename_safe); + } + } ++ free(filename_safe); + } + + static void print_report_dbline_attributes_plain(report_t *report, db_line* oline, db_line* nline, DB_ATTR_TYPE report_attrs) { +@@ -156,7 +172,9 @@ static void print_report_dbline_attributes_plain(report_t *report, db_line* olin + if (line->perm) { + report_printf(report, "%s: ", get_file_type_string(line->perm)); + } +- report_printf(report, "%s\n", line->filename); ++ char *filename_safe = stresc(line->filename); ++ report_printf(report, "%s\n", filename_safe); ++ free(filename_safe); + + print_dbline_attrs(report, oline, nline, report_attrs, _print_attribute); + } +@@ -193,9 +211,11 @@ static void print_report_details_plain(report_t *report, seltree* node) { + static void print_report_diff_attrs_entries_plain(report_t *report) { + for(int i = 0; i < report->num_diff_attrs_entries; ++i) { + char *str = NULL; ++ char *entry_safe = stresc(report->diff_attrs_entries[i].entry); + report_printf(report, "Entry %s in databases has different attributes: %s\n", +- report->diff_attrs_entries[i].entry, ++ entry_safe, + str= diff_attributes(report->diff_attrs_entries[i].old_attrs, report->diff_attrs_entries[i].new_attrs)); ++ free(entry_safe); + free(str); + } + report->num_diff_attrs_entries = 0; +diff --git a/src/util.c b/src/util.c +index 900b3f43..2df2c19d 100644 +--- a/src/util.c ++++ b/src/util.c +@@ -143,6 +143,40 @@ int cmpurl(url_t* u1,url_t* u2) + return RETOK; + } + ++static size_t escape_str(const char *unescaped_str, char *str, size_t s) { ++ size_t n = 0; ++ size_t i = 0; ++ char c; ++ while (i < s && (c = unescaped_str[i])) { ++ if ((c >= 0 && (c < 0x1f || c == 0x7f)) || ++ (c == '\\' && isdigit(unescaped_str[i+1]) ++ && isdigit(unescaped_str[i+2]) ++ && isdigit(unescaped_str[i+3]) ++ ) ) { ++ if (str) { snprintf(&str[n], 5, "\\%03o", c); } ++ n += 4; ++ } else { ++ if (str) { str[n] = c; } ++ n++; ++ } ++ i++; ++ } ++ if (str) { str[n] = '\0'; } ++ n++; ++ return n; ++} ++ ++char *strnesc(const char *unescaped_str, size_t s) { ++ int n = escape_str(unescaped_str, NULL, s); ++ char *str = checked_malloc(n); ++ escape_str(unescaped_str, str, s); ++ return str; ++} ++ ++char *stresc(const char *unescaped_str) { ++ return strnesc(unescaped_str, strlen(unescaped_str)); ++} ++ + /* Returns 1 if the string contains unsafe characters, 0 otherwise. */ + int contains_unsafe (const char *s) + { diff --git a/backport-CVE-2025-54409.patch b/backport-CVE-2025-54409.patch new file mode 100644 index 0000000000000000000000000000000000000000..8b01d52cf19298dbac9bfccb1baf5521f1edf9bd --- /dev/null +++ b/backport-CVE-2025-54409.patch @@ -0,0 +1,100 @@ +From 54a6d0d9d5f14b81961d66373c0291bf4af4135a Mon Sep 17 00:00:00 2001 +From: Hannes von Haugwitz +Date: Thu, 7 Aug 2025 18:08:25 +0200 +Subject: [PATCH] Fix null pointer dereference after reading incorrectly + encoded xattr attributes from database + +* fix handling of empty xattr values +* fix handling of xattr keys containing a comma +* this addresses CVE-2025-54409 +* thanks to Rajesh Pangare for reporting this issue +--- + ChangeLog | 5 +++++ + src/db.c | 26 ++++++++++++++++++-------- + src/db_file.c | 2 +- + src/util.c | 2 +- + 4 files changed, 25 insertions(+), 10 deletions(-) + +diff --git a/ChangeLog b/ChangeLog +index 5242e8f..6ee3a25 100644 +--- a/ChangeLog ++++ b/ChangeLog +@@ -1,6 +1,11 @@ + 2025-08-07 Hannes von Haugwitz + * Escape control characters in report and log output (CVE-2025-54389), + thanks to Rajesh Pangare for reporting this issue ++ * Fix null pointer dereference after reading incorrectly encoded xattr ++ attributes from database (CVE-2025-54409) ++ - fix handling of empty xattr values ++ - fix handling of xattr keys containing a comma ++ - thanks to Rajesh Pangare for reporting this issue + + 2025-07-06 Hannes von Haugwitz + * Release aide 0.19.1 +diff --git a/src/db.c b/src/db.c +index 6675e19..375b220 100644 +--- a/src/db.c ++++ b/src/db.c +@@ -374,18 +374,28 @@ db_line* db_char2line(char** ss, database* db){ + num = 0; + while (num < line->xattrs->num) + { +- byte *val = NULL; +- size_t vsz = 0; +- + tval = strtok(NULL, ","); + decode_string(tval); + line->xattrs->ents[num].key = checked_strdup(tval); + tval = strtok(NULL, ","); +- val = base64tobyte(tval, strlen(tval), &vsz); +- line->xattrs->ents[num].val = val; +- line->xattrs->ents[num].vsz = vsz; +- +- ++num; ++ if (strcmp(tval,"0") != 0) { ++ line->xattrs->ents[num].val = decode_base64(tval, strlen(tval), &line->xattrs->ents[num].vsz); ++ } else { ++ line->xattrs->ents[num].val = checked_strdup(""); ++ line->xattrs->ents[num].vsz = 0; ++ } ++ if (line->xattrs->ents[num].val == NULL) { ++ LOG_DB_FORMAT_LINE(LOG_LEVEL_WARNING, "error while reading xattrs for '%s' from database (discarding extended attributes)", line->filename) ++ for (int j = num; j >= 0 ; --j) { ++ free(line->xattrs->ents[j].key); ++ line->xattrs->ents[j].key = NULL; ++ free(line->xattrs->ents[j].val); ++ line->xattrs->ents[j].val = NULL; ++ } ++ line->xattrs->num = 0; ++ } else { ++ ++num; ++ } + } + } + #endif +diff --git a/src/db_file.c b/src/db_file.c +index e016e11..505f8af 100644 +--- a/src/db_file.c ++++ b/src/db_file.c +@@ -377,7 +377,7 @@ static int str_xattr(char *str, int n, xattrs_type *xattrs) { + enc_key = encode_string(xattr->key); + } + char *enc_value = encode_base64(xattr->val, xattr->vsz); +- m += str_format(str, n + m, ",%s,%s", enc_key?enc_key:xattr->key, enc_value); ++ m += str_format(str, n + m, ",%s,%s", enc_key?enc_key:xattr->key, enc_value?enc_value:"0"); + free(enc_key); + free(enc_value); + ++xattr; +diff --git a/src/util.c b/src/util.c +index 2df2c19..edc7453 100644 +--- a/src/util.c ++++ b/src/util.c +@@ -48,7 +48,7 @@ + #include "util.h" + #include "errorcodes.h" + +-#define URL_UNSAFE " <>\"#%{}|\\^~[]`@:\033'" ++#define URL_UNSAFE " <>\"#%{}|\\^~[]`@:\033'," + #define ISPRINT(c) (isascii(c) && isprint(c)) + + pthread_mutex_t stderr_mutex = PTHREAD_MUTEX_INITIALIZER;