/* * libkmod - interface to kernel module operations * * Copyright (C) 2011-2013 ProFUSION embedded systems * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, see . */ #include #include #include #include #include #include #include #include #include #ifdef ENABLE_ZSTD #include #endif #ifdef ENABLE_XZ #include #endif #ifdef ENABLE_ZLIB #include #endif #include #include "libkmod.h" #include "libkmod-internal.h" struct kmod_file { int fd; enum kmod_file_compression_type compression; off_t size; void *memory; int (*load)(struct kmod_file *file); const struct kmod_ctx *ctx; struct kmod_elf *elf; }; #ifdef ENABLE_ZSTD static int zstd_read_block(struct kmod_file *file, size_t block_size, ZSTD_inBuffer *input, size_t *input_capacity) { ssize_t rdret; int ret; if (*input_capacity < block_size) { free((void *)input->src); input->src = malloc(block_size); if (input->src == NULL) { ret = -errno; ERR(file->ctx, "zstd: %m\n"); return ret; } *input_capacity = block_size; } rdret = read(file->fd, (void *)input->src, block_size); if (rdret < 0) { ret = -errno; ERR(file->ctx, "zstd: %m\n"); return ret; } input->pos = 0; input->size = rdret; return 0; } static int zstd_ensure_outbuffer_space(ZSTD_outBuffer *buffer, size_t min_free) { uint8_t *old_buffer = buffer->dst; int ret = 0; if (buffer->size - buffer->pos >= min_free) return 0; buffer->size += min_free; buffer->dst = realloc(buffer->dst, buffer->size); if (buffer->dst == NULL) { ret = -errno; free(old_buffer); } return ret; } static int zstd_decompress_block(struct kmod_file *file, ZSTD_DStream *dstr, ZSTD_inBuffer *input, ZSTD_outBuffer *output, size_t *next_block_size) { size_t out_buf_min_size = ZSTD_DStreamOutSize(); int ret = 0; do { ssize_t dsret; ret = zstd_ensure_outbuffer_space(output, out_buf_min_size); if (ret) { ERR(file->ctx, "zstd: %s\n", strerror(-ret)); break; } dsret = ZSTD_decompressStream(dstr, output, input); if (ZSTD_isError(dsret)) { ret = -EINVAL; ERR(file->ctx, "zstd: %s\n", ZSTD_getErrorName(dsret)); break; } if (dsret > 0) *next_block_size = (size_t)dsret; } while (input->pos < input->size || output->pos > output->size || output->size - output->pos < out_buf_min_size); return ret; } static int load_zstd(struct kmod_file *file) { ZSTD_DStream *dstr; size_t next_block_size; size_t zst_inb_capacity = 0; ZSTD_inBuffer zst_inb = { 0 }; ZSTD_outBuffer zst_outb = { 0 }; int ret; dstr = ZSTD_createDStream(); if (dstr == NULL) { ret = -EINVAL; ERR(file->ctx, "zstd: Failed to create decompression stream\n"); goto out; } next_block_size = ZSTD_initDStream(dstr); while (true) { ret = zstd_read_block(file, next_block_size, &zst_inb, &zst_inb_capacity); if (ret != 0) goto out; if (zst_inb.size == 0) /* EOF */ break; ret = zstd_decompress_block(file, dstr, &zst_inb, &zst_outb, &next_block_size); if (ret != 0) goto out; } ZSTD_freeDStream(dstr); free((void *)zst_inb.src); file->memory = zst_outb.dst; file->size = zst_outb.pos; return 0; out: if (dstr != NULL) ZSTD_freeDStream(dstr); free((void *)zst_inb.src); free((void *)zst_outb.dst); return ret; } #else static int load_zstd(struct kmod_file *file) { return -ENOSYS; } #endif static const char magic_zstd[] = {0x28, 0xB5, 0x2F, 0xFD}; #ifdef ENABLE_XZ static void xz_uncompress_belch(struct kmod_file *file, lzma_ret ret) { switch (ret) { case LZMA_MEM_ERROR: ERR(file->ctx, "xz: %s\n", strerror(ENOMEM)); break; case LZMA_FORMAT_ERROR: ERR(file->ctx, "xz: File format not recognized\n"); break; case LZMA_OPTIONS_ERROR: ERR(file->ctx, "xz: Unsupported compression options\n"); break; case LZMA_DATA_ERROR: ERR(file->ctx, "xz: File is corrupt\n"); break; case LZMA_BUF_ERROR: ERR(file->ctx, "xz: Unexpected end of input\n"); break; default: ERR(file->ctx, "xz: Internal error (bug)\n"); break; } } static int xz_uncompress(lzma_stream *strm, struct kmod_file *file) { uint8_t in_buf[BUFSIZ], out_buf[BUFSIZ]; lzma_action action = LZMA_RUN; lzma_ret ret; void *p = NULL; size_t total = 0; strm->avail_in = 0; strm->next_out = out_buf; strm->avail_out = sizeof(out_buf); while (true) { if (strm->avail_in == 0) { ssize_t rdret = read(file->fd, in_buf, sizeof(in_buf)); if (rdret < 0) { ret = -errno; goto out; } strm->next_in = in_buf; strm->avail_in = rdret; if (rdret == 0) action = LZMA_FINISH; } ret = lzma_code(strm, action); if (strm->avail_out == 0 || ret != LZMA_OK) { size_t write_size = BUFSIZ - strm->avail_out; char *tmp = realloc(p, total + write_size); if (tmp == NULL) { ret = -errno; goto out; } memcpy(tmp + total, out_buf, write_size); total += write_size; p = tmp; strm->next_out = out_buf; strm->avail_out = BUFSIZ; } if (ret == LZMA_STREAM_END) break; if (ret != LZMA_OK) { xz_uncompress_belch(file, ret); ret = -EINVAL; goto out; } } file->memory = p; file->size = total; return 0; out: free(p); return ret; } static int load_xz(struct kmod_file *file) { lzma_stream strm = LZMA_STREAM_INIT; lzma_ret lzret; int ret; lzret = lzma_stream_decoder(&strm, UINT64_MAX, LZMA_CONCATENATED); if (lzret == LZMA_MEM_ERROR) { ERR(file->ctx, "xz: %s\n", strerror(ENOMEM)); return -ENOMEM; } else if (lzret != LZMA_OK) { ERR(file->ctx, "xz: Internal error (bug)\n"); return -EINVAL; } ret = xz_uncompress(&strm, file); lzma_end(&strm); return ret; } #else static int load_xz(struct kmod_file *file) { return -ENOSYS; } #endif static const char magic_xz[] = {0xfd, '7', 'z', 'X', 'Z', 0}; #ifdef ENABLE_ZLIB #define READ_STEP (4 * 1024 * 1024) static int load_zlib(struct kmod_file *file) { int err = 0; off_t did = 0, total = 0; _cleanup_free_ unsigned char *p = NULL; gzFile gzf; int gzfd; errno = 0; gzfd = fcntl(file->fd, F_DUPFD_CLOEXEC, 3); if (gzfd < 0) return -errno; gzf = gzdopen(gzfd, "rb"); /* takes ownership of the fd */ if (gzf == NULL) { close(gzfd); return -errno; } for (;;) { int r; if (did == total) { void *tmp = realloc(p, total + READ_STEP); if (tmp == NULL) { err = -errno; goto error; } total += READ_STEP; p = tmp; } r = gzread(gzf, p + did, total - did); if (r == 0) break; else if (r < 0) { int gzerr; const char *gz_errmsg = gzerror(gzf, &gzerr); ERR(file->ctx, "gzip: %s\n", gz_errmsg); /* gzip might not set errno here */ err = gzerr == Z_ERRNO ? -errno : -EINVAL; goto error; } did += r; } file->memory = p; file->size = did; p = NULL; gzclose(gzf); return 0; error: gzclose(gzf); /* closes the gzfd */ return err; } #else static int load_zlib(struct kmod_file *file) { return -ENOSYS; } #endif static const char magic_zlib[] = {0x1f, 0x8b}; static int load_reg(struct kmod_file *file) { struct stat st; if (fstat(file->fd, &st) < 0) return -errno; file->size = st.st_size; file->memory = mmap(NULL, file->size, PROT_READ, MAP_PRIVATE, file->fd, 0); if (file->memory == MAP_FAILED) { file->memory = NULL; return -errno; } return 0; } static const struct comp_type { size_t magic_size; enum kmod_file_compression_type compression; const char *magic_bytes; int (*load)(struct kmod_file *file); } comp_types[] = { {sizeof(magic_zstd), KMOD_FILE_COMPRESSION_ZSTD, magic_zstd, load_zstd}, {sizeof(magic_xz), KMOD_FILE_COMPRESSION_XZ, magic_xz, load_xz}, {sizeof(magic_zlib), KMOD_FILE_COMPRESSION_ZLIB, magic_zlib, load_zlib}, {0, KMOD_FILE_COMPRESSION_NONE, NULL, load_reg} }; struct kmod_elf *kmod_file_get_elf(struct kmod_file *file) { int err; if (file->elf) return file->elf; err = kmod_file_load_contents(file); if (err) { errno = err; return NULL; } file->elf = kmod_elf_new(file->memory, file->size); return file->elf; } struct kmod_file *kmod_file_open(const struct kmod_ctx *ctx, const char *filename) { struct kmod_file *file; char buf[7]; ssize_t sz; assert_cc(sizeof(magic_zstd) < sizeof(buf)); assert_cc(sizeof(magic_xz) < sizeof(buf)); assert_cc(sizeof(magic_zlib) < sizeof(buf)); file = calloc(1, sizeof(struct kmod_file)); if (file == NULL) return NULL; file->fd = open(filename, O_RDONLY|O_CLOEXEC); if (file->fd < 0) { free(file); return NULL; } sz = read_str_safe(file->fd, buf, sizeof(buf)); lseek(file->fd, 0, SEEK_SET); if (sz != (sizeof(buf) - 1)) { if (sz < 0) errno = -sz; else errno = EINVAL; close(file->fd); free(file); return NULL; } for (unsigned int i = 0; i < ARRAY_SIZE(comp_types); i++) { const struct comp_type *itr = &comp_types[i]; file->load = itr->load; file->compression = itr->compression; if (itr->magic_size && memcmp(buf, itr->magic_bytes, itr->magic_size) == 0) { break; } } file->ctx = ctx; return file; } /* * Callers should just check file->memory got updated */ int kmod_file_load_contents(struct kmod_file *file) { if (file->memory) return 0; /* The load functions already log possible errors. */ return file->load(file); } void *kmod_file_get_contents(const struct kmod_file *file) { return file->memory; } off_t kmod_file_get_size(const struct kmod_file *file) { return file->size; } enum kmod_file_compression_type kmod_file_get_compression(const struct kmod_file *file) { return file->compression; } int kmod_file_get_fd(const struct kmod_file *file) { return file->fd; } void kmod_file_unref(struct kmod_file *file) { if (file->elf) kmod_elf_unref(file->elf); if (file->compression == KMOD_FILE_COMPRESSION_NONE) { if (file->memory) munmap(file->memory, file->size); } else { free(file->memory); } close(file->fd); free(file); }