mirror of
https://github.com/penpot/penpot.git
synced 2026-07-01 20:05:26 +00:00
* ✨ Add font processing resource limits via prlimit Font processing tools (fontforge, sfnt2woff, woff2sfnt, woff2_decompress) were invoked via clojure.java.shell/sh with no timeouts or resource limits. This adds process-level resource limits using prlimit(1) and the shell/exec! infrastructure from the ImageMagick hardening work. shell/exec! changes: - Add :prlimit parameter that prepends prlimit(1) to the command - :prlimit takes {:mem <MiB> :cpu <seconds>} for address space and CPU time limits, enforced by the kernel's RLIMIT subsystem - prlimit-cmd builds the prlimit command prefix (private helper) Font processing changes: - Replace all clojure.java.shell/sh calls with shell/exec! via exec-font! - exec-font! applies font-prlimit (512 MiB, 30s CPU, 60s wall-clock) - All 5 conversion functions (ttf->otf, otf->ttf, ttf-or-otf->woff, woff->sfnt, woff2->sfnt) use try/finally for explicit temp file cleanup - Remove clojure.java.shell require from media.clj Tests: - Add exec-prlimit-normal, exec-prlimit-cpu, exec-prlimit-memory tests Closes #10234 Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app> * ✨ Make font processing resource limits configurable Replace hardcoded font-prlimit map and wall-clock timeout with config-driven values under the PENPOT_FONT_PROCESS_* namespace. The prlimit implementation detail is not exposed in config keys. Co-authored-by: deepseek-v4-flash <deepseek-v4-flash@penpot.app> --------- Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app> Co-authored-by: deepseek-v4-flash <deepseek-v4-flash@penpot.app>
108 lines
4.2 KiB
Clojure
108 lines
4.2 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
|
|
|
(ns backend-tests.shell-test
|
|
(:require
|
|
[app.common.exceptions :as ex]
|
|
[app.util.shell :as shell]
|
|
[clojure.string :as str]
|
|
[clojure.test :as t]))
|
|
|
|
(t/deftest exec-normal-completes
|
|
(t/testing "normal process completes within timeout"
|
|
(let [result (shell/exec! {}
|
|
:cmd ["echo" "hello"]
|
|
:timeout 10)]
|
|
(t/is (= 0 (:exit result)))
|
|
(t/is (str/includes? (:out result) "hello")))))
|
|
|
|
(t/deftest exec-captures-stderr
|
|
(t/testing "stderr is captured separately"
|
|
(let [result (shell/exec! {}
|
|
:cmd ["bash" "-c" "echo out; echo err >&2"]
|
|
:timeout 10)]
|
|
(t/is (= 0 (:exit result)))
|
|
(t/is (str/includes? (:out result) "out"))
|
|
(t/is (str/includes? (:err result) "err")))))
|
|
|
|
(t/deftest exec-non-zero-exit
|
|
(t/testing "non-zero exit code is captured"
|
|
(let [result (shell/exec! {}
|
|
:cmd ["bash" "-c" "exit 42"]
|
|
:timeout 10)]
|
|
(t/is (= 42 (:exit result))))))
|
|
|
|
(t/deftest exec-with-env
|
|
(t/testing "environment variables are passed to the process"
|
|
(let [result (shell/exec! {}
|
|
:cmd ["bash" "-c" "echo $MY_VAR"]
|
|
:env {"MY_VAR" "test-value"}
|
|
:timeout 10)]
|
|
(t/is (= 0 (:exit result)))
|
|
(t/is (str/includes? (:out result) "test-value")))))
|
|
|
|
(t/deftest exec-with-input
|
|
(t/testing "stdin input is passed to the process"
|
|
(let [result (shell/exec! {}
|
|
:cmd ["cat"]
|
|
:in "hello from stdin"
|
|
:timeout 10)]
|
|
(t/is (= 0 (:exit result)))
|
|
(t/is (str/includes? (:out result) "hello from stdin")))))
|
|
|
|
(t/deftest exec-timeout-kills-process
|
|
(t/testing "process that exceeds timeout is killed and raises exception"
|
|
(let [start (System/currentTimeMillis)]
|
|
(try
|
|
(shell/exec! {}
|
|
:cmd ["sleep" "60"]
|
|
:timeout 1)
|
|
(t/is false "should have thrown")
|
|
(catch Exception e
|
|
(let [elapsed (- (System/currentTimeMillis) start)
|
|
data (ex-data e)]
|
|
;; Should complete quickly due to timeout, not wait 60s
|
|
(t/is (< elapsed 10000) "process should be killed within ~1 second")
|
|
(t/is (= :internal (:type data)))
|
|
(t/is (= :process-timeout (:code data)))
|
|
(t/is (= 1 (:timeout data)))))))))
|
|
|
|
(t/deftest exec-no-timeout-waits
|
|
(t/testing "without timeout, process runs to completion"
|
|
(let [result (shell/exec! {}
|
|
:cmd ["sleep" "0.1"]
|
|
:timeout nil)]
|
|
(t/is (= 0 (:exit result))))))
|
|
|
|
(t/deftest exec-prlimit-normal
|
|
(t/testing "normal process completes within prlimit"
|
|
(let [result (shell/exec! {}
|
|
:cmd ["echo" "hello"]
|
|
:prlimit {:mem 256 :cpu 10}
|
|
:timeout 10)]
|
|
(t/is (= 0 (:exit result)))
|
|
(t/is (str/includes? (:out result) "hello")))))
|
|
|
|
(t/deftest exec-prlimit-cpu
|
|
(t/testing "process exceeding CPU limit is killed"
|
|
(let [result (shell/exec! {}
|
|
:cmd ["bash" "-c" "while true; do :; done"]
|
|
:prlimit {:cpu 2}
|
|
:timeout 10)]
|
|
(t/is (not= 0 (:exit result))))))
|
|
|
|
(t/deftest exec-prlimit-memory
|
|
(t/testing "process exceeding memory limit is killed"
|
|
;; Use python3 to allocate more memory than the limit allows.
|
|
;; This test requires python3 to be available in the environment.
|
|
(let [result (shell/exec! {}
|
|
:cmd ["python3" "-c"
|
|
"import sys; x = bytearray(600 * 1024 * 1024); sys.exit(0)"]
|
|
:prlimit {:mem 256}
|
|
:timeout 10)]
|
|
;; Should fail because 600 MiB > 256 MiB limit
|
|
(t/is (not= 0 (:exit result))))))
|