Page MenuHomeFreeBSD

dlopen(3) crashes when opening empty .so
ClosedPublic

Authored by sobomax on Jan 29 2016, 5:34 AM.
Tags
None
Referenced Files
Unknown Object (File)
Mar 21 2024, 12:26 AM
Unknown Object (File)
Feb 27 2024, 3:54 PM
Unknown Object (File)
Feb 15 2024, 12:52 AM
Unknown Object (File)
Dec 22 2023, 9:15 PM
Unknown Object (File)
Nov 11 2023, 10:02 PM
Unknown Object (File)
Nov 10 2023, 4:37 PM
Unknown Object (File)
Nov 10 2023, 6:53 AM
Unknown Object (File)
Nov 8 2023, 12:29 AM

Details

Summary

This seems like a very trivial bug that should have been squashed a long time ago, but for some reason it was not. Basically, without this change dlopen(3)'ing an empty .so file would just cause application to dump core with SIGSEGV.

Make sure the file has enough data for at least the ELF header before mmap'ing it.

Test Plan

ATF test case is attached.

Diff Detail

Repository
rS FreeBSD src repository - subversion
Lint
Lint Not Applicable
Unit
Tests Not Applicable

Event Timeline

sobomax retitled this revision from to dlopen(3) crashes when opening empty .so.
sobomax updated this object.
sobomax edited the test plan for this revision. (Show Details)
sobomax set the repository for this revision to rS FreeBSD src repository - subversion.

Small test case:

#include <sys/stat.h>
#include <dlfcn.h>
#include <err.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    char tempname[] = "/tmp/temp.XXXXXX";
    char *fname, *soname;
    int fd;
    void *dlh;

    fname = mktemp(tempname);
    if (fname == NULL) {
        err(1, "mktemp");
    }
    asprintf(&soname, "%s.so", fname);
    fd = open(soname, O_WRONLY | O_CREAT | O_TRUNC, DEFFILEMODE);
    if (fd == -1) {
        err(1, "open(%s)", soname);
    }
    printf("%s %d\n", soname, fd);
    close(fd);
    dlh = dlopen(soname, RTLD_LAZY);
    if (dlh != NULL) {
        dlclose(dlh);
    } else {
        printf("%s\n", dlerror());
    }
    unlink(soname);
}
sobomax edited the test plan for this revision. (Show Details)
sobomax removed rS FreeBSD src repository - subversion as the repository for this revision.

ATF test case is attached.

Please upload patches with more context in the future (-U9999).

It seems like mmap(2) *ought* to fail on zero-length files. Shortly after the new fstat check is an mmap failure check:

331         hdr = mmap(NULL, PAGE_SIZE, PROT_READ, MAP_PRIVATE | MAP_PREFAULT_READ,
332             fd, 0);
333         if (hdr == (Elf_Ehdr *)MAP_FAILED) {
334                 _rtld_error("%s: read error: %s", path, rtld_strerror(errno));
335                 return (NULL);
336         }

Why doesn't that fail?

lib/libc/tests/gen/dlopen_empty_test.c
48 ↗(On Diff #12838)

I wouldn't bother trying to free in the segv handler. The program will simply crash and free all memory when the handler returns anyway.

It seems like vm_mmap_vnode should fail if the vnode va_size is zero. No non-zero request size is satisfiable with a zero-length file.

In D5112#108970, @cem wrote:

Please upload patches with more context in the future (-U9999).

It seems like mmap(2) *ought* to fail on zero-length files. Shortly after the new fstat check is an mmap failure check:

331         hdr = mmap(NULL, PAGE_SIZE, PROT_READ, MAP_PRIVATE | MAP_PREFAULT_READ,
332             fd, 0);
333         if (hdr == (Elf_Ehdr *)MAP_FAILED) {
334                 _rtld_error("%s: read error: %s", path, rtld_strerror(errno));
335                 return (NULL);
336         }

Why doesn't that fail?

I don't know. It returns pointer, but attempt to read few bytes offset from that pointer causes SEGV. In any case that would be a separate issue, technically file can have say only 1 byte, so mmap would not fail but reading beyond that 1 byte would be just as incorrect, although probably not detectable since memory mapping is done in PAGE_SIZE chunks AFAIK.

-Max

sobomax set the repository for this revision to rS FreeBSD src repository - subversion.

use -U9999, remove free() in signal handler

I don't know. It returns pointer, but attempt to read few bytes offset from that pointer causes SEGV. In any case that would be a separate issue, technically file can have say only 1 byte, so mmap would not fail but reading beyond that 1 byte would be just as incorrect, although probably not detectable since memory mapping is done in PAGE_SIZE chunks AFAIK.

Even a 1-byte file would be valid to map PAGE_SIZE (the remaining contents of the partial page should be zero). It is just the zero length file where mapping any non-zero length ought to fail. (And zero-length mmap requests are invalid by definition, so it is invalid to mmap zero-length files at all.)

A 1-byte file should successfully mmap the partial page (the program can read the full PAGE_SIZE, of course), then fail to parse as ELF and dlopen fails as expected (no segfault). A zero byte file should failed to mmap PAGE_SIZE bytes and return MAP_FAILED.

In D5112#108993, @cem wrote:

[...] A zero byte file should failed to mmap PAGE_SIZE bytes and return MAP_FAILED.

Where does this documented? I see nothing on manpage and internets don't return anything concise. Our mmap() definitely does not do it (10.1, 11.0)

-Max

In D5112#108993, @cem wrote:

[...] A zero byte file should failed to mmap PAGE_SIZE bytes and return MAP_FAILED.

Where does this documented? I see nothing on manpage and internets don't return anything concise. Our mmap() definitely does not do it (10.1, 11.0)

This is just my opinion. Discussing with Jilles on IRC now.

lib/libc/tests/gen/dlopen_empty_test.c
73–76 ↗(On Diff #12842)

Per Jilles, this should probably be SIGBUS. Are you sure you saw SIGSEGV?

Ok, Jilles says reservations larger than file size are allowed. The intent is to allow extensions to the file without having to find a new address space for mmapping. Access to whole pages beyond EOF should raise SIGBUS.

In D5112#109005, @cem wrote:

Ok, Jilles says reservations larger than file size are allowed. The intent is to allow extensions to the file without having to find a new address space for mmapping. Access to whole pages beyond EOF should raise SIGBUS.

Well, I am catching only that (sigaction(SIGSEGV), so it must be it:

$ ./dlopen_empty_test dlopen_empty_test
dlopen_empty_test: WARNING: Running test cases outside of kyua(1) is unsupported
dlopen_empty_test: WARNING: No isolation nor timeout control is being applied; you may get unexpected failures; see atf-test-case(4)
failed: got SIGSEGV in the dlopen(3)
$

lib/libc/tests/gen/dlopen_empty_test.c
73–76 ↗(On Diff #12842)

Well, I am catching only that (sigaction(SIGSEGV), so it must be it:

$ ./dlopen_empty_test dlopen_empty_test
dlopen_empty_test: WARNING: Running test cases outside of kyua(1) is unsupported
dlopen_empty_test: WARNING: No isolation nor timeout control is being applied; you may get unexpected failures; see atf-test-case(4)
failed: got SIGSEGV in the dlopen(3)
$

cem added a reviewer: cem.

Ok. The fault should actually be raising SIGBUS, not SIGSEGV, but that's a wholly separate bug. The fix for rtld and test cases look good, thanks.

This revision is now accepted and ready to land.Jan 29 2016, 9:47 PM
mjg added inline comments.
libexec/rtld-elf/map_object.c
342 ↗(On Diff #12842)

Why is this being done here?

I somewhat understand this is a nice place, but just the header size is not enough.

Most importantly though fstat was already performed in load_object and now you are just duplicating it. Given how incomplete this validation is, I would argue an acceptable hack would just check for non-zero in aforementiooned load_object.

We should really avoid adding syscalls to common initialisation paths.

mjg requested changes to this revision.Jan 30 2016, 12:13 AM
mjg added a reviewer: mjg.
This revision now requires changes to proceed.Jan 30 2016, 12:13 AM
sobomax edited edge metadata.

Eliminate duplicate fstat() call.

Suggested by: mjg

mjg edited edge metadata.

Thanks.

Although I would remove the NULL check.

Also, is not the signal SIGBUS as opposed to SIGSEGV? Just for commit message purposes.

This revision is now accepted and ready to land.Jan 30 2016, 12:33 AM

mjd, thanks, I'll just pass that info down. I think for execve path, it's safe to assume that the kernel already has done lot of legwork to make sure the file has at least some ELF-foo in it. As for the sizeof(Elf_Ehdr) check, yes not ideal, but that should be enough to make sure we can read off the elf magic in the IS_ELF(), which is actually where we get SIGSEGV now.

Ouch, I see that stat buffer indeed can be null. I guess it is acceptable to omit the sanity check.

In D5112#109036, @mjg wrote:

Also, is not the signal SIGBUS as opposed to SIGSEGV? Just for commit message purposes.

It's definitely SIGSEGV, see the test case and my earlier conversation with cem. Could be some separate issue in mmap(2).

-Max

This revision was automatically updated to reflect the committed changes.