this repo has no description
1#!/usr/bin/env bash
2# triage-syscalls.sh — Automated syscall triage for Darling + Nix
3#
4# This script runs various Nix operations inside a Darling prefix and
5# captures "Unimplemented syscall" messages, ENOSYS errors, and other
6# indicators of missing kernel functionality. The output is a table
7# suitable for pasting into plan/syscall-triage.md.
8#
9# Usage:
10# ./scripts/triage-syscalls.sh [OPTIONS]
11#
12# Options:
13# --prefix <path> Darling prefix (default: ~/.darling or $DPREFIX)
14# --output <file> Write results to file (default: stdout)
15# --strace Also run strace on darlingserver (requires root)
16# --xtrace Enable DARLING_XTRACE for detailed Darwin tracing
17# --operations <list> Comma-separated list of operations to test
18# (default: all). Available: version,eval,store,
19# touch,mv,curl,install,build,channel
20# --timeout <secs> Timeout per operation (default: 60)
21# --help Show this help
22#
23# Output:
24# A Markdown-formatted table of discovered syscalls, with columns:
25# Syscall # | Caller | Operation | Message | Count
26#
27# Prerequisites:
28# - Darling must be installed and `darling shell echo ok` must work
29# - For Nix-related tests, Nix must be installed in the prefix
30# (run scripts/install-nix-in-darling.sh first)
31#
32# See: plan/03-phase1-syscalls.md (Task 1.7)
33# plan/syscall-triage.md
34
35set -euo pipefail
36
37# ── Defaults ────────────────────────────────────────────────────────────────
38
39DARLING_PREFIX="${DPREFIX:-$HOME/.darling}"
40OUTPUT_FILE=""
41USE_STRACE=0
42USE_XTRACE=0
43OPERATIONS="version,eval,store,touch,mv,curl,build"
44TIMEOUT=60
45SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
46REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
47
48# Temp directory for logs
49TRIAGE_TMP=""
50
51# Known syscall number → name mapping (macOS/XNU BSD syscalls)
52# Source: src/external/xnu/bsd/kern/syscalls.master
53# This covers the most commonly-seen unimplemented syscalls.
54declare -A SYSCALL_NAMES=(
55 [1]="exit"
56 [2]="fork"
57 [3]="read"
58 [4]="write"
59 [5]="open"
60 [6]="close"
61 [7]="wait4"
62 [9]="link"
63 [10]="unlink"
64 [12]="chdir"
65 [15]="chmod"
66 [16]="chown"
67 [20]="getpid"
68 [23]="setuid"
69 [24]="getuid"
70 [25]="geteuid"
71 [27]="recvmsg"
72 [28]="sendmsg"
73 [29]="recvfrom"
74 [30]="accept"
75 [33]="access"
76 [36]="sync"
77 [37]="kill"
78 [39]="getppid"
79 [41]="dup"
80 [42]="pipe"
81 [43]="getegid"
82 [46]="sigaction"
83 [47]="getgid"
84 [48]="sigprocmask"
85 [49]="getlogin"
86 [50]="setlogin"
87 [51]="acct"
88 [53]="sigaltstack"
89 [54]="ioctl"
90 [56]="revoke"
91 [57]="symlink"
92 [58]="readlink"
93 [59]="execve"
94 [60]="umask"
95 [61]="chroot"
96 [65]="msync"
97 [66]="vfork"
98 [73]="munmap"
99 [74]="mprotect"
100 [75]="madvise"
101 [78]="mincore"
102 [79]="getgroups"
103 [80]="setgroups"
104 [82]="setpgid"
105 [83]="setitimer"
106 [85]="swapon"
107 [86]="getitimer"
108 [89]="getdtablesize"
109 [90]="dup2"
110 [92]="fcntl"
111 [93]="select"
112 [95]="fsync"
113 [96]="setpriority"
114 [97]="socket"
115 [98]="connect"
116 [100]="getpriority"
117 [104]="bind"
118 [105]="setsockopt"
119 [106]="listen"
120 [111]="sigsuspend"
121 [116]="gettimeofday"
122 [117]="getrusage"
123 [118]="getsockopt"
124 [120]="readv"
125 [121]="writev"
126 [122]="settimeofday"
127 [123]="fchown"
128 [124]="fchmod"
129 [128]="rename"
130 [131]="flock"
131 [132]="mkfifo"
132 [133]="sendto"
133 [134]="shutdown"
134 [135]="socketpair"
135 [136]="mkdir"
136 [137]="rmdir"
137 [138]="utimes"
138 [139]="futimes"
139 [140]="adjtime"
140 [142]="gethostuuid"
141 [147]="setsid"
142 [151]="getpgid"
143 [152]="setprivexec"
144 [153]="pread"
145 [154]="pwrite"
146 [157]="statfs"
147 [158]="fstatfs"
148 [159]="unmount"
149 [165]="mount"
150 [167]="csops"
151 [169]="csops_audittoken"
152 [170]="fdatasync"
153 [173]="waitid"
154 [180]="kdebug_trace64"
155 [181]="kdebug_trace"
156 [182]="kdebug_typefilter"
157 [183]="setgid"
158 [184]="setegid"
159 [185]="seteuid"
160 [187]="stat"
161 [188]="fstat"
162 [189]="lstat"
163 [190]="pathconf"
164 [191]="fpathconf"
165 [194]="getrlimit"
166 [195]="setrlimit"
167 [196]="getdirentries"
168 [197]="mmap"
169 [199]="lseek"
170 [200]="truncate"
171 [201]="ftruncate"
172 [202]="sysctl"
173 [203]="mlock"
174 [204]="munlock"
175 [205]="undelete"
176 [216]="mkcomplex"
177 [220]="getattrlist"
178 [221]="setattrlist"
179 [222]="getdirentriesattr"
180 [223]="exchangedata"
181 [225]="searchfs"
182 [226]="delete"
183 [227]="copyfile"
184 [228]="fgetattrlist"
185 [229]="fsetattrlist"
186 [230]="poll"
187 [233]="getxattr"
188 [234]="fgetxattr"
189 [235]="setxattr"
190 [236]="fsetxattr"
191 [237]="removexattr"
192 [238]="fremovexattr"
193 [239]="listxattr"
194 [240]="flistxattr"
195 [241]="fsctl"
196 [242]="initgroups"
197 [243]="posix_spawn"
198 [244]="ffsctl"
199 [247]="nfsclnt"
200 [248]="fhopen"
201 [250]="minherit"
202 [266]="shm_open"
203 [267]="shm_unlink"
204 [268]="sem_open"
205 [269]="sem_close"
206 [270]="sem_unlink"
207 [271]="sem_wait"
208 [272]="sem_trywait"
209 [273]="sem_post"
210 [274]="sysctlbyname"
211 [277]="open_extended"
212 [278]="umask_extended"
213 [279]="stat_extended"
214 [280]="lstat_extended"
215 [281]="fstat_extended"
216 [282]="chmod_extended"
217 [283]="fchmod_extended"
218 [284]="access_extended"
219 [285]="settid"
220 [286]="gettid"
221 [288]="kqueue"
222 [289]="kevent"
223 [296]="mlockall"
224 [297]="munlockall"
225 [301]="issetugid"
226 [302]="__pthread_kill"
227 [303]="__pthread_sigmask"
228 [305]="__disable_threadsignal"
229 [310]="__semwait_signal"
230 [311]="proc_info"
231 [322]="getsid"
232 [324]="pread_nocancel"
233 [325]="pwrite_nocancel"
234 [327]="aio_suspend"
235 [336]="proc_rlimit_control"
236 [338]="iopolicysys"
237 [339]="process_policy"
238 [340]="mlockall"
239 [341]="munlockall"
240 [343]="issetugid"
241 [344]="__pthread_chdir"
242 [345]="__pthread_fchdir"
243 [346]="audit"
244 [347]="auditon"
245 [350]="getaudit_addr"
246 [351]="setaudit_addr"
247 [357]="getentropy"
248 [360]="getattrlistbulk"
249 [361]="clonefileat"
250 [362]="openat"
251 [363]="openat_nocancel"
252 [364]="renameat"
253 [366]="faccessat"
254 [367]="fchmodat"
255 [368]="fchownat"
256 [369]="fstatat"
257 [370]="fstatat64"
258 [371]="linkat"
259 [372]="unlinkat"
260 [373]="readlinkat"
261 [374]="symlinkat"
262 [375]="mkdirat"
263 [376]="getattrlistat"
264 [377]="proc_trace_log"
265 [378]="bsdthread_ctl"
266 [380]="openbyid_np"
267 [381]="recvmsg_x"
268 [382]="sendmsg_x"
269 [384]="guarded_open_np"
270 [385]="guarded_close_np"
271 [386]="guarded_kqueue_np"
272 [387]="change_fdguard_np"
273 [388]="usrctl"
274 [389]="proc_rlimit_control"
275 [394]="coalition"
276 [395]="coalition_info"
277 [396]="necp_match_policy"
278 [397]="getattrlistbulk"
279 [398]="clonefileat"
280 [399]="openat"
281 [400]="openat_nocancel"
282 [401]="renameat"
283 [403]="faccessat"
284 [404]="fchmodat"
285 [405]="fchownat"
286 [406]="fstatat"
287 [407]="fstatat64"
288 [408]="linkat"
289 [409]="unlinkat"
290 [410]="readlinkat"
291 [411]="symlinkat"
292 [412]="mkdirat"
293 [413]="getattrlistat"
294 [414]="proc_trace_log"
295 [415]="bsdthread_ctl"
296 [417]="openbyid_np"
297 [418]="recvmsg_x"
298 [419]="sendmsg_x"
299 [420]="thread_selfusage"
300 [421]="csrctl"
301 [422]="guarded_open_dprotected_np"
302 [423]="guarded_write_np"
303 [424]="guarded_pwrite_np"
304 [425]="guarded_writev_np"
305 [426]="renameatx_np"
306 [427]="mremap_encrypted"
307 [428]="netagent_trigger"
308 [429]="stack_snapshot_with_config"
309 [430]="microstackshot"
310 [431]="grab_pgo_data"
311 [432]="persona"
312 [438]="fs_snapshot"
313 [441]="terminate_with_payload"
314 [442]="abort_with_payload"
315 [443]="necp_session_open"
316 [444]="necp_session_action"
317 [449]="fclonefileat"
318 [450]="fs_snapshot"
319 [452]="terminate_with_payload"
320 [453]="abort_with_payload"
321 [462]="clonefile"
322 [463]="close_nocancel"
323 [464]="accept_nocancel"
324 [468]="msync_nocancel"
325 [469]="fcntl_nocancel"
326 [470]="select_nocancel"
327 [471]="fsgetpath"
328 [473]="pselect"
329 [474]="pselect_nocancel"
330 [475]="read_nocancel"
331 [476]="write_nocancel"
332 [477]="open_dprotected_np"
333 [480]="kevent_qos"
334 [481]="kevent_id"
335 [482]="__mac_execve"
336 [483]="__mac_syscall"
337 [484]="__mac_get_file"
338 [485]="__mac_set_file"
339 [486]="__mac_get_link"
340 [487]="__mac_set_link"
341 [488]="renameatx_np"
342 [489]="setxattr"
343 [500]="getentropy"
344 [515]="ulock_wait"
345 [516]="ulock_wake"
346 [517]="fclonefileat"
347 [518]="fs_snapshot"
348 [519]="terminate_with_payload"
349 [520]="abort_with_payload"
350)
351
352# ── Colors ──────────────────────────────────────────────────────────────────
353
354if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
355 RED='\033[0;31m'
356 GREEN='\033[0;32m'
357 YELLOW='\033[0;33m'
358 BLUE='\033[0;34m'
359 BOLD='\033[1m'
360 DIM='\033[2m'
361 RESET='\033[0m'
362else
363 RED='' GREEN='' YELLOW='' BLUE='' BOLD='' DIM='' RESET=''
364fi
365
366# ── Helpers ─────────────────────────────────────────────────────────────────
367
368log() { echo -e "${GREEN}[triage]${RESET} $*" >&2; }
369warn() { echo -e "${YELLOW}[triage] WARNING:${RESET} $*" >&2; }
370err() { echo -e "${RED}[triage] ERROR:${RESET} $*" >&2; }
371debug() { echo -e "${DIM}[triage] $*${RESET}" >&2; }
372fatal() { err "$@"; exit 1; }
373
374usage() {
375 sed -n '/^# Usage:/,/^# See:/p' "$0" | sed 's/^# \?//'
376 exit 0
377}
378
379# Run a command inside the Darling prefix, capturing all output
380dsh() {
381 local logfile="$1"
382 shift
383 timeout "$TIMEOUT" darling shell "$@" >"$logfile" 2>&1 || true
384}
385
386# Run a command inside the Darling prefix with bash -lc
387dsh_bash() {
388 local logfile="$1"
389 shift
390 timeout "$TIMEOUT" darling shell bash -lc "$*" >"$logfile" 2>&1 || true
391}
392
393# Run with DARLING_XTRACE if requested
394dsh_traced() {
395 local logfile="$1"
396 shift
397 local env_args=()
398 if [ "$USE_XTRACE" -eq 1 ]; then
399 env_args=(env DARLING_XTRACE=1)
400 fi
401 timeout "$TIMEOUT" "${env_args[@]}" darling shell "$@" >"$logfile" 2>&1 || true
402}
403
404dsh_bash_traced() {
405 local logfile="$1"
406 shift
407 local env_args=()
408 if [ "$USE_XTRACE" -eq 1 ]; then
409 env_args=(env DARLING_XTRACE=1)
410 fi
411 timeout "$TIMEOUT" "${env_args[@]}" darling shell bash -lc "$*" >"$logfile" 2>&1 || true
412}
413
414# Resolve a syscall number to its name
415syscall_name() {
416 local num="$1"
417 if [[ -v "SYSCALL_NAMES[$num]" ]]; then
418 echo "${SYSCALL_NAMES[$num]}"
419 else
420 echo "unknown_$num"
421 fi
422}
423
424# ── Argument Parsing ────────────────────────────────────────────────────────
425
426while [ $# -gt 0 ]; do
427 case "$1" in
428 --prefix)
429 DARLING_PREFIX="$2"
430 shift 2
431 ;;
432 --output)
433 OUTPUT_FILE="$2"
434 shift 2
435 ;;
436 --strace)
437 USE_STRACE=1
438 shift
439 ;;
440 --xtrace)
441 USE_XTRACE=1
442 shift
443 ;;
444 --operations)
445 OPERATIONS="$2"
446 shift 2
447 ;;
448 --timeout)
449 TIMEOUT="$2"
450 shift 2
451 ;;
452 --help|-h)
453 usage
454 ;;
455 *)
456 fatal "Unknown option: $1 (try --help)"
457 ;;
458 esac
459done
460
461# ── Setup ───────────────────────────────────────────────────────────────────
462
463TRIAGE_TMP=$(mktemp -d "${TMPDIR:-/tmp}/darling-triage.XXXXXX")
464cleanup() {
465 if [ -n "$TRIAGE_TMP" ] && [ -d "$TRIAGE_TMP" ]; then
466 rm -rf "$TRIAGE_TMP"
467 fi
468}
469trap cleanup EXIT
470
471log "${BOLD}Darling Syscall Triage${RESET}"
472log "Prefix: $DARLING_PREFIX"
473log "Operations: $OPERATIONS"
474log "Timeout: ${TIMEOUT}s per operation"
475log "XTrace: $([ "$USE_XTRACE" -eq 1 ] && echo "enabled" || echo "disabled")"
476log "Strace: $([ "$USE_STRACE" -eq 1 ] && echo "enabled" || echo "disabled")"
477log "Temp dir: $TRIAGE_TMP"
478echo "" >&2
479
480# ── Preflight ───────────────────────────────────────────────────────────────
481
482if ! command -v darling &>/dev/null; then
483 fatal "darling not found in PATH. Build it first with: nix build .#darling"
484fi
485
486log "Checking darling shell..."
487if ! timeout 30 darling shell echo "ok" &>/dev/null; then
488 fatal "darling shell is not functional. Try: darling shell echo ok"
489fi
490log " darling shell: ${GREEN}OK${RESET}"
491
492# Check if Nix is available
493HAS_NIX=0
494if timeout 15 darling shell bash -lc 'command -v nix' &>/dev/null; then
495 HAS_NIX=1
496 log " Nix in prefix: ${GREEN}found${RESET}"
497else
498 warn "Nix not found in prefix. Nix-specific operations will be skipped."
499 warn "Run scripts/install-nix-in-darling.sh first for full triage."
500fi
501
502echo "" >&2
503
504# ── Strace setup ────────────────────────────────────────────────────────────
505
506STRACE_PID=""
507STRACE_LOG=""
508
509start_strace() {
510 if [ "$USE_STRACE" -eq 0 ]; then
511 return
512 fi
513
514 local server_pid
515 server_pid=$(pidof darlingserver 2>/dev/null || true)
516 if [ -z "$server_pid" ]; then
517 warn "darlingserver not running; cannot attach strace"
518 return
519 fi
520
521 STRACE_LOG="$TRIAGE_TMP/strace.log"
522 strace -f -p "$server_pid" -e trace=all -o "$STRACE_LOG" &
523 STRACE_PID=$!
524 sleep 1
525 debug "strace attached to darlingserver (pid=$server_pid)"
526}
527
528stop_strace() {
529 if [ -n "$STRACE_PID" ]; then
530 kill "$STRACE_PID" 2>/dev/null || true
531 wait "$STRACE_PID" 2>/dev/null || true
532 STRACE_PID=""
533 fi
534}
535
536# ── Operations ──────────────────────────────────────────────────────────────
537
538# Each operation function takes a log directory and produces a log file
539# named <operation>.log inside that directory.
540
541op_version() {
542 local logdir="$1"
543
544 log " Testing: ${BLUE}darling shell echo ok${RESET}"
545 dsh_traced "$logdir/echo.log" echo "hello from darling"
546
547 log " Testing: ${BLUE}sw_vers${RESET}"
548 dsh_traced "$logdir/sw_vers.log" sw_vers
549
550 log " Testing: ${BLUE}uname -a${RESET}"
551 dsh_traced "$logdir/uname.log" uname -a
552
553 if [ "$HAS_NIX" -eq 1 ]; then
554 log " Testing: ${BLUE}nix --version${RESET}"
555 dsh_bash_traced "$logdir/nix_version.log" "nix --version"
556
557 log " Testing: ${BLUE}nix-env --version${RESET}"
558 dsh_bash_traced "$logdir/nix_env_version.log" "nix-env --version"
559
560 log " Testing: ${BLUE}nix-store --version${RESET}"
561 dsh_bash_traced "$logdir/nix_store_version.log" "nix-store --version"
562 fi
563}
564
565op_eval() {
566 local logdir="$1"
567
568 if [ "$HAS_NIX" -eq 0 ]; then
569 warn " Skipping eval tests (Nix not installed)"
570 return
571 fi
572
573 log " Testing: ${BLUE}nix-instantiate --eval -E '1 + 1'${RESET}"
574 dsh_bash_traced "$logdir/eval_simple.log" "nix-instantiate --eval -E '1 + 1'"
575
576 log " Testing: ${BLUE}nix eval --expr '1 + 1'${RESET}"
577 dsh_bash_traced "$logdir/eval_nix3.log" "nix eval --expr '1 + 1'"
578
579 log " Testing: ${BLUE}builtins.currentSystem${RESET}"
580 dsh_bash_traced "$logdir/eval_system.log" "nix eval --expr 'builtins.currentSystem'"
581
582 log " Testing: ${BLUE}nix eval (complex expression)${RESET}"
583 dsh_bash_traced "$logdir/eval_complex.log" \
584 "nix eval --expr 'let f = x: if x <= 1 then 1 else x * f (x - 1); in f 10'"
585
586 log " Testing: ${BLUE}nix eval (import)${RESET}"
587 dsh_bash_traced "$logdir/eval_import.log" \
588 "nix eval --expr 'builtins.length (builtins.attrNames builtins)'"
589}
590
591op_store() {
592 local logdir="$1"
593
594 if [ "$HAS_NIX" -eq 0 ]; then
595 warn " Skipping store tests (Nix not installed)"
596 return
597 fi
598
599 log " Testing: ${BLUE}nix-store --verify${RESET}"
600 dsh_bash_traced "$logdir/store_verify.log" "nix-store --verify --no-build 2>&1 || nix-store --verify"
601
602 log " Testing: ${BLUE}nix-store --dump-db${RESET}"
603 dsh_bash_traced "$logdir/store_dump_db.log" "nix-store --dump-db | head -50"
604
605 log " Testing: ${BLUE}nix-store --gc --print-dead${RESET}"
606 dsh_bash_traced "$logdir/store_gc_dead.log" "nix-store --gc --print-dead 2>&1 | head -20"
607}
608
609op_touch() {
610 local logdir="$1"
611
612 log " Testing: ${BLUE}touch /tmp/triage_test${RESET} (built-in)"
613 dsh_traced "$logdir/touch_builtin.log" /usr/bin/touch /tmp/triage_test_builtin
614
615 log " Testing: ${BLUE}touch -t (timestamp)${RESET}"
616 dsh_traced "$logdir/touch_timestamp.log" /usr/bin/touch -t 202301011200 /tmp/triage_test_ts
617
618 if [ "$HAS_NIX" -eq 1 ]; then
619 log " Testing: ${BLUE}Nix coreutils touch${RESET}"
620 dsh_bash_traced "$logdir/touch_nix.log" \
621 "if type -P touch >/dev/null; then touch /tmp/triage_test_nix; else echo 'touch not on PATH'; fi"
622 fi
623
624 # Clean up
625 dsh "$TRIAGE_TMP/touch_cleanup.log" rm -f /tmp/triage_test_builtin /tmp/triage_test_ts /tmp/triage_test_nix
626}
627
628op_mv() {
629 local logdir="$1"
630
631 log " Testing: ${BLUE}mv (built-in)${RESET}"
632 dsh_traced "$logdir/mv_builtin_setup.log" /usr/bin/touch /tmp/triage_mv_src
633 dsh_traced "$logdir/mv_builtin.log" /bin/mv /tmp/triage_mv_src /tmp/triage_mv_dst
634
635 if [ "$HAS_NIX" -eq 1 ]; then
636 log " Testing: ${BLUE}Nix coreutils mv${RESET}"
637 dsh_bash_traced "$logdir/mv_nix_setup.log" "touch /tmp/triage_mv_nix_src"
638 dsh_bash_traced "$logdir/mv_nix.log" \
639 "if type -P mv >/dev/null; then mv /tmp/triage_mv_nix_src /tmp/triage_mv_nix_dst; else echo 'mv not on PATH'; fi"
640 fi
641
642 # Clean up
643 dsh "$TRIAGE_TMP/mv_cleanup.log" rm -f /tmp/triage_mv_src /tmp/triage_mv_dst /tmp/triage_mv_nix_src /tmp/triage_mv_nix_dst
644}
645
646op_curl() {
647 local logdir="$1"
648
649 log " Testing: ${BLUE}curl --version${RESET}"
650 dsh_traced "$logdir/curl_version.log" curl --version
651
652 log " Testing: ${BLUE}curl https://cache.nixos.org/nix-cache-info${RESET}"
653 dsh_traced "$logdir/curl_https.log" curl -sfI https://cache.nixos.org/nix-cache-info
654
655 log " Testing: ${BLUE}curl http (plain)${RESET}"
656 dsh_traced "$logdir/curl_http.log" curl -sfI http://example.com/
657}
658
659op_build() {
660 local logdir="$1"
661
662 if [ "$HAS_NIX" -eq 0 ]; then
663 warn " Skipping build tests (Nix not installed)"
664 return
665 fi
666
667 log " Testing: ${BLUE}trivial derivation build${RESET}"
668 dsh_bash_traced "$logdir/build_trivial.log" \
669 "nix-build --no-out-link --expr 'derivation { name = \"triage-test\"; builder = \"/bin/bash\"; args = [\"-c\" \"echo ok > \\\$out\"]; system = \"x86_64-darwin\"; }' 2>&1"
670
671 log " Testing: ${BLUE}sandbox-exec passthrough${RESET}"
672 dsh_traced "$logdir/sandbox_exec.log" \
673 /usr/bin/sandbox-exec -f /dev/null -D _GLOBAL_TMP_DIR=/tmp /bin/echo "sandbox-exec passthrough ok"
674}
675
676op_channel() {
677 local logdir="$1"
678
679 if [ "$HAS_NIX" -eq 0 ]; then
680 warn " Skipping channel tests (Nix not installed)"
681 return
682 fi
683
684 log " Testing: ${BLUE}nix-channel --list${RESET}"
685 dsh_bash_traced "$logdir/channel_list.log" "nix-channel --list"
686}
687
688op_install() {
689 local logdir="$1"
690
691 if [ "$HAS_NIX" -eq 0 ]; then
692 warn " Skipping install tests (Nix not installed)"
693 return
694 fi
695
696 log " Testing: ${BLUE}nix-env --query --installed${RESET}"
697 dsh_bash_traced "$logdir/env_query.log" "nix-env --query --installed 2>&1 || true"
698}
699
700# ── Run Operations ──────────────────────────────────────────────────────────
701
702IFS=',' read -ra OPS <<< "$OPERATIONS"
703
704LOGDIR="$TRIAGE_TMP/logs"
705mkdir -p "$LOGDIR"
706
707start_strace
708
709for op in "${OPS[@]}"; do
710 op=$(echo "$op" | tr -d '[:space:]')
711 opdir="$LOGDIR/$op"
712 mkdir -p "$opdir"
713
714 log "${BOLD}Running operation: $op${RESET}"
715
716 case "$op" in
717 version) op_version "$opdir" ;;
718 eval) op_eval "$opdir" ;;
719 store) op_store "$opdir" ;;
720 touch) op_touch "$opdir" ;;
721 mv) op_mv "$opdir" ;;
722 curl) op_curl "$opdir" ;;
723 build) op_build "$opdir" ;;
724 channel) op_channel "$opdir" ;;
725 install) op_install "$opdir" ;;
726 *)
727 warn "Unknown operation: $op (skipping)"
728 ;;
729 esac
730
731 echo "" >&2
732done
733
734stop_strace
735
736# ── Analysis ────────────────────────────────────────────────────────────────
737
738log "${BOLD}Analyzing results...${RESET}"
739
740# Patterns to search for in the logs
741PATTERNS=(
742 'Unimplemented syscall'
743 'unimplemented syscall'
744 'ENOSYS'
745 'not.implemented'
746 'STUB'
747 'Function not implemented'
748 'Segmentation fault'
749 'Bus error'
750 'Abort trap'
751 'Illegal instruction'
752 'Bad system call'
753 'Bad file descriptor'
754)
755
756PATTERN_REGEX=$(IFS='|'; echo "${PATTERNS[*]}")
757
758# Collect all findings into a single file
759FINDINGS="$TRIAGE_TMP/findings.txt"
760: > "$FINDINGS"
761
762# Process each log file
763while IFS= read -r -d '' logfile; do
764 relpath="${logfile#"$LOGDIR"/}"
765 operation="${relpath%%/*}"
766 testname="${relpath#*/}"
767 testname="${testname%.log}"
768
769 if grep -qiE "$PATTERN_REGEX" "$logfile" 2>/dev/null; then
770 while IFS= read -r line; do
771 echo "$operation|$testname|$line" >> "$FINDINGS"
772 done < <(grep -iE "$PATTERN_REGEX" "$logfile" 2>/dev/null || true)
773 fi
774done < <(find "$LOGDIR" -name '*.log' -print0 2>/dev/null)
775
776# Also check strace log if available
777if [ -n "$STRACE_LOG" ] && [ -f "$STRACE_LOG" ]; then
778 while IFS= read -r line; do
779 echo "strace|darlingserver|$line" >> "$FINDINGS"
780 done < <(grep -iE 'ENOSYS|ENOTSUP' "$STRACE_LOG" 2>/dev/null | head -100 || true)
781fi
782
783# ── Extract syscall numbers ─────────────────────────────────────────────────
784
785SYSCALLS_FILE="$TRIAGE_TMP/syscalls.txt"
786: > "$SYSCALLS_FILE"
787
788# Pattern: "Unimplemented syscall (NNN)" or "Unimplemented syscall NNN"
789while IFS= read -r line; do
790 if [[ "$line" =~ [Uu]nimplemented[[:space:]]syscall[[:space:]]*\(?([0-9]+)\)? ]]; then
791 num="${BASH_REMATCH[1]}"
792 name=$(syscall_name "$num")
793 op="${line%%|*}"
794 echo "$num|$name|$op|$line" >> "$SYSCALLS_FILE"
795 fi
796done < "$FINDINGS"
797
798# ── Other issues (non-syscall) ──────────────────────────────────────────────
799
800OTHER_FILE="$TRIAGE_TMP/other_issues.txt"
801: > "$OTHER_FILE"
802
803while IFS= read -r line; do
804 if [[ ! "$line" =~ [Uu]nimplemented[[:space:]]syscall ]]; then
805 echo "$line" >> "$OTHER_FILE"
806 fi
807done < "$FINDINGS"
808
809# ── Generate Report ─────────────────────────────────────────────────────────
810
811generate_report() {
812 local timestamp
813 timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
814
815 cat <<EOF
816# Syscall Triage Report
817
818Generated: $timestamp
819Darling prefix: $DARLING_PREFIX
820Operations tested: $OPERATIONS
821XTrace: $([ "$USE_XTRACE" -eq 1 ] && echo "enabled" || echo "disabled")
822Strace: $([ "$USE_STRACE" -eq 1 ] && echo "enabled" || echo "disabled")
823Nix available: $([ "$HAS_NIX" -eq 1 ] && echo "yes" || echo "no")
824
825---
826
827## Unimplemented Syscalls
828
829EOF
830
831 if [ -s "$SYSCALLS_FILE" ]; then
832 echo "| Syscall # | Name | Operation | Count | Sample Message |"
833 echo "|-----------|------|-----------|-------|----------------|"
834
835 # Deduplicate and count occurrences
836 sort "$SYSCALLS_FILE" | while IFS='|' read -r num name op full_line; do
837 echo "$num|$name|$op"
838 done | sort | uniq -c | sort -rn | while read -r count entry; do
839 IFS='|' read -r num name op <<< "$entry"
840 # Get the first sample message for this syscall
841 sample=$(grep "^${num}|" "$SYSCALLS_FILE" | head -1 | cut -d'|' -f4-)
842 # Truncate long messages
843 if [ ${#sample} -gt 80 ]; then
844 sample="${sample:0:77}..."
845 fi
846 # Escape pipe chars in the message for markdown
847 sample="${sample//|/\\|}"
848 echo "| $num | \`$name\` | $op | $count | $sample |"
849 done
850
851 echo ""
852 else
853 echo "*No unimplemented syscalls detected.* :tada:"
854 echo ""
855 echo "This either means:"
856 echo "1. All tested operations use fully-implemented syscalls, or"
857 echo "2. The operations didn't exercise enough codepaths (try --xtrace or more operations)"
858 echo ""
859 fi
860
861 cat <<EOF
862## Other Issues
863
864EOF
865
866 if [ -s "$OTHER_FILE" ]; then
867 echo "| Category | Operation | Test | Message |"
868 echo "|----------|-----------|------|---------|"
869
870 while IFS='|' read -r op test msg; do
871 # Categorize the issue
872 local category="Unknown"
873 case "$msg" in
874 *[Ss]egmentation*fault*|*SIGSEGV*) category="**SEGFAULT**" ;;
875 *[Bb]us*error*|*SIGBUS*) category="**BUS ERROR**" ;;
876 *[Aa]bort*|*SIGABRT*) category="**ABORT**" ;;
877 *[Ii]llegal*instruction*|*SIGILL*) category="**SIGILL**" ;;
878 *ENOSYS*) category="ENOSYS" ;;
879 *STUB*|*[Ss]tub*) category="Stub" ;;
880 *[Nn]ot.implemented*) category="Not impl" ;;
881 *[Bb]ad*file*descriptor*) category="Bad FD" ;;
882 *) category="Other" ;;
883 esac
884
885 # Truncate long messages
886 local short_msg="$msg"
887 if [ ${#short_msg} -gt 80 ]; then
888 short_msg="${short_msg:0:77}..."
889 fi
890 short_msg="${short_msg//|/\\|}"
891
892 echo "| $category | $op | $test | $short_msg |"
893 done < "$OTHER_FILE"
894
895 echo ""
896 else
897 echo "*No other issues detected.*"
898 echo ""
899 fi
900
901 cat <<EOF
902## Summary
903
904- **Total log files analyzed**: $(find "$LOGDIR" -name '*.log' 2>/dev/null | wc -l)
905- **Files with findings**: $([ -s "$FINDINGS" ] && wc -l < "$FINDINGS" || echo 0) lines
906- **Unique unimplemented syscalls**: $([ -s "$SYSCALLS_FILE" ] && cut -d'|' -f1 "$SYSCALLS_FILE" | sort -u | wc -l || echo 0)
907- **Other issues**: $([ -s "$OTHER_FILE" ] && wc -l < "$OTHER_FILE" || echo 0)
908
909## Recommended Actions
910
911EOF
912
913 if [ -s "$SYSCALLS_FILE" ]; then
914 echo "### Must Fix (causes crashes / blocks Nix operations)"
915 echo ""
916
917 # List unique syscalls sorted by number
918 local seen_nums=""
919 while IFS='|' read -r num name op _; do
920 if [[ ! " $seen_nums " =~ " $num " ]]; then
921 seen_nums="$seen_nums $num"
922 echo "- **Syscall $num** (\`$name\`): Add to syscall triage table in \`plan/syscall-triage.md\`"
923 fi
924 done < <(sort -t'|' -k1,1n "$SYSCALLS_FILE" | sort -t'|' -k1,1n -u)
925
926 echo ""
927 fi
928
929 cat <<EOF
930### Next Steps
931
9321. Add any new syscalls to \`plan/syscall-triage.md\`
9332. For each "Must fix" syscall, determine the best implementation strategy:
934 - Full translation to Linux equivalent
935 - Stub returning ENOTSUP (if caller handles gracefully)
936 - Stub returning 0 (if call is informational/optional)
9373. Re-run this triage after implementing fixes to verify they work
9384. Run with \`--xtrace\` for more detailed tracing if needed
939
940## Raw Logs
941
942Log files are saved in: \`$TRIAGE_TMP/logs/\`
943
944EOF
945
946 # List all log files and whether they had issues
947 echo "| Log File | Status | Size |"
948 echo "|----------|--------|------|"
949 while IFS= read -r -d '' logfile; do
950 local relpath="${logfile#"$LOGDIR"/}"
951 local size
952 size=$(wc -c < "$logfile")
953 local status="${GREEN}clean${RESET}"
954 if grep -qiE "$PATTERN_REGEX" "$logfile" 2>/dev/null; then
955 status="${RED}issues found${RESET}"
956 elif [ "$size" -eq 0 ]; then
957 status="${YELLOW}empty${RESET}"
958 fi
959 echo "| \`$relpath\` | $status | ${size}B |"
960 done < <(find "$LOGDIR" -name '*.log' -print0 2>/dev/null | sort -z)
961
962 echo ""
963 echo "---"
964 echo "*Generated by \`scripts/triage-syscalls.sh\` — see [plan/syscall-triage.md](../plan/syscall-triage.md)*"
965}
966
967# ── Output ──────────────────────────────────────────────────────────────────
968
969REPORT="$TRIAGE_TMP/report.md"
970generate_report > "$REPORT"
971
972if [ -n "$OUTPUT_FILE" ]; then
973 cp "$REPORT" "$OUTPUT_FILE"
974 log "Report saved to: ${BOLD}$OUTPUT_FILE${RESET}"
975else
976 echo "" >&2
977 log "${BOLD}═══ Triage Report ═══${RESET}"
978 echo "" >&2
979 cat "$REPORT"
980fi
981
982# Print a short summary to stderr regardless
983echo "" >&2
984UNIQUE_SYSCALLS=$([ -s "$SYSCALLS_FILE" ] && cut -d'|' -f1 "$SYSCALLS_FILE" | sort -u | wc -l || echo 0)
985OTHER_COUNT=$([ -s "$OTHER_FILE" ] && wc -l < "$OTHER_FILE" || echo 0)
986
987if [ "$UNIQUE_SYSCALLS" -eq 0 ] && [ "$OTHER_COUNT" -eq 0 ]; then
988 log "${GREEN}${BOLD}No issues found!${RESET} All tested operations passed cleanly."
989else
990 log "Found ${RED}${BOLD}$UNIQUE_SYSCALLS${RESET} unimplemented syscall(s) and ${YELLOW}${BOLD}$OTHER_COUNT${RESET} other issue(s)."
991fi
992log "Full logs: $TRIAGE_TMP/logs/"
993[ -n "$OUTPUT_FILE" ] && log "Report: $OUTPUT_FILE"