From patchwork Fri Apr 19 03:49:03 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: DJ Delorie X-Patchwork-Id: 1925355 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@legolas.ozlabs.org Authentication-Results: legolas.ozlabs.org; dkim=pass (1024-bit key; unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=MHJv5yu7; dkim-atps=neutral Authentication-Results: legolas.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=sourceware.org (client-ip=8.43.85.97; helo=server2.sourceware.org; envelope-from=libc-alpha-bounces+incoming=patchwork.ozlabs.org@sourceware.org; receiver=patchwork.ozlabs.org) Received: from server2.sourceware.org (server2.sourceware.org [8.43.85.97]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (secp384r1) server-digest SHA384) (No client certificate requested) by legolas.ozlabs.org (Postfix) with ESMTPS id 4VLLJF2m65z1yZP for ; Fri, 19 Apr 2024 13:50:05 +1000 (AEST) Received: from server2.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id AB8C8384AB70 for ; Fri, 19 Apr 2024 03:50:03 +0000 (GMT) X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.133.124]) by sourceware.org (Postfix) with ESMTPS id C2348384AB48 for ; Fri, 19 Apr 2024 03:49:06 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org C2348384AB48 Authentication-Results: sourceware.org; dmarc=pass (p=none dis=none) header.from=redhat.com Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=redhat.com ARC-Filter: OpenARC Filter v1.0.0 sourceware.org C2348384AB48 Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=170.10.133.124 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1713498551; cv=none; b=rm0DtpU9zgvVNYzX85e0NsTs049kVyG2woP/oQcnPahZ/2sa9b0YK3uoE2ftek8sRPxNnsab+j3KLxqrM7AodUggD3QivhLpwMUC8GxDqUvyeyXQ6Dy5V19KbB9lcj7nPpE2G4IOwcYwvmhUMVZx+BrFbsfONZe6KYfdT6HF1Q0= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1713498551; c=relaxed/simple; bh=Uc3SMNGGbMd6l11+7oSt96WJ5uL4KBxsJj5W/cwt8ZA=; h=DKIM-Signature:Date:Message-Id:From:To:Subject; b=SxqwVY4DdSmNeNctJxhWVdUUpUL2r+/E2X4ytIfYAG6pvbbTIb8kXbd2z59DGnNJYC6l0LwJsGQKCTzXngWkCr5zPsN3mXWZOqJ34YGVsyKkdVBTd/Z9puWxt5G5u3HtTdHhDlfoQjd3auWXD+qhXbUMDTfVS2bxXD+t4dl3iLw= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1713498546; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:content-type:content-type; bh=MDs7546R7RQ9x0uY9UcySeZlixe1HpxrQ0JSA5PMLWU=; b=MHJv5yu7/hWNKB8zO+gRS9vT7sJlRHM6yjutDYC/s2Ine1Q7OiVjtuN4jsfkTJ+NpnXDFG c90oc96vsT/1pybji2DV1Vz2rK4ko9UHFk/B+Cf2mjy4gfrwfah2ZeWOTE8Il9IpGAiBFa adTayV1wohd7IQN2dBi3p3amm+ZU1M0= Received: from mimecast-mx02.redhat.com (mimecast-mx02.redhat.com [66.187.233.88]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-18-n_J4lEaWOHi39oHzoPxihA-1; Thu, 18 Apr 2024 23:49:04 -0400 X-MC-Unique: n_J4lEaWOHi39oHzoPxihA-1 Received: from smtp.corp.redhat.com (int-mx06.intmail.prod.int.rdu2.redhat.com [10.11.54.6]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mimecast-mx02.redhat.com (Postfix) with ESMTPS id 003991049CA1 for ; Fri, 19 Apr 2024 03:49:04 +0000 (UTC) Received: from greed.delorie.com (unknown [10.22.8.231]) by smtp.corp.redhat.com (Postfix) with ESMTPS id DDB502166B34 for ; Fri, 19 Apr 2024 03:49:03 +0000 (UTC) Received: from greed.delorie.com.redhat.com (localhost [127.0.0.1]) by greed.delorie.com (8.16.1/8.16.1) with ESMTP id 43J3n3Sq1041561 for ; Thu, 18 Apr 2024 23:49:03 -0400 Date: Thu, 18 Apr 2024 23:49:03 -0400 Message-Id: From: DJ Delorie To: libc-alpha@sourceware.org Subject: [PATCH 2/3] Add system-wide tunables: cache ld.so.cache X-Scanned-By: MIMEDefang 3.4.1 on 10.11.54.6 X-Mimecast-Spam-Score: 0 X-Mimecast-Originator: redhat.com X-Spam-Status: No, score=-11.4 required=5.0 tests=BAYES_00, DKIMWL_WL_HIGH, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, RCVD_IN_DNSWL_NONE, RCVD_IN_MSPIKE_H4, RCVD_IN_MSPIKE_WL, SPF_HELO_NONE, SPF_NONE, TXREP autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on server2.sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces+incoming=patchwork.ozlabs.org@sourceware.org The purpose of this change is twofold: 1. The ld.so.cache is cached in memory and only re-read if/when it changes on disk. This allows us to have much more intensive security checks in the future, without impacting performance as much. It also allows for cases where the cache is corrupted - we continue using the last valid one. 2. We break out the load/check logic so that the cache can be loaded independently of the library lookup, such as for code that only needs to look at the extensions. --- elf/dl-cache.c | 252 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 167 insertions(+), 85 deletions(-) diff --git a/elf/dl-cache.c b/elf/dl-cache.c index 85f3f179ed..d90278889d 100644 --- a/elf/dl-cache.c +++ b/elf/dl-cache.c @@ -26,6 +26,8 @@ #include <_itoa.h> #include #include +#include +#include #ifndef _DL_PLATFORMS_COUNT # define _DL_PLATFORMS_COUNT 0 @@ -35,6 +37,10 @@ static struct cache_file *cache; static struct cache_file_new *cache_new; static size_t cachesize; +static struct cache_extension_all_loaded ext; + +static struct stat cache_file_time; +static struct stat new_cache_file_time; #ifdef SHARED /* This is used to cache the priorities of glibc-hwcaps @@ -57,6 +63,7 @@ glibc_hwcaps_priorities_free (void) free (glibc_hwcaps_priorities); glibc_hwcaps_priorities = NULL; glibc_hwcaps_priorities_allocated = 0; + glibc_hwcaps_priorities_length = 0; } /* Ordered comparison of a hwcaps string from the cache on the left @@ -88,10 +95,6 @@ glibc_hwcaps_compare (uint32_t left_index, struct dl_hwcaps_priority *right) static void glibc_hwcaps_priorities_init (void) { - struct cache_extension_all_loaded ext; - if (!cache_extension_load (cache_new, cache, cachesize, &ext)) - return; - uint32_t length = (ext.sections[cache_extension_tag_glibc_hwcaps].size / sizeof (uint32_t)); if (length > glibc_hwcaps_priorities_allocated) @@ -390,6 +393,159 @@ _dl_cache_libcmp (const char *p1, const char *p2) return *p1 - *p2; } +/* Set the cache back to the "no cache" state, which may include + cleaning up a loaded cache. */ +static void +_dl_maybe_unload_ldsocache (void) +{ + if (cache != NULL) + __munmap (cache, cachesize); + + cache = NULL; + cache_new = NULL; + cachesize = 0; + +#ifdef SHARED + glibc_hwcaps_priorities_free (); +#endif +} + +/* Returns TRUE if for any reason the cache needs to be reloaded + (including, the first time, loaded). */ +static bool +_dl_check_ldsocache_needs_loading (void) +{ + int rv; + static int copy_old_time = 0; + + /* Save the previous stat every time. We only care when this + changes, and we only stat it here, so we can get away with doing + the copy now instead of at every single return statement in this + function. However, we only need to copy it if the previous stat + succeeded. The only way this could be subverted is if the admin + moves the file aside, then moves it back, but CACHE would be set + to NULL in the interim so that would be detected. */ + if (copy_old_time) + cache_file_time = new_cache_file_time; + rv = __stat (LD_SO_CACHE, &new_cache_file_time); + copy_old_time = (rv >= 0); + + /* No file to load, but there used to be. Assume user intentionally + deleted the cache and act accordingly. */ + if (rv < 0 && cache != NULL) + { + _dl_maybe_unload_ldsocache (); + return false; + } + + /* No file to load and no loaded cache, so nothing to do. */ + if (rv < 0) + return false; + + /* Any file is better than no file (likely the first time + through). */ + if (cache == NULL) + return true; + + /* At this point, NEW_CACHE_FILE_TIME is valid as well as + CACHE_FILE_TIME, so we compare them. We list fields in the order + they're most likely to be different in. */ + return ((new_cache_file_time.st_mtime != cache_file_time.st_mtime) + || (new_cache_file_time.st_ino != cache_file_time.st_ino) + || (new_cache_file_time.st_size != cache_file_time.st_size) + || (new_cache_file_time.st_dev != cache_file_time.st_dev) + ); +} + +/* Attemps to load and validate the cache. On return, CACHE is either + unchanged (still loaded or still not loaded) or valid. */ +static void +_dl_maybe_load_ldsocache (void) +{ + struct cache_file *tmp_cache = NULL; + struct cache_file_new *tmp_cache_new = NULL; + size_t tmp_cachesize = 0; + + /* Read the contents of the file. */ + void *file = _dl_sysdep_read_whole_file (LD_SO_CACHE, &tmp_cachesize, + PROT_READ); + + /* We can handle three different cache file formats here: + - only the new format + - the old libc5/glibc2.0/2.1 format + - the old format with the new format in it + The following checks if the cache contains any of these formats. */ + if (file != MAP_FAILED && tmp_cachesize > sizeof *cache_new + && memcmp (file, CACHEMAGIC_VERSION_NEW, + sizeof CACHEMAGIC_VERSION_NEW - 1) == 0 + /* Check for corruption, avoiding overflow. */ + && ((tmp_cachesize - sizeof *cache_new) / sizeof (struct file_entry_new) + >= ((struct cache_file_new *) file)->nlibs)) + { + if (! cache_file_new_matches_endian (file)) + { + __munmap (file, tmp_cachesize); + return; + } + + tmp_cache_new = file; + tmp_cache = file; + } + else if (file != MAP_FAILED && cachesize > sizeof *cache + && memcmp (file, CACHEMAGIC, sizeof CACHEMAGIC - 1) == 0 + /* Check for corruption, avoiding overflow. */ + && ((tmp_cachesize - sizeof *cache) / sizeof (struct file_entry) + >= ((struct cache_file *) file)->nlibs)) + { + size_t offset; + /* Looks ok. */ + tmp_cache = file; + + /* Check for new version. */ + offset = ALIGN_CACHE (sizeof (struct cache_file) + + cache->nlibs * sizeof (struct file_entry)); + + tmp_cache_new = (struct cache_file_new *) ((void *) tmp_cache + offset); + if (tmp_cachesize < (offset + sizeof (struct cache_file_new)) + || memcmp (tmp_cache_new->magic, CACHEMAGIC_VERSION_NEW, + sizeof CACHEMAGIC_VERSION_NEW - 1) != 0) + tmp_cache_new = NULL; + else + { + if (! cache_file_new_matches_endian (tmp_cache_new)) + /* The old-format part of the cache is bogus as well + if the endianness does not match. (But it is + unclear how the new header can be located if the + endianness does not match.) */ + { + __munmap (file, tmp_cachesize); + return; + } + } + } + else + { + if (file != MAP_FAILED) + __munmap (file, tmp_cachesize); + return; + } + + struct cache_extension_all_loaded tmp_ext; + if (!cache_extension_load (tmp_cache_new, tmp_cache, tmp_cachesize, &tmp_ext)) + /* The extension is corrupt, so the cache is corrupt. */ + return; + + /* If we've gotten here, the loaded cache is good and we need to + save it. */ + _dl_maybe_unload_ldsocache (); + cache = tmp_cache; + cache_new = tmp_cache_new; + cachesize = tmp_cachesize; + ext = tmp_ext; + + assert (cache != NULL); +} + /* Look up NAME in ld.so.cache and return the file name stored there, or null if none is found. The cache is loaded if it was not already. If loading @@ -405,81 +561,14 @@ _dl_load_cache_lookup (const char *name) if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_LIBS)) _dl_debug_printf (" search cache=%s\n", LD_SO_CACHE); - if (cache == NULL) - { - /* Read the contents of the file. */ - void *file = _dl_sysdep_read_whole_file (LD_SO_CACHE, &cachesize, - PROT_READ); - - /* We can handle three different cache file formats here: - - only the new format - - the old libc5/glibc2.0/2.1 format - - the old format with the new format in it - The following checks if the cache contains any of these formats. */ - if (file != MAP_FAILED && cachesize > sizeof *cache_new - && memcmp (file, CACHEMAGIC_VERSION_NEW, - sizeof CACHEMAGIC_VERSION_NEW - 1) == 0 - /* Check for corruption, avoiding overflow. */ - && ((cachesize - sizeof *cache_new) / sizeof (struct file_entry_new) - >= ((struct cache_file_new *) file)->nlibs)) - { - if (! cache_file_new_matches_endian (file)) - { - __munmap (file, cachesize); - file = (void *) -1; - } - cache_new = file; - cache = file; - } - else if (file != MAP_FAILED && cachesize > sizeof *cache - && memcmp (file, CACHEMAGIC, sizeof CACHEMAGIC - 1) == 0 - /* Check for corruption, avoiding overflow. */ - && ((cachesize - sizeof *cache) / sizeof (struct file_entry) - >= ((struct cache_file *) file)->nlibs)) - { - size_t offset; - /* Looks ok. */ - cache = file; - - /* Check for new version. */ - offset = ALIGN_CACHE (sizeof (struct cache_file) - + cache->nlibs * sizeof (struct file_entry)); - - cache_new = (struct cache_file_new *) ((void *) cache + offset); - if (cachesize < (offset + sizeof (struct cache_file_new)) - || memcmp (cache_new->magic, CACHEMAGIC_VERSION_NEW, - sizeof CACHEMAGIC_VERSION_NEW - 1) != 0) - cache_new = (void *) -1; - else - { - if (! cache_file_new_matches_endian (cache_new)) - { - /* The old-format part of the cache is bogus as well - if the endianness does not match. (But it is - unclear how the new header can be located if the - endianness does not match.) */ - cache = (void *) -1; - cache_new = (void *) -1; - __munmap (file, cachesize); - } - } - } - else - { - if (file != MAP_FAILED) - __munmap (file, cachesize); - cache = (void *) -1; - } + if (_dl_check_ldsocache_needs_loading ()) + _dl_maybe_load_ldsocache (); - assert (cache != NULL); - } - - if (cache == (void *) -1) - /* Previously looked for the cache file and didn't find it. */ + if (cache == NULL) return NULL; const char *best; - if (cache_new != (void *) -1) + if (cache_new != NULL) { const char *string_table = (const char *) cache_new; best = search_cache (string_table, cachesize, @@ -505,7 +594,7 @@ _dl_load_cache_lookup (const char *name) return NULL; /* The double copy is *required* since malloc may be interposed - and call dlopen itself whose completion would unmap the data + and call dlopen itself whose completion may unmap the data we are accessing. Therefore we must make the copy of the mapping data without using malloc. */ char *temp; @@ -523,14 +612,7 @@ _dl_load_cache_lookup (const char *name) void _dl_unload_cache (void) { - if (cache != NULL && cache != (struct cache_file *) -1) - { - __munmap (cache, cachesize); - cache = NULL; - } -#ifdef SHARED - /* This marks the glibc_hwcaps_priorities array as out-of-date. */ - glibc_hwcaps_priorities_length = 0; -#endif + /* Functionality is no longer needed, but kept for internal ABI for + now. */ } #endif