From: Alexey Gladkov <gladkov.alexey@gmail.com>
To: Sakib Sarkar <sakibsarkar74@gmail.com>
Cc: kbd@lists.linux.dev
Subject: Re: Security: OS command injection.
Date: Thu, 16 Jul 2026 12:11:56 +0200 [thread overview]
Message-ID: <aliubIWH_hQ0hA4O@example.org> (raw)
In-Reply-To: <CAFZjH1OGMXy-zFg1OddhYcyhcz3gtnhfpB-pHhvbejLa3xAEdw@mail.gmail.com>
On Thu, Jul 16, 2026 at 02:47:35PM +0700, Sakib Sarkar wrote:
> Hello,
> The report is a markdown file and attached.
> Please let me know if more information is needed
Thanks for the report.
> Thanks
> Sakib Sarkar
> # Command Injection in libkbdfile Decompression Fallback
>
> **CWE:** CWE-78 (OS Command Injection)
> **Component:** `src/libkbdfile/kbdfile.c` — `kbd` project (legionus/kbd)
> **Affected consumers:** `loadkeys`, `setfont`, `psfxtable`
psfxtable is not intended for use on the system at all.
You're missing the point that the `loadkeys` and `setfont` tools should be
controlled exclusively by root. The root should not execute them with
untrusted arguments; otherwise, an incorrect filename will be the least of
his problems.
I don't find the described PoC relevant because if an attacker controls
the path to the keymap, he will be able to gain control of the system in
another way by using the keymap.
> ---
>
> ## 1. Summary
>
> `libkbdfile`, the shared file-opening layer used by `loadkeys`, `setfont`, and `psfxtable`, builds a shell command string from an unsanitized file path and executes it via `popen()` when its in-process decompression fallback is unavailable. A path containing shell metacharacters results in arbitrary command execution in the context of the process running the affected tool.
>
> Because the vulnerable code lives in the shared library rather than in any individual binary, **every consumer of libkbdfile inherits the bug** — this is not specific to one tool's argument parsing.
>
> ---
>
> ## 2. Technical Details
>
> ### 2.1 Root cause
>
> In `src/libkbdfile/kbdfile.c`:
>
> - `open_pathname()` inspects the first two bytes of a file for known compression magic. If a match is found, it first tries the corresponding in-process decompressor (loaded at runtime via `dlopen` against libz/liblzma/libzstd/libbz2).
> - If that decompressor is unavailable (library not installed, `dlopen` fails) and `KBDFILE_IGNORE_DECOMP_UTILS` is not set, it falls back to `pipe_open()` (kbdfile.c:149–167):
>
> ```c
> static int
> pipe_open(const struct decompressor *dc, struct kbdfile *fp)
> {
> char *pipe_cmd;
>
> pipe_cmd = malloc(strlen(dc->cmd) + strlen(fp->pathname) + 2);
> if (pipe_cmd == NULL)
> return -1;
>
> sprintf(pipe_cmd, "%s %s", dc->cmd, fp->pathname);
>
> fp->fd = popen(pipe_cmd, "r");
> ...
> }
> ```
>
> `fp->pathname` is the caller-supplied path, set directly via `kbdfile_set_pathname()` or constructed with an appended suffix (`.gz`/`.bz2`/`.xz`/`.zst`) in `maybe_pipe_open()`. It is never shell-quoted, escaped, or validated. `popen()` invokes `/bin/sh -c` on the resulting string, so shell metacharacters (`;`, `|`, `` ` ``, `$()`, etc.) in the path are interpreted by the shell rather than treated as a literal filename.
>
> ### 2.2 Trigger conditions
>
> The vulnerable path is reached when:
> 1. A file's magic bytes (or suffix, via `maybe_pipe_open()`'s retry logic) match a supported compression format, **and**
> 2. The in-process decompressor for that format is unavailable at runtime (missing shared library dependency).
>
> This makes the bug latent/environment-dependent: on systems where zlib/liblzma/etc. are always present, the in-process path is taken and the shell fallback is never reached. On minimal or stripped-down systems (containers, embedded images, custom installs), the fallback becomes live.
>
> ### 2.3 Proof of concept
>
> ```sh
> #!/bin/sh
> # PoC: command injection via unsanitized path in kbdfile pipe_open fallback
> set -e
> WORK="/tmp/x; touch /tmp/PWNED #"
> mkdir -p "$WORK"
> printf '\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff' > "$WORK/evil.gz"
> loadkeys "$WORK/evil.gz" 2>/dev/null || true
> [ -e /tmp/PWNED ] && echo "VULNERABLE" || echo "not triggered (in-process decompressor present)"
> ```
>
> With libz absent, `loadkeys` resolves the gzip magic, the in-process decompressor returns `NULL`, and `pipe_open()` executes `gzip -d -c /tmp/x; touch /tmp/PWNED #/evil.gz` via the shell — the `;` terminates the intended command and runs the injected one.
>
> ---
>
> ## 3. Remediation
>
> 1. **Primary fix — eliminate the shell entirely.** Replace the `popen()`-based fallback with a direct `fork()`/`execvp()` (or `posix_spawn`) call using an argv array, so the path is never interpreted by `/bin/sh`:
>
> ```c
> static int
> pipe_open(const struct decompressor *dc, struct kbdfile *fp)
> {
> int pipefd[2];
> pid_t pid;
>
> if (pipe(pipefd) != 0)
> return -1;
>
> pid = fork();
> if (pid == 0) {
> dup2(pipefd[1], STDOUT_FILENO);
> close(pipefd[0]);
> close(pipefd[1]);
> execlp(dc->cmd_token, dc->cmd_token, fp->pathname, (char *)NULL);
> _exit(127);
> }
> close(pipefd[1]);
> fp->fd = fdopen(pipefd[0], "r");
> return (fp->fd == NULL) ? -1 : 0;
> }
> ```
--
Rgrds, legion
prev parent reply other threads:[~2026-07-16 10:12 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-16 7:47 Security: OS command injection Sakib Sarkar
2026-07-16 10:11 ` Alexey Gladkov [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=aliubIWH_hQ0hA4O@example.org \
--to=gladkov.alexey@gmail.com \
--cc=kbd@lists.linux.dev \
--cc=sakibsarkar74@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.