X

News, tips, partners, and perspectives for the Oracle Solaris operating system

Expanding the Library

Alan Coopersmith
Senior Principal Software Engineer

It's now been one year since the GA release of Oracle Solaris 11.4. During that time, we've released 12 Support Repository Updates (SRUs), with the latest, SRU12 (aka 11.4.12) coming out last week. These releases have included hundreds of updates to the bundled FOSS packages, over 2500 bug fixes, and over 300 enhancements.

One area a few of us have been working on lately is making improvements to the standard C library, libc, that increase compatibility with the Linux & BSD libraries and make developing for Solaris easier. These include:

  • I expanded our set of common memory allocation interfaces with malloc_usable_size() and reallocarray() in SRU 10, and reallocf() in SRU 12. These are explained in detail later in this post.
  • In SRU 12, Ali added implementations of explicit_memset() and explicit_bzero() which do the same things as memset() and bzero(), but under names that the compiler won't optimize out if it thinks the memory is never read after these calls.
  • Ali also added the qsort_r() function in SRU 12, which allows passing data to the comparison function as an argument instead of relying on global pointers, which isn't multi-thread safe. We've followed the form proposed for standardization, as found in glibc, which differs in the order of arguments to the one found in FreeBSD 12.
  • Darren added support in SRU 12 for the MADV_DONTDUMP flag to madvise() to exclude a memory region from a core dump, using the functionality already implemented as the MC_CORE_PRUNE_OUT flag to memcntl().
  • Ali also updated <sys/mman.h> in SRU 12 to use void * instead of caddr_t in the arguments to madvise(), memcntl(), and mincore(). Since the C standard allows promoting any pointer type to a void * in function arguments, this should be safe unless your code has its own declaration of these functions that uses caddr_t. If necessary, you can #define __USE_LEGACY_PROTOTYPES__ to have the header use the old caddr_t prototypes instead.

We're not stopping there. Implementations of mkostemp() and mkostemps() are currently in testing for a future SRU, and we're evaluating what to do next.

My contributions were in our common memory allocators, and the effects go beyond just increasing compatibility.

The simplest of the new functions is reallocf(), which takes the same arguments as realloc(), but if the allocation fails, it frees the old pointer, so that every caller doesn't have to replicate the same boilerplate code to save the old pointer and free it manually on failure. Forgetting to do this is a very common error, as many static analysers will report, so this makes it easier to clean your code of such issues at a low cost. You may note in the SRU 12 ReadMe that we indeed used this function to clean up a couple of our own bugs:

  • 29405632 code to get isalist leaks memory if realloc() fails
  • 29405650 mkdevalloc frees wrong pointer when realloc() fails

reallocarray() is also fairly simple - like realloc() it can either allocate new memory or change the size of an existing allocation, but instead of a single size value, it takes the size of an object and the number of objects to allocate space for, like calloc(), and makes sure there isn't an integer overflow when multiplied together. This also helps save adding integer overflow checks to many callers, and is easier to audit for correctness by having one overflow check in libc, instead of hundreds or more spread across a code base.

I first encountered reallocarray() not long after spending months working to eradicate integer overflow bugs from the protocol handling code in X11 servers and clients. Reading about OpenBSD introducing reallocarray seemed like such a better way to solve these problems than all the hand-coded checks we'd added to the X code base. (Though it couldn't replace all of them, since many didn't involve memory allocations.) Adding an implementation of reallocarray() to the Xserver and then to libX11, along with a macro to make a simple mallocarray() equivalent that just called reallocarray() with a NULL pointer for the old value, helped prove the usefulness of having this function. As adoption spread, I nominated it for standardization, and added it to the Solaris libc as well.

The interface that was most complicated and took the most work was malloc_usable_size(). The other two could be implemented as simple wrappers around any realloc() implementation without knowing the details of the allocation internals, and thus just have a single version in libc. However, since malloc_usable_size() has to look into the internal data structures of the memory allocator to determine the size of an allocated block, it has to have a different implementation for each memory allocator. As noted in the Alternative Implementations section of the malloc(3C) man page we currently ship 8 different implementations in Oracle Solaris 11.4 - in the libc, libadimalloc, libbsdmalloc, libmalloc, libmapmalloc, libmtmalloc, libumem, and watchmalloc libraries. While there are some commonalities, there are also some major differences, and this involved several completely different sets of code to implement across all these libraries.

While this seems like a lot of work for a rarely used function (though the few users are rather important, like the Mozilla applications and several memory/address sanitizers for systems without ADI support), it turned out to be a very worthwhile addition as it allowed writing a common set of unit tests across all 8 malloc implementations. These tests not only helped confirm the fixes for some long-standing bugs, but helped find bugs we hadn't known about in various corner cases, either by checking that allocation sizes are in the range expected, or by using malloc_usable_size() as a reliable way to check whether or not a pointer is free after given operations.

For instance, a test that boils down to

    p2 = malloc(73);
    assert(p2 != NULL);
    s = malloc_usable_size(p2);
    /* request a size that realloc can't possibly allocate */
    p = realloc(p2, SIZE_MAX - 4096);
    assert(p == NULL);
    assert(malloc_usable_size(p2) == s);
found that some of our realloc() implementations would first increase the size of the block to include any following free blocks from the memory arena, and if that wasn't enough, tried allocating a new block — but never restored the original block size if that failed, leading to wasted memory allocation. The ancient bsdmalloc() actually freed the old block before trying to allocate a new one, and if it failed, left the old block unexpectedly unallocated.

Bugs fixed as part of this include:

  • 29242273 libmalloc: SEGV in realloc of sizes near SIZE_MAX
  • 29497791 libmalloc can waste memory when realloc() fails
  • 15243567 libbsdmalloc doesn't set errno when it should
  • 29242228 libbsdmalloc has incomplete set of malloc replacement symbols
  • 29497790 libbsdmalloc can free old pointer when realloc() fails
  • 15225938 realloc() in libmapmalloc gets SIGSEGV when cannot allocate enough memory
  • 29503888 libmapmalloc can waste memory when realloc() fails

So you can see we've both strengthened our compatibility story, and hardened our code in the process, making Solaris 11.4 better for both developers and users.

Join the discussion

Comments ( 1 )
  • Boyd Adamson Sunday, September 8, 2019
    Hey thanks for this post Alan! It’s great to see some old school tech content come up in my feed rather than hollow product announcements and conference promotions
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.