linux/kernel/module/decompress.c
Luis Chamberlain df3e764d8e module: add debug stats to help identify memory pressure
Loading modules with finit_module() can end up using vmalloc(), vmap()
and vmalloc() again, for a total of up to 3 separate allocations in the
worst case for a single module. We always kernel_read*() the module,
that's a vmalloc(). Then vmap() is used for the module decompression,
and if so the last read buffer is freed as we use the now decompressed
module buffer to stuff data into our copy module. The last allocation is
specific to each architectures but pretty much that's generally a series
of vmalloc() calls or a variation of vmalloc to handle ELF sections with
special permissions.

Evaluation with new stress-ng module support [1] with just 100 ops
is proving that you can end up using GiBs of data easily even with all
care we have in the kernel and userspace today in trying to not load modules
which are already loaded. 100 ops seems to resemble the sort of pressure a
system with about 400 CPUs can create on module loading. Although issues
relating to duplicate module requests due to each CPU inucurring a new
module reuest is silly and some of these are being fixed, we currently lack
proper tooling to help diagnose easily what happened, when it happened
and who likely is to blame -- userspace or kernel module autoloading.

Provide an initial set of stats which use debugfs to let us easily scrape
post-boot information about failed loads. This sort of information can
be used on production worklaods to try to optimize *avoiding* redundant
memory pressure using finit_module().

There's a few examples that can be provided:

A 255 vCPU system without the next patch in this series applied:

Startup finished in 19.143s (kernel) + 7.078s (userspace) = 26.221s
graphical.target reached after 6.988s in userspace

And 13.58 GiB of virtual memory space lost due to failed module loading:

root@big ~ # cat /sys/kernel/debug/modules/stats
         Mods ever loaded       67
     Mods failed on kread       0
Mods failed on decompress       0
  Mods failed on becoming       0
      Mods failed on load       1411
        Total module size       11464704
      Total mod text size       4194304
       Failed kread bytes       0
  Failed decompress bytes       0
    Failed becoming bytes       0
        Failed kmod bytes       14588526272
 Virtual mem wasted bytes       14588526272
         Average mod size       171115
    Average mod text size       62602
  Average fail load bytes       10339140
Duplicate failed modules:
              module-name        How-many-times                    Reason
                kvm_intel                   249                      Load
                      kvm                   249                      Load
                irqbypass                     8                      Load
         crct10dif_pclmul                   128                      Load
      ghash_clmulni_intel                    27                      Load
             sha512_ssse3                    50                      Load
           sha512_generic                   200                      Load
              aesni_intel                   249                      Load
              crypto_simd                    41                      Load
                   cryptd                   131                      Load
                    evdev                     2                      Load
                serio_raw                     1                      Load
               virtio_pci                     3                      Load
                     nvme                     3                      Load
                nvme_core                     3                      Load
    virtio_pci_legacy_dev                     3                      Load
    virtio_pci_modern_dev                     3                      Load
                   t10_pi                     3                      Load
                   virtio                     3                      Load
             crc32_pclmul                     6                      Load
           crc64_rocksoft                     3                      Load
             crc32c_intel                    40                      Load
              virtio_ring                     3                      Load
                    crc64                     3                      Load

The following screen shot, of a simple 8vcpu 8 GiB KVM guest with the
next patch in this series applied, shows 226.53 MiB are wasted in virtual
memory allocations which due to duplicate module requests during boot.
It also shows an average module memory size of 167.10 KiB and an an
average module .text + .init.text size of 61.13 KiB. The end shows all
modules which were detected as duplicate requests and whether or not
they failed early after just the first kernel_read*() call or late after
we've already allocated the private space for the module in
layout_and_allocate(). A system with module decompression would reveal
more wasted virtual memory space.

We should put effort now into identifying the source of these duplicate
module requests and trimming these down as much possible. Larger systems
will obviously show much more wasted virtual memory allocations.

root@kmod ~ # cat /sys/kernel/debug/modules/stats
         Mods ever loaded       67
     Mods failed on kread       0
Mods failed on decompress       0
  Mods failed on becoming       83
      Mods failed on load       16
        Total module size       11464704
      Total mod text size       4194304
       Failed kread bytes       0
  Failed decompress bytes       0
    Failed becoming bytes       228959096
        Failed kmod bytes       8578080
 Virtual mem wasted bytes       237537176
         Average mod size       171115
    Average mod text size       62602
  Avg fail becoming bytes       2758544
  Average fail load bytes       536130
Duplicate failed modules:
              module-name        How-many-times                    Reason
                kvm_intel                     7                  Becoming
                      kvm                     7                  Becoming
                irqbypass                     6           Becoming & Load
         crct10dif_pclmul                     7           Becoming & Load
      ghash_clmulni_intel                     7           Becoming & Load
             sha512_ssse3                     6           Becoming & Load
           sha512_generic                     7           Becoming & Load
              aesni_intel                     7                  Becoming
              crypto_simd                     7           Becoming & Load
                   cryptd                     3           Becoming & Load
                    evdev                     1                  Becoming
                serio_raw                     1                  Becoming
                     nvme                     3                  Becoming
                nvme_core                     3                  Becoming
                   t10_pi                     3                  Becoming
               virtio_pci                     3                  Becoming
             crc32_pclmul                     6           Becoming & Load
           crc64_rocksoft                     3                  Becoming
             crc32c_intel                     3                  Becoming
    virtio_pci_modern_dev                     2                  Becoming
    virtio_pci_legacy_dev                     1                  Becoming
                    crc64                     2                  Becoming
                   virtio                     2                  Becoming
              virtio_ring                     2                  Becoming

[0] https://github.com/ColinIanKing/stress-ng.git
[1] echo 0 > /proc/sys/vm/oom_dump_tasks
    ./stress-ng --module 100 --module-name xfs

Signed-off-by: Luis Chamberlain <mcgrof@kernel.org>
2023-04-18 11:15:24 -07:00

369 lines
7.8 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright 2021 Google LLC.
*/
#include <linux/init.h>
#include <linux/highmem.h>
#include <linux/kobject.h>
#include <linux/mm.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/sysfs.h>
#include <linux/vmalloc.h>
#include "internal.h"
static int module_extend_max_pages(struct load_info *info, unsigned int extent)
{
struct page **new_pages;
new_pages = kvmalloc_array(info->max_pages + extent,
sizeof(info->pages), GFP_KERNEL);
if (!new_pages)
return -ENOMEM;
memcpy(new_pages, info->pages, info->max_pages * sizeof(info->pages));
kvfree(info->pages);
info->pages = new_pages;
info->max_pages += extent;
return 0;
}
static struct page *module_get_next_page(struct load_info *info)
{
struct page *page;
int error;
if (info->max_pages == info->used_pages) {
error = module_extend_max_pages(info, info->used_pages);
if (error)
return ERR_PTR(error);
}
page = alloc_page(GFP_KERNEL | __GFP_HIGHMEM);
if (!page)
return ERR_PTR(-ENOMEM);
info->pages[info->used_pages++] = page;
return page;
}
#if defined(CONFIG_MODULE_COMPRESS_GZIP)
#include <linux/zlib.h>
#define MODULE_COMPRESSION gzip
#define MODULE_DECOMPRESS_FN module_gzip_decompress
/*
* Calculate length of the header which consists of signature, header
* flags, time stamp and operating system ID (10 bytes total), plus
* an optional filename.
*/
static size_t module_gzip_header_len(const u8 *buf, size_t size)
{
const u8 signature[] = { 0x1f, 0x8b, 0x08 };
size_t len = 10;
if (size < len || memcmp(buf, signature, sizeof(signature)))
return 0;
if (buf[3] & 0x08) {
do {
/*
* If we can't find the end of the file name we must
* be dealing with a corrupted file.
*/
if (len == size)
return 0;
} while (buf[len++] != '\0');
}
return len;
}
static ssize_t module_gzip_decompress(struct load_info *info,
const void *buf, size_t size)
{
struct z_stream_s s = { 0 };
size_t new_size = 0;
size_t gzip_hdr_len;
ssize_t retval;
int rc;
gzip_hdr_len = module_gzip_header_len(buf, size);
if (!gzip_hdr_len) {
pr_err("not a gzip compressed module\n");
return -EINVAL;
}
s.next_in = buf + gzip_hdr_len;
s.avail_in = size - gzip_hdr_len;
s.workspace = kmalloc(zlib_inflate_workspacesize(), GFP_KERNEL);
if (!s.workspace)
return -ENOMEM;
rc = zlib_inflateInit2(&s, -MAX_WBITS);
if (rc != Z_OK) {
pr_err("failed to initialize decompressor: %d\n", rc);
retval = -EINVAL;
goto out;
}
do {
struct page *page = module_get_next_page(info);
if (IS_ERR(page)) {
retval = PTR_ERR(page);
goto out_inflate_end;
}
s.next_out = kmap_local_page(page);
s.avail_out = PAGE_SIZE;
rc = zlib_inflate(&s, 0);
kunmap_local(s.next_out);
new_size += PAGE_SIZE - s.avail_out;
} while (rc == Z_OK);
if (rc != Z_STREAM_END) {
pr_err("decompression failed with status %d\n", rc);
retval = -EINVAL;
goto out_inflate_end;
}
retval = new_size;
out_inflate_end:
zlib_inflateEnd(&s);
out:
kfree(s.workspace);
return retval;
}
#elif defined(CONFIG_MODULE_COMPRESS_XZ)
#include <linux/xz.h>
#define MODULE_COMPRESSION xz
#define MODULE_DECOMPRESS_FN module_xz_decompress
static ssize_t module_xz_decompress(struct load_info *info,
const void *buf, size_t size)
{
static const u8 signature[] = { 0xfd, '7', 'z', 'X', 'Z', 0 };
struct xz_dec *xz_dec;
struct xz_buf xz_buf;
enum xz_ret xz_ret;
size_t new_size = 0;
ssize_t retval;
if (size < sizeof(signature) ||
memcmp(buf, signature, sizeof(signature))) {
pr_err("not an xz compressed module\n");
return -EINVAL;
}
xz_dec = xz_dec_init(XZ_DYNALLOC, (u32)-1);
if (!xz_dec)
return -ENOMEM;
xz_buf.in_size = size;
xz_buf.in = buf;
xz_buf.in_pos = 0;
do {
struct page *page = module_get_next_page(info);
if (IS_ERR(page)) {
retval = PTR_ERR(page);
goto out;
}
xz_buf.out = kmap_local_page(page);
xz_buf.out_pos = 0;
xz_buf.out_size = PAGE_SIZE;
xz_ret = xz_dec_run(xz_dec, &xz_buf);
kunmap_local(xz_buf.out);
new_size += xz_buf.out_pos;
} while (xz_buf.out_pos == PAGE_SIZE && xz_ret == XZ_OK);
if (xz_ret != XZ_STREAM_END) {
pr_err("decompression failed with status %d\n", xz_ret);
retval = -EINVAL;
goto out;
}
retval = new_size;
out:
xz_dec_end(xz_dec);
return retval;
}
#elif defined(CONFIG_MODULE_COMPRESS_ZSTD)
#include <linux/zstd.h>
#define MODULE_COMPRESSION zstd
#define MODULE_DECOMPRESS_FN module_zstd_decompress
static ssize_t module_zstd_decompress(struct load_info *info,
const void *buf, size_t size)
{
static const u8 signature[] = { 0x28, 0xb5, 0x2f, 0xfd };
ZSTD_outBuffer zstd_dec;
ZSTD_inBuffer zstd_buf;
zstd_frame_header header;
size_t wksp_size;
void *wksp = NULL;
ZSTD_DStream *dstream;
size_t ret;
size_t new_size = 0;
int retval;
if (size < sizeof(signature) ||
memcmp(buf, signature, sizeof(signature))) {
pr_err("not a zstd compressed module\n");
return -EINVAL;
}
zstd_buf.src = buf;
zstd_buf.pos = 0;
zstd_buf.size = size;
ret = zstd_get_frame_header(&header, zstd_buf.src, zstd_buf.size);
if (ret != 0) {
pr_err("ZSTD-compressed data has an incomplete frame header\n");
retval = -EINVAL;
goto out;
}
if (header.windowSize > (1 << ZSTD_WINDOWLOG_MAX)) {
pr_err("ZSTD-compressed data has too large a window size\n");
retval = -EINVAL;
goto out;
}
wksp_size = zstd_dstream_workspace_bound(header.windowSize);
wksp = kmalloc(wksp_size, GFP_KERNEL);
if (!wksp) {
retval = -ENOMEM;
goto out;
}
dstream = zstd_init_dstream(header.windowSize, wksp, wksp_size);
if (!dstream) {
pr_err("Can't initialize ZSTD stream\n");
retval = -ENOMEM;
goto out;
}
do {
struct page *page = module_get_next_page(info);
if (!IS_ERR(page)) {
retval = PTR_ERR(page);
goto out;
}
zstd_dec.dst = kmap_local_page(page);
zstd_dec.pos = 0;
zstd_dec.size = PAGE_SIZE;
ret = zstd_decompress_stream(dstream, &zstd_dec, &zstd_buf);
kunmap_local(zstd_dec.dst);
retval = zstd_get_error_code(ret);
if (retval)
break;
new_size += zstd_dec.pos;
} while (zstd_dec.pos == PAGE_SIZE && ret != 0);
if (retval) {
pr_err("ZSTD-decompression failed with status %d\n", retval);
retval = -EINVAL;
goto out;
}
retval = new_size;
out:
kfree(wksp);
return retval;
}
#else
#error "Unexpected configuration for CONFIG_MODULE_DECOMPRESS"
#endif
int module_decompress(struct load_info *info, const void *buf, size_t size)
{
unsigned int n_pages;
ssize_t data_size;
int error;
#if defined(CONFIG_MODULE_STATS)
info->compressed_len = size;
#endif
/*
* Start with number of pages twice as big as needed for
* compressed data.
*/
n_pages = DIV_ROUND_UP(size, PAGE_SIZE) * 2;
error = module_extend_max_pages(info, n_pages);
data_size = MODULE_DECOMPRESS_FN(info, buf, size);
if (data_size < 0) {
error = data_size;
goto err;
}
info->hdr = vmap(info->pages, info->used_pages, VM_MAP, PAGE_KERNEL);
if (!info->hdr) {
error = -ENOMEM;
goto err;
}
info->len = data_size;
return 0;
err:
module_decompress_cleanup(info);
return error;
}
void module_decompress_cleanup(struct load_info *info)
{
int i;
if (info->hdr)
vunmap(info->hdr);
for (i = 0; i < info->used_pages; i++)
__free_page(info->pages[i]);
kvfree(info->pages);
info->pages = NULL;
info->max_pages = info->used_pages = 0;
}
#ifdef CONFIG_SYSFS
static ssize_t compression_show(struct kobject *kobj,
struct kobj_attribute *attr, char *buf)
{
return sysfs_emit(buf, __stringify(MODULE_COMPRESSION) "\n");
}
static struct kobj_attribute module_compression_attr = __ATTR_RO(compression);
static int __init module_decompress_sysfs_init(void)
{
int error;
error = sysfs_create_file(&module_kset->kobj,
&module_compression_attr.attr);
if (error)
pr_warn("Failed to create 'compression' attribute");
return 0;
}
late_initcall(module_decompress_sysfs_init);
#endif