Commit Diff


commit - b89b0f9f7cd2526f6cf5e8100dbf094df79b2806
commit + 9ed49f84562145a3245831fa21ecd166a6b6ad03
blob - 264f0de9c56fe8475ec036508dc367fbed7d5396
blob + adcbeb49b51889b957dcc764d39bdb5c9a248808
--- Makefile
+++ Makefile
@@ -9,7 +9,7 @@ RM ?= rm -f
 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
@@ -57,10 +57,16 @@ run: install
 	$(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
@@ -96,13 +96,18 @@ export ISOLATE_ENV=$pisol_project
 # 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")
 }
 
@@ -205,7 +210,10 @@ pisol_validate_grants() {
 			;;
 		--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
@@ -271,9 +279,13 @@ pisol_exec_seatbelt() {
 				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)
@@ -306,7 +318,9 @@ pisol_exec_seatbelt() {
 
 	# 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"))
@@ -318,7 +332,10 @@ pisol_exec_seatbelt() {
 		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
@@ -164,20 +164,24 @@
         (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)
@@ -264,8 +268,10 @@
           (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
@@ -1,6 +1,6 @@
 (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")))
@@ -17,11 +17,15 @@
             ((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)))