Skip to content

Comments

Fix Issue 1818: Heap UAF in syscheck decoder#2178

Merged
atomicturtle merged 4 commits intoossec:mainfrom
atomicturtle:260125-03
Jan 26, 2026
Merged

Fix Issue 1818: Heap UAF in syscheck decoder#2178
atomicturtle merged 4 commits intoossec:mainfrom
atomicturtle:260125-03

Conversation

@atomicturtle
Copy link
Member

  • Preserve program_name and hostname before freeing full_log
  • Set ownership flags (EF_FREE_PNAME, EF_FREE_HNAME) for proper cleanup
  • Fixes CVE-2020-8447
  • Includes regression test to verify fix

- Preserve program_name and hostname before freeing full_log
- Set ownership flags (EF_FREE_PNAME, EF_FREE_HNAME) for proper cleanup
- Fixes CVE-2020-8447
- Includes regression test to verify fix
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a heap use-after-free in the syscheck decoder where lf->program_name and lf->hostname could still reference memory inside lf->full_log after it is freed (CVE-2020-8447).

Changes:

  • Preserve program_name/hostname before full_log is freed and replaced during syscheck message rewriting.
  • Set EF_FREE_PNAME / EF_FREE_HNAME so Free_Eventinfo() cleans up the new allocations.
  • Add a regression test file intended to validate the fix.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/analysisd/decoders/syscheck.c Duplicates program_name/hostname before freeing/replacing full_log in two syscheck DB search outcomes.
src/tests/regressions/issue_1818_syscheck_uaf.c Adds a new regression test scaffold for Issue 1818 / syscheck UAF scenario.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +49 to +56
/* Simulate what OS_CleanMSG does: allocate full_log and point program_name/hostname into it */
char *test_log = strdup("Jan 15 12:00:00 hostname ossec-syscheckd: test message");
lf.full_log = test_log;
lf.log = test_log;

/* Point program_name and hostname into the full_log buffer (simulating OS_CleanMSG behavior) */
lf.program_name = strstr(test_log, "ossec-syscheckd");
lf.hostname = strstr(test_log, "hostname");
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This “regression test” doesn’t exercise the real vulnerable path: it never calls OS_CleanMSG() or DecodeSyscheck(), and instead manually simulates the fixed behavior. As written, it will still pass even if syscheck.c regresses. Update the test to construct a realistic syscheck message, run it through OS_CleanMSG + SyscheckInit + DecodeSyscheck, and then assert program_name/hostname no longer point into the old full_log after the decoder replaces it.

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +42
/* Declarations */
int OS_CleanMSG(char *msg, Eventinfo *lf);
int DecodeSyscheck(Eventinfo *lf);
void SyscheckInit(void);

Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These forward declarations (OS_CleanMSG/DecodeSyscheck/SyscheckInit) are currently unused, which can break builds when compiled with -Werror/-Wunused-function. Either remove the unused prototypes or (preferably) call the real functions as part of the regression test.

Suggested change
/* Declarations */
int OS_CleanMSG(char *msg, Eventinfo *lf);
int DecodeSyscheck(Eventinfo *lf);
void SyscheckInit(void);

Copilot uses AI. Check for mistakes.
- Improves error handling consistency with codebase conventions
- os_strdup calls ErrorExit on OOM instead of returning NULL
- Ensures allocation success before setting ownership flags
- Addresses Copilot code review feedback
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +49 to +86
/* Simulate what OS_CleanMSG does: allocate full_log and point program_name/hostname into it */
char *test_log = strdup("Jan 15 12:00:00 hostname ossec-syscheckd: test message");
lf.full_log = test_log;
lf.log = test_log;

/* Point program_name and hostname into the full_log buffer (simulating OS_CleanMSG behavior) */
lf.program_name = strstr(test_log, "ossec-syscheckd");
lf.hostname = strstr(test_log, "hostname");

printf("Before simulating syscheck decoder:\n");
printf(" full_log ptr: %p\n", lf.full_log);
printf(" program_name: '%s' (ptr: %p)\n", lf.program_name, lf.program_name);
printf(" hostname: '%s' (ptr: %p)\n", lf.hostname, lf.hostname);
printf(" flags: 0x%x\n", lf.flags);

char *old_full_log = lf.full_log;
char *old_pname = lf.program_name;
char *old_hname = lf.hostname;

/* Simulate what the FIXED syscheck decoder does:
* 1. Preserve program_name and hostname (strdup them)
* 2. Set ownership flags
* 3. Free old full_log and allocate new one
*/
if (lf.program_name) {
char *tmp_pname = strdup(lf.program_name);
lf.program_name = tmp_pname;
lf.flags |= EF_FREE_PNAME;
}
if (lf.hostname) {
char *tmp_hname = strdup(lf.hostname);
lf.hostname = tmp_hname;
lf.flags |= EF_FREE_HNAME;
}

free(lf.full_log);
lf.full_log = strdup("New message after syscheck processing");
lf.log = lf.full_log;
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This regression test doesn’t exercise the real vulnerable/fixed code path in syscheck.c (it only simulates the fix with local strdup/free). As written, it will still pass even if the syscheck decoder remains vulnerable, so it doesn’t actually protect against regressions for Issue 1818/CVE-2020-8447. Update the test to invoke the production decoder logic (e.g., OS_CleanMSG + DecodeSyscheck with a minimal, test-controlled syscheck DB setup), and assert that lf.program_name/lf.hostname no longer alias the freed lf.full_log and that the corresponding ownership flags are set.

Copilot uses AI. Check for mistakes.
Comment on lines 606 to 618
/* Preserve program_name and hostname before freeing full_log */
if (lf->program_name) {
char *tmp_pname = NULL;
os_strdup(lf->program_name, tmp_pname);
lf->program_name = tmp_pname;
lf->flags |= EF_FREE_PNAME;
}
if (lf->hostname) {
char *tmp_hname = NULL;
os_strdup(lf->hostname, tmp_hname);
lf->hostname = tmp_hname;
lf->flags |= EF_FREE_HNAME;
}
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The program_name/hostname preservation logic is duplicated in multiple branches in DB_Search. To reduce the risk of future edits fixing one path but not the other, consider extracting this into a small helper (e.g., preserve_eventinfo_header_fields(lf)) and calling it from both places.

Copilot uses AI. Check for mistakes.
Comment on lines 715 to 727
/* Preserve program_name and hostname before freeing full_log */
if (lf->program_name) {
char *tmp_pname = NULL;
os_strdup(lf->program_name, tmp_pname);
lf->program_name = tmp_pname;
lf->flags |= EF_FREE_PNAME;
}
if (lf->hostname) {
char *tmp_hname = NULL;
os_strdup(lf->hostname, tmp_hname);
lf->hostname = tmp_hname;
lf->flags |= EF_FREE_HNAME;
}
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same duplication as above: this second copy of the program_name/hostname preservation block should ideally be shared with the earlier branch via a helper to avoid divergence over time.

Copilot uses AI. Check for mistakes.
Addresses code review findings:
1. Free existing allocation before strdup if flag already set (event reuse)
2. Zero_Eventinfo now frees flagged memory before NULLing pointers
3. Free_Eventinfo clears flags after freeing and NULLs pointers
4. Remove redundant NULL checks before free()

These changes prevent memory leaks in edge cases like event reuse
and ensure robust memory management throughout event lifecycle.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 724 to 738
if (lf->program_name) {
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_PNAME) {
free(lf->program_name);
}
char *tmp_pname = NULL;
os_strdup(lf->program_name, tmp_pname);
lf->program_name = tmp_pname;
lf->flags |= EF_FREE_PNAME;
}
if (lf->hostname) {
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_HNAME) {
free(lf->hostname);
}
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this second code path, lf->hostname is freed when EF_FREE_HNAME is set and then used as the source for os_strdup(...), which is a use-after-free. Copy before freeing, or skip the duplication if the flag indicates the string is already owned.

Suggested change
if (lf->program_name) {
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_PNAME) {
free(lf->program_name);
}
char *tmp_pname = NULL;
os_strdup(lf->program_name, tmp_pname);
lf->program_name = tmp_pname;
lf->flags |= EF_FREE_PNAME;
}
if (lf->hostname) {
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_HNAME) {
free(lf->hostname);
}
if (lf->program_name && !(lf->flags & EF_FREE_PNAME)) {
char *tmp_pname = NULL;
os_strdup(lf->program_name, tmp_pname);
lf->program_name = tmp_pname;
lf->flags |= EF_FREE_PNAME;
}
if (lf->hostname && !(lf->flags & EF_FREE_HNAME)) {

Copilot uses AI. Check for mistakes.
Comment on lines 186 to 190
if (lf->flags & EF_FREE_PNAME) {
free(lf->program_name);
}
char *tmp_pname = NULL;
os_strdup(lf->program_name, tmp_pname);
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When EF_FREE_PNAME is set you free lf->program_name and then immediately pass lf->program_name as the source to os_strdup(...). This is a use-after-free on the “event reuse” path. Either keep the existing owned allocation, or duplicate into a temporary before freeing the old pointer.

Copilot uses AI. Check for mistakes.
Comment on lines 196 to 200
if (lf->flags & EF_FREE_HNAME) {
free(lf->hostname);
}
char *tmp_hname = NULL;
os_strdup(lf->hostname, tmp_hname);
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as program_name: when EF_FREE_HNAME is set you free lf->hostname and then call os_strdup(lf->hostname, ...) using freed memory as the source. Copy first (or reuse the existing allocation when already owned).

Copilot uses AI. Check for mistakes.
Comment on lines 608 to 625
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_PNAME) {
free(lf->program_name);
}
char *tmp_pname = NULL;
os_strdup(lf->program_name, tmp_pname);
lf->program_name = tmp_pname;
lf->flags |= EF_FREE_PNAME;
}
if (lf->hostname) {
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_HNAME) {
free(lf->hostname);
}
char *tmp_hname = NULL;
os_strdup(lf->hostname, tmp_hname);
lf->hostname = tmp_hname;
lf->flags |= EF_FREE_HNAME;
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the “event reuse” branch you free lf->program_name when EF_FREE_PNAME is set, but then immediately call os_strdup(lf->program_name, ...) using the same pointer as the source. This is a use-after-free when the flag is set. Fix by not freeing before copying (e.g., copy to a temporary first, then free old), or skip duplicating entirely when EF_FREE_PNAME is already set and the existing allocation can be reused.

Suggested change
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_PNAME) {
free(lf->program_name);
}
char *tmp_pname = NULL;
os_strdup(lf->program_name, tmp_pname);
lf->program_name = tmp_pname;
lf->flags |= EF_FREE_PNAME;
}
if (lf->hostname) {
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_HNAME) {
free(lf->hostname);
}
char *tmp_hname = NULL;
os_strdup(lf->hostname, tmp_hname);
lf->hostname = tmp_hname;
lf->flags |= EF_FREE_HNAME;
/* Duplicate only once: if EF_FREE_PNAME is not set, program_name may
* point into a transient buffer (e.g., full_log), so make our own copy.
* If the flag is already set, we already own a heap allocation and can reuse it.
*/
if (!(lf->flags & EF_FREE_PNAME)) {
char *tmp_pname = NULL;
os_strdup(lf->program_name, tmp_pname);
lf->program_name = tmp_pname;
lf->flags |= EF_FREE_PNAME;
}
}
if (lf->hostname) {
/* Same logic as program_name: only duplicate once when EF_FREE_HNAME is not set. */
if (!(lf->flags & EF_FREE_HNAME)) {
char *tmp_hname = NULL;
os_strdup(lf->hostname, tmp_hname);
lf->hostname = tmp_hname;
lf->flags |= EF_FREE_HNAME;
}

Copilot uses AI. Check for mistakes.
Comment on lines 608 to 625
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_PNAME) {
free(lf->program_name);
}
char *tmp_pname = NULL;
os_strdup(lf->program_name, tmp_pname);
lf->program_name = tmp_pname;
lf->flags |= EF_FREE_PNAME;
}
if (lf->hostname) {
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_HNAME) {
free(lf->hostname);
}
char *tmp_hname = NULL;
os_strdup(lf->hostname, tmp_hname);
lf->hostname = tmp_hname;
lf->flags |= EF_FREE_HNAME;
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same use-after-free pattern as program_name: when EF_FREE_HNAME is set you free lf->hostname and then call os_strdup(lf->hostname, ...) using freed memory as the source. Copy before freeing, or avoid duplicating when the hostname is already an owned allocation.

Suggested change
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_PNAME) {
free(lf->program_name);
}
char *tmp_pname = NULL;
os_strdup(lf->program_name, tmp_pname);
lf->program_name = tmp_pname;
lf->flags |= EF_FREE_PNAME;
}
if (lf->hostname) {
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_HNAME) {
free(lf->hostname);
}
char *tmp_hname = NULL;
os_strdup(lf->hostname, tmp_hname);
lf->hostname = tmp_hname;
lf->flags |= EF_FREE_HNAME;
/* Duplicate only if it is not already an owned allocation */
if (!(lf->flags & EF_FREE_PNAME)) {
char *tmp_pname = NULL;
os_strdup(lf->program_name, tmp_pname);
lf->program_name = tmp_pname;
lf->flags |= EF_FREE_PNAME;
}
}
if (lf->hostname) {
/* Duplicate only if it is not already an owned allocation */
if (!(lf->flags & EF_FREE_HNAME)) {
char *tmp_hname = NULL;
os_strdup(lf->hostname, tmp_hname);
lf->hostname = tmp_hname;
lf->flags |= EF_FREE_HNAME;
}

Copilot uses AI. Check for mistakes.
Comment on lines 725 to 740
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_PNAME) {
free(lf->program_name);
}
char *tmp_pname = NULL;
os_strdup(lf->program_name, tmp_pname);
lf->program_name = tmp_pname;
lf->flags |= EF_FREE_PNAME;
}
if (lf->hostname) {
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_HNAME) {
free(lf->hostname);
}
char *tmp_hname = NULL;
os_strdup(lf->hostname, tmp_hname);
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this second code path, lf->program_name is freed when EF_FREE_PNAME is set and then used as the source for os_strdup(...), which is a use-after-free. Preserve/copy the string before freeing (or reuse the existing allocation when the flag is already set).

Suggested change
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_PNAME) {
free(lf->program_name);
}
char *tmp_pname = NULL;
os_strdup(lf->program_name, tmp_pname);
lf->program_name = tmp_pname;
lf->flags |= EF_FREE_PNAME;
}
if (lf->hostname) {
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_HNAME) {
free(lf->hostname);
}
char *tmp_hname = NULL;
os_strdup(lf->hostname, tmp_hname);
char *old_pname = lf->program_name;
char *tmp_pname = NULL;
os_strdup(lf->program_name, tmp_pname);
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_PNAME) {
free(old_pname);
}
lf->program_name = tmp_pname;
lf->flags |= EF_FREE_PNAME;
}
if (lf->hostname) {
char *old_hname = lf->hostname;
char *tmp_hname = NULL;
os_strdup(lf->hostname, tmp_hname);
/* Free existing allocation if flag is already set (event reuse case) */
if (lf->flags & EF_FREE_HNAME) {
free(old_hname);
}

Copilot uses AI. Check for mistakes.
Previous fix had a use-after-free: we freed program_name/hostname
then immediately used the freed pointer in os_strdup().

New approach (Option 2):
- Only duplicate if flag is NOT set (string not already owned)
- If flag is set, string is already a separate allocation, skip duplication
- More efficient and avoids UAF entirely

Credit: Copilot code review
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +49 to +87
/* Simulate what OS_CleanMSG does: allocate full_log and point program_name/hostname into it */
char *test_log = strdup("Jan 15 12:00:00 hostname ossec-syscheckd: test message");
lf.full_log = test_log;
lf.log = test_log;

/* Point program_name and hostname into the full_log buffer (simulating OS_CleanMSG behavior) */
lf.program_name = strstr(test_log, "ossec-syscheckd");
lf.hostname = strstr(test_log, "hostname");

printf("Before simulating syscheck decoder:\n");
printf(" full_log ptr: %p\n", lf.full_log);
printf(" program_name: '%s' (ptr: %p)\n", lf.program_name, lf.program_name);
printf(" hostname: '%s' (ptr: %p)\n", lf.hostname, lf.hostname);
printf(" flags: 0x%x\n", lf.flags);

char *old_full_log = lf.full_log;
char *old_pname = lf.program_name;
char *old_hname = lf.hostname;

/* Simulate what the FIXED syscheck decoder does:
* 1. Preserve program_name and hostname (strdup them)
* 2. Set ownership flags
* 3. Free old full_log and allocate new one
*/
if (lf.program_name) {
char *tmp_pname = strdup(lf.program_name);
lf.program_name = tmp_pname;
lf.flags |= EF_FREE_PNAME;
}
if (lf.hostname) {
char *tmp_hname = strdup(lf.hostname);
lf.hostname = tmp_hname;
lf.flags |= EF_FREE_HNAME;
}

free(lf.full_log);
lf.full_log = strdup("New message after syscheck processing");
lf.log = lf.full_log;

Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This regression test does not exercise the real syscheck decoder path (e.g., OS_CleanMSG + DecodeSyscheck/DB_Search). It only simulates the "fixed" behavior by manually strdup'ing program_name/hostname and updating flags, so it will pass even if the production decoder still has the UAF. Consider constructing an actual syscheck event, calling OS_CleanMSG and DecodeSyscheck, and asserting that after the decoder replaces lf->full_log, lf->program_name/lf->hostname no longer point into the old buffer and the EF_FREE_* flags are set.

Copilot uses AI. Check for mistakes.
Comment on lines +606 to +620
/* Preserve program_name and hostname before freeing full_log */
if (lf->program_name && !(lf->flags & EF_FREE_PNAME)) {
/* Only duplicate if we don't already own it */
char *tmp_pname = NULL;
os_strdup(lf->program_name, tmp_pname);
lf->program_name = tmp_pname;
lf->flags |= EF_FREE_PNAME;
}
if (lf->hostname && !(lf->flags & EF_FREE_HNAME)) {
/* Only duplicate if we don't already own it */
char *tmp_hname = NULL;
os_strdup(lf->hostname, tmp_hname);
lf->hostname = tmp_hname;
lf->flags |= EF_FREE_HNAME;
}
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same "preserve program_name/hostname" block is duplicated in two branches of DB_Search. To reduce the risk of future drift (e.g., adding another field to preserve in one place but not the other), consider extracting this into a small helper (or macro) that preserves both fields and sets EF_FREE_* as needed, then call it from both sites.

Copilot uses AI. Check for mistakes.
@atomicturtle atomicturtle merged commit ba6df83 into ossec:main Jan 26, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant