commit - b89b0f9f7cd2526f6cf5e8100dbf094df79b2806
commit + 9ed49f84562145a3245831fa21ecd166a6b6ad03
blob - 264f0de9c56fe8475ec036508dc367fbed7d5396
blob + adcbeb49b51889b957dcc764d39bdb5c9a248808
--- Makefile
+++ Makefile
CSC ?= csc
CSC_LINK_FLAGS ?= -static
SHFMT ?= shfmt
-SHELL_FILES = bin/pisol test/integration.sh
+SHELL_FILES = bin/pisol test/integration.sh config/pisol/init
.PHONY: build tui test fmt fmt-check copy-config clean run install uninstall help
default: help
$(DESTDIR)$(BINDIR)/pisol
install: build
- $(INSTALL) -d $(DESTDIR)$(BINDIR) $(DESTDIR)$(LIBEXECDIR) $(DESTDIR)$(MANDIR)/man1
- $(INSTALL) -m 755 bin/pisol $(DESTDIR)$(BINDIR)/pisol
- $(INSTALL) -m 755 build/runtime/pisol $(DESTDIR)$(LIBEXECDIR)/pisol
- $(INSTALL) -m 644 doc/pisol.1 $(DESTDIR)$(MANDIR)/man1/pisol.1
+ @sudo=; \
+ if [ -z "$(DESTDIR)" ]; then \
+ d="$(BINDIR)"; \
+ while [ -n "$$d" ] && [ ! -e "$$d" ]; do d=$${d%/*}; done; \
+ [ -w "$$d" ] || sudo=sudo; \
+ fi; \
+ $$sudo $(INSTALL) -d $(DESTDIR)$(BINDIR) $(DESTDIR)$(LIBEXECDIR) $(DESTDIR)$(MANDIR)/man1; \
+ $$sudo $(INSTALL) -m 755 bin/pisol $(DESTDIR)$(BINDIR)/pisol; \
+ $$sudo $(INSTALL) -m 755 build/runtime/pisol $(DESTDIR)$(LIBEXECDIR)/pisol; \
+ $$sudo $(INSTALL) -m 644 doc/pisol.1 $(DESTDIR)$(MANDIR)/man1/pisol.1
uninstall:
$(RM) $(DESTDIR)$(BINDIR)/pisol $(DESTDIR)$(LIBEXECDIR)/pisol
blob - cbe803f49517cbee8e5aea0b0cfdcd4e5340152b
blob + 58e961af7f394a25b7fbdd7167ab8771d6a9940a
--- bin/pisol
+++ bin/pisol
# only widen the baseline, never narrow it.
args=()
-declare -A pisol_seen=()
+# A plain indexed array rather than an associative array, since macOS ships
+# bash 3.2 by default and declare -A requires bash 4+.
+pisol_seen=()
pisol_add_path() {
- local mode=$1 path=$2 key
+ local mode=$1 path=$2 key seen
[[ -e $path ]] || return 0
key=$mode:$path
- [[ -z ${pisol_seen[$key]:-} ]] || return 0
- pisol_seen[$key]=1
+ for seen in "${pisol_seen[@]:-}"; do
+ [[ $seen == "$key" ]] || continue
+ return 0
+ done
+ pisol_seen+=("$key")
args+=("$mode" "$path")
}
;;
--connect-tcp)
value=${args[index + 1]:-}
- if [[ ! $value =~ ^[0-9]+$ ]] || ((value < 1 || value > 65535)); then
+ # 10#$value forces base-10 arithmetic; without it bash treats a
+ # leading-zero value like 08 or 09 as invalid octal and the
+ # comparison errors out, silently passing validation.
+ if [[ ! $value =~ ^[0-9]+$ ]] || ((10#$value < 1 || 10#$value > 65535)); then
printf 'pisol: --connect-tcp port must be 1 to 65535, got %s\n' "$value" >&2
exit 2
fi
printf 'pisol: missing value for %s\n' "$grant" >&2
exit 2
fi
- socket=$(pisol_sbpl_quote "$path")
- profile+="(allow network-outbound (literal $socket))"$'\n'
- profile+="(allow file-read* file-write* (literal $socket))"$'\n'
+ # Resolved the same way as the file grants above: seatbelt
+ # subpath/literal filters only match fully resolved paths.
+ if path=$(pisol_realpath "$path"); then
+ socket=$(pisol_sbpl_quote "$path")
+ profile+="(allow network-outbound (literal $socket))"$'\n'
+ profile+="(allow file-read* file-write* (literal $socket))"$'\n'
+ fi
index=$((index + 2))
;;
--connect-tcp)
# A port grant is useless without name resolution, which seatbelt filters
# separately because it restricts UDP and Mach IPC as well as TCP.
- if ((${#ports[@]} > 0)); then
+ # --unrestricted-network needs the same resolver access: network* does
+ # not cover the mach-lookup DNS goes through.
+ if ((${#ports[@]} > 0)) || [[ -n $unrestricted_network ]]; then
profile+='(allow network-outbound (remote udp "*:53"))
(allow mach-lookup (global-name "com.apple.mDNSResponder"))
(allow network-outbound (literal "/private/var/run/mDNSResponder"))
profile+='(allow network*)'$'\n'
fi
- if ! grep -qF -- "$(pisol_sbpl_target "$pisol_project")" <<<"$profile"; then
+ if ! pisol_project_target=$(pisol_sbpl_target "$pisol_project"); then
+ pisol_project_target=
+ fi
+ if [[ -z $pisol_project_target ]] || ! grep -qF -- "$pisol_project_target" <<<"$profile"; then
printf 'pisol: failed to grant the project directory %s\n' "$pisol_project" >&2
exit 1
fi
blob - 87cdc9574857faac634fd70aedf1ce7440a19d82
blob + bf7ca4885a0750173c4c4410aa54dc0208b460c9
--- src/pisol/core.scm
+++ src/pisol/core.scm
(dynamic-wind
void
(lambda ()
- (receive (_ normal? status) (process-wait pid)
- (unless (and normal? (zero? status))
- (let* ((raw (read-string #f err))
- (message (if (eof-object? raw)
- "configuration evaluation failed"
- (trim raw))))
- (error 'load-configuration message path))))
- (let ((text (read-string #f in)))
+ (close-output-port out)
+ ;; Drain both pipes before waiting on the child: a config that
+ ;; emits more than a pipe buffer of output would otherwise block
+ ;; bash on write while this process blocks in process-wait,
+ ;; deadlocking pisol.
+ (let ((text (read-string #f in))
+ (raw-err (read-string #f err)))
+ (receive (_ normal? status) (process-wait pid)
+ (unless (and normal? (zero? status))
+ (let ((message (if (eof-object? raw-err)
+ "configuration evaluation failed"
+ (trim raw-err))))
+ (error 'load-configuration message path))))
(if (or (eof-object? text) (zero? (string-length text)))
'()
(nul-strings text))))
(lambda ()
(close-input-port in)
- (close-output-port out)
(close-input-port err))))))
(define (digits? text)
(begin (when (file-exists? temporary) (delete-file* temporary)) (abort exn))
(with-output-to-file temporary (lambda () (display (config->shell config))))
(set-file-permissions! temporary #o600)
- (when (file-exists? path) (delete-file path))
- (rename-file temporary path)
+ ;; Clobber via rename-file's own flag rather than delete-then-rename,
+ ;; so the replacement is atomic and a kill between the two steps
+ ;; cannot leave the user with neither file.
+ (rename-file temporary path #t)
(configuration-source-style-set! config 'canonical)
(configuration-dirty?-set! config #f)
#t))))
blob - 1f69d2762d5ab78674c417578d4669c2c3f072f3
blob + f0b1333a5958b6a74337dbd610c36ebb1b186c92
--- src/pisol/tui.scm
+++ src/pisol/tui.scm
(module pisol.tui (run-tui)
(import scheme (chicken base) (chicken condition) (chicken file) (chicken file posix) (chicken format)
- (chicken io) (chicken process) (chicken process-context) (chicken time)
+ (chicken io) (chicken port) (chicken process) (chicken process-context)
pisol.core)
(define esc (string (integer->char 27)))
(define color? (not (get-environment-variable "NO_COLOR")))
((char-whitespace? (string-ref text at)) (values (substring text 0 at) (trim (substring text at (string-length text)))))
(else (loop (+ at 1)))))))
(define (read-input prompt) (display prompt) (flush-output) (let ((line (read-line))) (and (not (eof-object? line)) line)))
- (define (terminal-input?) (zero? (system "test -t 0")))
- (define tui-temporary-counter 0)
+ (define (terminal-input?) (terminal-port? (current-input-port)))
(define (temporary-path)
- (set! tui-temporary-counter (+ tui-temporary-counter 1))
- (string-append (or (get-environment-variable "TMPDIR") "/tmp") "/pisol-path-" (number->string (current-seconds)) "." (number->string tui-temporary-counter)))
+ ;; file-mkstemp rather than a counter derived from the current second:
+ ;; two tui runs started in the same second could otherwise collide on
+ ;; the same path and race to read each other's captured input.
+ (let ((template (string-append (or (get-environment-variable "TMPDIR") "/tmp") "/pisol-path.XXXXXX")))
+ (receive (fd path) (file-mkstemp template)
+ (file-close fd)
+ path)))
(define (read-directory-path prompt initial)
(if (not (terminal-input?)) (read-input prompt)
(let ((output (temporary-path)))