dsh-eval-guard 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ISSUES.md +239 -0
- package/README.md +48 -18
- package/assets/neural-cifar/workspace.zip +0 -0
- package/assets/neural-cpp/workspace.zip +0 -0
- package/assets/neural-fp32/workspace.zip +0 -0
- package/assets/neural-pendulum/SPEC.md +43 -35
- package/assets/neural-pendulum/TASK.md +139 -33
- package/assets/neural-pendulum/rl-feasibility/README.md +60 -0
- package/assets/neural-pendulum/rl-feasibility/RESULTS.md +370 -0
- package/assets/neural-pendulum/rl-feasibility/evaluate.py +107 -0
- package/assets/neural-pendulum/rl-feasibility/finalize_run.py +168 -0
- package/assets/neural-pendulum/rl-feasibility/models.py +124 -0
- package/assets/neural-pendulum/rl-feasibility/obs.py +205 -0
- package/assets/neural-pendulum/rl-feasibility/plot_selected.py +55 -0
- package/assets/neural-pendulum/rl-feasibility/reference_controller.py +112 -0
- package/assets/neural-pendulum/rl-feasibility/results/curves_selected.png +0 -0
- package/assets/neural-pendulum/rl-feasibility/results/reference_controller.json +417 -0
- package/assets/neural-pendulum/rl-feasibility/results/summary.json +1599 -0
- package/assets/neural-pendulum/rl-feasibility/results/table.md +39 -0
- package/assets/neural-pendulum/rl-feasibility/sim.py +324 -0
- package/assets/neural-pendulum/rl-feasibility/summarize.py +175 -0
- package/assets/neural-pendulum/rl-feasibility/task.py +342 -0
- package/assets/neural-pendulum/rl-feasibility/test_contract.py +188 -0
- package/assets/neural-pendulum/rl-feasibility/train_ppo.py +403 -0
- package/assets/neural-pendulum/rl-feasibility/train_sac.py +406 -0
- package/assets/neural-pendulum/sim.cpp +13 -2
- package/assets/neural-pendulum/sim_eval +0 -0
- package/assets/neural-pendulum/workspace.zip +0 -0
- package/eval/README.md +19 -9
- package/eval/bomberman.py +5 -3
- package/eval/commit0.py +10 -3
- package/eval/common.py +169 -5
- package/eval/ctf.py +4 -2
- package/eval/featurebench.py +12 -8
- package/eval/land.py +4 -2
- package/eval/legacy/bomberman.py +5 -2
- package/eval/legacy/ctf.py +5 -2
- package/eval/legacy/featurebench.py +55 -15
- package/eval/legacy/land.py +6 -3
- package/eval/legacy/swe_bench_pro.py +57 -16
- package/eval/legacy/tank.py +2 -2
- package/eval/legacy/warehouse.py +4 -4
- package/eval/neural-cpp.py +9 -6
- package/eval/neural-fp32.py +17 -5
- package/eval/swe-bench-pro.py +11 -7
- package/eval/tank.py +11 -2
- package/eval/warehouse.py +17 -1
- package/lib/client/index.js +56 -29
- package/lib/client/index.js.map +1 -1
- package/lib/index.js +883 -298
- package/native/dsh-eval-supervisor +0 -0
- package/native/supervisor.c +232 -36
- package/neural-cifar-eval/README.md +5 -0
- package/neural-cifar-eval/common.py +13 -0
- package/neural-cifar-eval/ops_eval.py +17 -5
- package/neural-cifar-eval/run_eval.py +42 -32
- package/neural-pendulum-eval/README.md +62 -0
- package/neural-pendulum-eval/ops_eval.py +603 -0
- package/neural-pendulum-eval/run_eval.py +218 -137
- package/neural-pendulum-eval/sim_eval +0 -0
- package/package.json +7 -11
- package/assets/neural-pendulum/hidden/test_infra.cpp +0 -631
- package/assets/neural-pendulum/include/nn/nn.hpp +0 -88
|
Binary file
|
package/native/supervisor.c
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Installs a seccomp user-notification filter before exec'ing bwrap and enforces
|
|
5
5
|
* a path policy on file-access and network syscalls. Normal syscalls are not
|
|
6
|
-
* logged
|
|
7
|
-
*
|
|
6
|
+
* logged. File-policy violations are audited and kill the process group;
|
|
7
|
+
* AF_INET/AF_INET6 connect/bind/sendto are audited and soft-denied with EPERM
|
|
8
|
+
* so local software checks (pip version probe, NSS fallback) can continue.
|
|
8
9
|
*
|
|
9
10
|
* Usage:
|
|
10
11
|
* dsh-eval-supervisor --policy <policy-file> --audit <audit-file> -- <program> [args...]
|
|
@@ -57,6 +58,17 @@
|
|
|
57
58
|
#ifndef SYS_open_by_handle_at
|
|
58
59
|
#define SYS_open_by_handle_at 304
|
|
59
60
|
#endif
|
|
61
|
+
#ifndef SYS_fchmodat2
|
|
62
|
+
#define SYS_fchmodat2 452
|
|
63
|
+
#endif
|
|
64
|
+
|
|
65
|
+
#if defined(__x86_64__)
|
|
66
|
+
#define SUPERVISOR_AUDIT_ARCH AUDIT_ARCH_X86_64
|
|
67
|
+
#elif defined(__aarch64__)
|
|
68
|
+
#define SUPERVISOR_AUDIT_ARCH AUDIT_ARCH_AARCH64
|
|
69
|
+
#else
|
|
70
|
+
#error "dsh-eval-supervisor: unsupported architecture"
|
|
71
|
+
#endif
|
|
60
72
|
|
|
61
73
|
#define MAX_RULES 512
|
|
62
74
|
#define MAX_PATH 4096
|
|
@@ -76,14 +88,33 @@ static int g_dev_count;
|
|
|
76
88
|
static Rule g_rules[MAX_RULES];
|
|
77
89
|
static int g_rule_count;
|
|
78
90
|
static int g_audit_fd = -1;
|
|
91
|
+
static int g_soft_deny; /* network-family denial: EPERM, do not kill the group */
|
|
79
92
|
static volatile sig_atomic_t g_stop;
|
|
93
|
+
/* The sandbox mounted a private tmpfs at /tmp (workspace under host /tmp). */
|
|
94
|
+
static int g_tmpfs_tmp;
|
|
95
|
+
static char g_requested_cwd[MAX_PATH];
|
|
80
96
|
/* bwrap itself needs AF_UNIX and AF_NETLINK sockets during setup. The target
|
|
81
97
|
* command is allowed no sockets at all once bwrap has exec'd it. */
|
|
82
98
|
static int g_command_started;
|
|
83
99
|
static pid_t g_current_pid;
|
|
84
100
|
static int g_tmpbind;
|
|
85
101
|
|
|
86
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Whether the notifying process is bubblewrap itself (setup phase).
|
|
104
|
+
*
|
|
105
|
+
* The bwrap command may be a package-manager shim that re-execs several times,
|
|
106
|
+
* so identification is by executable name (as upstream did). The setup-only
|
|
107
|
+
* exemptions it enables are restricted to paths that do not exist after
|
|
108
|
+
* pivot_root, and the command-started gate closes them for the target.
|
|
109
|
+
*/
|
|
110
|
+
static int is_bwrap_name(const char *name) {
|
|
111
|
+
if (name == NULL || name[0] == '\0') return 0;
|
|
112
|
+
/* Debian/Fedora ship /usr/bin/bwrap; the project/package is also commonly
|
|
113
|
+
* called bubblewrap. Accept both spellings so bwrap's own setup writes
|
|
114
|
+
* (uid_map/gid_map under /proc) are recognized regardless of binary name. */
|
|
115
|
+
return strstr(name, "bwrap") != NULL || strstr(name, "bubblewrap") != NULL;
|
|
116
|
+
}
|
|
117
|
+
|
|
87
118
|
static int is_bwrap(pid_t pid) {
|
|
88
119
|
char proc[64];
|
|
89
120
|
char exe[MAX_PATH];
|
|
@@ -93,7 +124,7 @@ static int is_bwrap(pid_t pid) {
|
|
|
93
124
|
exe[n] = '\0';
|
|
94
125
|
const char *base = strrchr(exe, '/');
|
|
95
126
|
base = base == NULL ? exe : base + 1;
|
|
96
|
-
return
|
|
127
|
+
return is_bwrap_name(base);
|
|
97
128
|
}
|
|
98
129
|
|
|
99
130
|
static void on_signal(int sig) {
|
|
@@ -302,6 +333,9 @@ static int rules_override_read(const char *path, int initial) {
|
|
|
302
333
|
|
|
303
334
|
static int path_allowed(const char *path, int write_access) {
|
|
304
335
|
if (g_workspace[0] != '\0' && starts_with_seg(path, g_workspace)) return 1;
|
|
336
|
+
/* A private tmpfs at /tmp is ephemeral by construction: it never exposes
|
|
337
|
+
* host files, so both read and write are safe. */
|
|
338
|
+
if (g_tmpfs_tmp && starts_with_seg(path, "/tmp")) return 1;
|
|
305
339
|
/* bubblewrap builds its new root under /tmp and /newroot before pivot_root.
|
|
306
340
|
* Those transient paths exist only during setup and are removed on exit. */
|
|
307
341
|
if ((!g_command_started || (g_current_pid > 0 && is_bwrap(g_current_pid))) &&
|
|
@@ -393,6 +427,17 @@ __attribute__((unused)) static int should_notify(long nr) {
|
|
|
393
427
|
case SYS_faccessat2:
|
|
394
428
|
case SYS_readlink:
|
|
395
429
|
case SYS_readlinkat:
|
|
430
|
+
case SYS_symlink:
|
|
431
|
+
case SYS_symlinkat:
|
|
432
|
+
case SYS_link:
|
|
433
|
+
case SYS_linkat:
|
|
434
|
+
case SYS_mknod:
|
|
435
|
+
case SYS_mknodat:
|
|
436
|
+
case SYS_utimensat:
|
|
437
|
+
case SYS_statx:
|
|
438
|
+
#ifdef SYS_fchmodat2
|
|
439
|
+
case SYS_fchmodat2:
|
|
440
|
+
#endif
|
|
396
441
|
case SYS_socket:
|
|
397
442
|
case SYS_connect:
|
|
398
443
|
case SYS_bind:
|
|
@@ -460,6 +505,17 @@ static const char *syscall_name(long nr) {
|
|
|
460
505
|
case SYS_faccessat2: return "faccessat2";
|
|
461
506
|
case SYS_readlink: return "readlink";
|
|
462
507
|
case SYS_readlinkat: return "readlinkat";
|
|
508
|
+
case SYS_symlink: return "symlink";
|
|
509
|
+
case SYS_symlinkat: return "symlinkat";
|
|
510
|
+
case SYS_link: return "link";
|
|
511
|
+
case SYS_linkat: return "linkat";
|
|
512
|
+
case SYS_mknod: return "mknod";
|
|
513
|
+
case SYS_mknodat: return "mknodat";
|
|
514
|
+
case SYS_utimensat: return "utimensat";
|
|
515
|
+
case SYS_statx: return "statx";
|
|
516
|
+
#ifdef SYS_fchmodat2
|
|
517
|
+
case SYS_fchmodat2: return "fchmodat2";
|
|
518
|
+
#endif
|
|
463
519
|
case SYS_socket: return "socket";
|
|
464
520
|
case SYS_connect: return "connect";
|
|
465
521
|
case SYS_bind: return "bind";
|
|
@@ -501,49 +557,63 @@ static int check_notification(pid_t pid, const struct seccomp_notif *req, int *w
|
|
|
501
557
|
|
|
502
558
|
path_out[0] = '\0';
|
|
503
559
|
g_current_pid = pid;
|
|
560
|
+
g_soft_deny = 0;
|
|
504
561
|
if (nr == SYS_execve || nr == SYS_execveat) {
|
|
505
562
|
char executable[MAX_PATH];
|
|
506
563
|
executable[0] = '\0';
|
|
507
564
|
(void)read_remote_string(pid, nr == SYS_execve ? a0 : a1, executable, sizeof(executable));
|
|
508
565
|
const char *base = strrchr(executable, '/');
|
|
509
566
|
base = base == NULL ? executable : base + 1;
|
|
510
|
-
if (
|
|
567
|
+
if (!is_bwrap_name(base)) g_command_started = 1;
|
|
511
568
|
*write_access = 0;
|
|
512
569
|
snprintf(path_out, MAX_PATH, "<exec>");
|
|
513
570
|
return 1;
|
|
514
571
|
}
|
|
515
572
|
if (nr == SYS_socket) {
|
|
516
|
-
|
|
573
|
+
/* Creating a socket does not transmit anything. bwrap already put the
|
|
574
|
+
* command in a private network namespace; tools such as pip create an
|
|
575
|
+
* AF_INET socket for local/version checks without connecting. All the
|
|
576
|
+
* operations that can actually reach a peer (connect/bind/listen/accept/
|
|
577
|
+
* sendto/recvfrom) remain denied below. */
|
|
517
578
|
*write_access = 0;
|
|
518
579
|
snprintf(path_out, MAX_PATH, "<network:socket>");
|
|
519
|
-
|
|
520
|
-
* needed by NSS, Python and the dynamic loader. AF_INET/AF_INET6 and raw
|
|
521
|
-
* sockets are still blocked. */
|
|
522
|
-
if (domain == AF_UNIX || domain == AF_NETLINK) return 1;
|
|
523
|
-
return 0;
|
|
580
|
+
return 1;
|
|
524
581
|
}
|
|
525
582
|
if (is_network_syscall(nr)) {
|
|
526
583
|
*write_access = 0;
|
|
527
584
|
snprintf(path_out, MAX_PATH, "<network:%s>", syscall_name(nr));
|
|
528
|
-
/*
|
|
529
|
-
*
|
|
585
|
+
/* bwrap itself needs AF_UNIX bind/listen/connect while it sets up the
|
|
586
|
+
* namespace; only the target's traffic is checked. */
|
|
587
|
+
if (!g_command_started || (g_current_pid > 0 && is_bwrap(g_current_pid))) return 1;
|
|
588
|
+
if (nr == SYS_listen || nr == SYS_accept || nr == SYS_accept4 || nr == SYS_recvfrom) {
|
|
589
|
+
/* No peer can appear outside the private netns; these operate on an
|
|
590
|
+
* already-authorized local socket. */
|
|
591
|
+
return 1;
|
|
592
|
+
}
|
|
593
|
+
if (nr == SYS_connect || nr == SYS_bind || nr == SYS_sendto) {
|
|
594
|
+
unsigned long addr_ptr = nr == SYS_sendto ? (unsigned long)req->data.args[4] : a1;
|
|
595
|
+
unsigned short family = 0;
|
|
596
|
+
if (addr_ptr != 0 && read_remote(pid, addr_ptr, &family, sizeof(family)) == 0) {
|
|
597
|
+
if (family == AF_UNIX || family == AF_NETLINK) return 1;
|
|
598
|
+
if (family == AF_INET || family == AF_INET6) {
|
|
599
|
+
/* A version check or NSS probe may create the socket and attempt a
|
|
600
|
+
* connection; return EPERM instead of killing the whole group. The
|
|
601
|
+
* private netns is the hard boundary; this is the graceful denial. */
|
|
602
|
+
g_soft_deny = 1;
|
|
603
|
+
snprintf(path_out, MAX_PATH, "<network:%s:inet>", syscall_name(nr));
|
|
604
|
+
return 0;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
g_soft_deny = 1;
|
|
608
|
+
return 0;
|
|
609
|
+
}
|
|
530
610
|
return 1;
|
|
531
611
|
}
|
|
532
612
|
if (nr == SYS_io_uring_setup || nr == SYS_ptrace || nr == SYS_open_by_handle_at) {
|
|
613
|
+
/* io_uring worker threads bypass a process's seccomp filter, so it can never
|
|
614
|
+
* be allowed inside the sandbox (libuv stays on epoll via UV_USE_IO_URING=0). */
|
|
533
615
|
*write_access = 0;
|
|
534
616
|
snprintf(path_out, MAX_PATH, "<forbidden:%s>", syscall_name(nr));
|
|
535
|
-
if (nr == SYS_io_uring_setup) {
|
|
536
|
-
char proc[64];
|
|
537
|
-
char exe[MAX_PATH];
|
|
538
|
-
snprintf(proc, sizeof(proc), "/proc/%d/exe", (int)pid);
|
|
539
|
-
ssize_t exe_n = readlink(proc, exe, sizeof(exe) - 1);
|
|
540
|
-
if (exe_n > 0) {
|
|
541
|
-
exe[exe_n] = '\0';
|
|
542
|
-
const char *base = strrchr(exe, '/');
|
|
543
|
-
base = base == NULL ? exe : base + 1;
|
|
544
|
-
if (strncmp(base, "node", 4) == 0) return 1;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
617
|
return 0;
|
|
548
618
|
}
|
|
549
619
|
|
|
@@ -606,15 +676,59 @@ static int check_notification(pid_t pid, const struct seccomp_notif *req, int *w
|
|
|
606
676
|
case SYS_readlinkat:
|
|
607
677
|
case SYS_fchmodat:
|
|
608
678
|
case SYS_fchownat:
|
|
679
|
+
case SYS_fchmodat2:
|
|
680
|
+
dirfd = (int)a0;
|
|
681
|
+
path_ptr = a1;
|
|
682
|
+
write = (nr == SYS_unlinkat || nr == SYS_mkdirat || nr == SYS_fchmodat || nr == SYS_fchownat || nr == SYS_fchmodat2);
|
|
683
|
+
break;
|
|
684
|
+
case SYS_symlink:
|
|
685
|
+
path_ptr = a1;
|
|
686
|
+
write = 1;
|
|
687
|
+
break;
|
|
688
|
+
case SYS_symlinkat:
|
|
689
|
+
dirfd = (int)a1;
|
|
690
|
+
path_ptr = a2;
|
|
691
|
+
write = 1;
|
|
692
|
+
break;
|
|
693
|
+
case SYS_link:
|
|
694
|
+
path_ptr = a0;
|
|
695
|
+
write = 0;
|
|
696
|
+
break;
|
|
697
|
+
case SYS_linkat:
|
|
609
698
|
dirfd = (int)a0;
|
|
610
699
|
path_ptr = a1;
|
|
611
|
-
write =
|
|
700
|
+
write = 0;
|
|
701
|
+
break;
|
|
702
|
+
case SYS_mknod:
|
|
703
|
+
path_ptr = a0;
|
|
704
|
+
write = 1;
|
|
705
|
+
break;
|
|
706
|
+
case SYS_mknodat:
|
|
707
|
+
dirfd = (int)a0;
|
|
708
|
+
path_ptr = a1;
|
|
709
|
+
write = 1;
|
|
710
|
+
break;
|
|
711
|
+
case SYS_utimensat:
|
|
712
|
+
dirfd = (int)a0;
|
|
713
|
+
path_ptr = a1;
|
|
714
|
+
write = 1;
|
|
715
|
+
break;
|
|
716
|
+
case SYS_statx:
|
|
717
|
+
dirfd = (int)a0;
|
|
718
|
+
path_ptr = a1;
|
|
719
|
+
write = 0;
|
|
612
720
|
break;
|
|
613
721
|
default:
|
|
614
722
|
*write_access = 0;
|
|
615
723
|
return 1;
|
|
616
724
|
}
|
|
617
725
|
|
|
726
|
+
if (path_ptr == 0) {
|
|
727
|
+
/* fd-only form (for example utimensat with AT_EMPTY_PATH): no new path is touched. */
|
|
728
|
+
snprintf(path_out, MAX_PATH, "<fd>");
|
|
729
|
+
*write_access = write;
|
|
730
|
+
return 1;
|
|
731
|
+
}
|
|
618
732
|
if (read_remote_string(pid, path_ptr, raw, sizeof(raw)) != 0) {
|
|
619
733
|
snprintf(path_out, MAX_PATH, "<unreadable>");
|
|
620
734
|
*write_access = write;
|
|
@@ -641,13 +755,38 @@ static int check_notification(pid_t pid, const struct seccomp_notif *req, int *w
|
|
|
641
755
|
}
|
|
642
756
|
snprintf(path_out, MAX_PATH, "%s", path);
|
|
643
757
|
*write_access = write;
|
|
758
|
+
/* renames and hard links also name a second, mutated path; it must pass the
|
|
759
|
+
* same write policy even though only the first name was resolved above. */
|
|
760
|
+
if (nr == SYS_rename || nr == SYS_renameat || nr == SYS_renameat2 || nr == SYS_link || nr == SYS_linkat) {
|
|
761
|
+
unsigned long second_ptr;
|
|
762
|
+
int second_dirfd = AT_FDCWD;
|
|
763
|
+
if (nr == SYS_rename || nr == SYS_link) {
|
|
764
|
+
second_ptr = a1;
|
|
765
|
+
} else {
|
|
766
|
+
second_dirfd = (int)a2;
|
|
767
|
+
second_ptr = a3;
|
|
768
|
+
}
|
|
769
|
+
char raw2[MAX_PATH];
|
|
770
|
+
char path2[MAX_PATH];
|
|
771
|
+
int existed2 = 0;
|
|
772
|
+
raw2[0] = '\0';
|
|
773
|
+
if (read_remote_string(pid, second_ptr, raw2, sizeof(raw2)) == 0 && raw2[0] != '\0') {
|
|
774
|
+
if (resolve_path(pid, second_dirfd, raw2, &existed2, path2, sizeof(path2)) != 0
|
|
775
|
+
|| !path_allowed(path2, 1)) {
|
|
776
|
+
snprintf(path_out, MAX_PATH, "%s", path2);
|
|
777
|
+
*write_access = 1;
|
|
778
|
+
return 0;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
644
782
|
/* `mkdir -p` walks every existing ancestor and calls mkdir on each one.
|
|
645
783
|
* Creating an already-existing workspace ancestor is a kernel no-op, not an
|
|
646
784
|
* out-of-workspace write. */
|
|
647
785
|
if (write && existed && (nr == SYS_mkdir || nr == SYS_mkdirat) && is_exact_policy_path(path)) return 1;
|
|
648
786
|
/* bwrap writes the user-namespace uid/gid maps under /proc before it execs
|
|
649
787
|
* the target. Nothing the target runs may write under /proc. */
|
|
650
|
-
if (write &&
|
|
788
|
+
if (write && starts_with_seg(path, "/proc") &&
|
|
789
|
+
(!g_command_started || (g_current_pid > 0 && is_bwrap(g_current_pid)))) return 1;
|
|
651
790
|
return path_allowed(path, write) ? 1 : 0;
|
|
652
791
|
}
|
|
653
792
|
|
|
@@ -704,6 +843,20 @@ static void reply_loop(int fd) {
|
|
|
704
843
|
char syscall_buf[64];
|
|
705
844
|
snprintf(syscall_buf, sizeof(syscall_buf), "%s", syscall_name((long)req.data.nr));
|
|
706
845
|
audit_record((pid_t)req.pid, syscall_buf, path, write_access);
|
|
846
|
+
if (g_soft_deny) {
|
|
847
|
+
/* A software check (pip version probe, NSS lookup fallback) gets a
|
|
848
|
+
* normal EPERM and can continue; file-policy violations still kill. */
|
|
849
|
+
respond(fd, &req, 0);
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
/* io_uring is a soft denial: libuv probes it despite UV_USE_IO_URING=0
|
|
853
|
+
* and must receive EPERM and fall back to epoll instead of taking the
|
|
854
|
+
* whole command group down. No io_uring instance can exist, so the
|
|
855
|
+
* path-check bypass this syscall would otherwise enable stays closed. */
|
|
856
|
+
if ((long)req.data.nr == SYS_io_uring_setup) {
|
|
857
|
+
respond(fd, &req, 0);
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
707
860
|
char debug_exe[512]; char debug_proc[64];
|
|
708
861
|
snprintf(debug_proc, sizeof(debug_proc), "/proc/%d/exe", (int)req.pid);
|
|
709
862
|
ssize_t debug_n = readlink(debug_proc, debug_exe, sizeof(debug_exe) - 1);
|
|
@@ -747,6 +900,8 @@ static void parse_policy(const char *file) {
|
|
|
747
900
|
if (g_dev_count < MAX_RULES) snprintf(g_dev[g_dev_count++], MAX_PATH, "%s", value);
|
|
748
901
|
} else if (strcmp(kind, "tmpbind") == 0) {
|
|
749
902
|
if (strcmp(value, g_workspace) != 0) g_tmpbind = 1;
|
|
903
|
+
} else if (strcmp(kind, "tmpfs") == 0) {
|
|
904
|
+
if (strcmp(value, "/tmp") == 0) g_tmpfs_tmp = 1;
|
|
750
905
|
} else if (strcmp(kind, "rule") == 0) {
|
|
751
906
|
char *sub = value;
|
|
752
907
|
char *arg = strchr(sub, ' ');
|
|
@@ -772,6 +927,10 @@ int main(int argc, char **argv) {
|
|
|
772
927
|
if (strcmp(argv[i], "--") == 0) { i++; break; }
|
|
773
928
|
if (strcmp(argv[i], "--policy") == 0 && i + 1 < argc) { policy = argv[++i]; continue; }
|
|
774
929
|
if (strcmp(argv[i], "--audit") == 0 && i + 1 < argc) { audit = argv[++i]; continue; }
|
|
930
|
+
if (strcmp(argv[i], "--cwd") == 0 && i + 1 < argc) {
|
|
931
|
+
snprintf(g_requested_cwd, sizeof(g_requested_cwd), "%s", argv[++i]);
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
775
934
|
if (strcmp(argv[i], "--label") == 0 && i + 1 < argc) { i++; continue; }
|
|
776
935
|
fprintf(stderr, "dsh-eval-supervisor: unknown argument %s\n", argv[i]);
|
|
777
936
|
return 125;
|
|
@@ -825,7 +984,7 @@ int main(int argc, char **argv) {
|
|
|
825
984
|
|
|
826
985
|
/* arch gate */
|
|
827
986
|
filter[n++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch));
|
|
828
|
-
filter[n++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,
|
|
987
|
+
filter[n++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SUPERVISOR_AUDIT_ARCH, 1, 0);
|
|
829
988
|
filter[n++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS);
|
|
830
989
|
filter[n++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr));
|
|
831
990
|
/* One comparison per intercepted syscall; long but far below the 4096 limit. */
|
|
@@ -861,6 +1020,17 @@ int main(int argc, char **argv) {
|
|
|
861
1020
|
ADD_SYSCALL(SYS_faccessat2);
|
|
862
1021
|
ADD_SYSCALL(SYS_readlink);
|
|
863
1022
|
ADD_SYSCALL(SYS_readlinkat);
|
|
1023
|
+
ADD_SYSCALL(SYS_symlink);
|
|
1024
|
+
ADD_SYSCALL(SYS_symlinkat);
|
|
1025
|
+
ADD_SYSCALL(SYS_link);
|
|
1026
|
+
ADD_SYSCALL(SYS_linkat);
|
|
1027
|
+
ADD_SYSCALL(SYS_mknod);
|
|
1028
|
+
ADD_SYSCALL(SYS_mknodat);
|
|
1029
|
+
ADD_SYSCALL(SYS_utimensat);
|
|
1030
|
+
ADD_SYSCALL(SYS_statx);
|
|
1031
|
+
#ifdef SYS_fchmodat2
|
|
1032
|
+
ADD_SYSCALL(SYS_fchmodat2);
|
|
1033
|
+
#endif
|
|
864
1034
|
ADD_SYSCALL(SYS_socket);
|
|
865
1035
|
ADD_SYSCALL(SYS_connect);
|
|
866
1036
|
ADD_SYSCALL(SYS_bind);
|
|
@@ -908,19 +1078,45 @@ int main(int argc, char **argv) {
|
|
|
908
1078
|
/* bwrap pivots into a fresh root and would leave the target at /. Preserve
|
|
909
1079
|
* the cwd the consumer chose (the tool's workdir) with bwrap --chdir. */
|
|
910
1080
|
{
|
|
911
|
-
char
|
|
1081
|
+
char inherited[MAX_PATH];
|
|
912
1082
|
const char *use_cwd = g_workspace;
|
|
913
|
-
if (
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
if (sep < 0) sep = argc;
|
|
917
|
-
char **next = (char **)calloc((size_t)(argc + 3), sizeof(char *));
|
|
1083
|
+
if (g_requested_cwd[0] != '\0' && starts_with_seg(g_requested_cwd, g_workspace)) use_cwd = g_requested_cwd;
|
|
1084
|
+
else if (getcwd(inherited, sizeof(inherited)) != NULL && starts_with_seg(inherited, g_workspace)) use_cwd = inherited;
|
|
1085
|
+
char **next = (char **)calloc((size_t)(argc + 16), sizeof(char *));
|
|
918
1086
|
if (next == NULL) _exit(125);
|
|
919
1087
|
int out = 0;
|
|
920
|
-
|
|
1088
|
+
next[out++] = argv[i]; /* bwrap */
|
|
921
1089
|
next[out++] = (char *)"--chdir";
|
|
922
1090
|
next[out++] = (char *)use_cwd;
|
|
923
|
-
|
|
1091
|
+
int env_restored = 0;
|
|
1092
|
+
for (int k = i + 1; k < argc; k++) {
|
|
1093
|
+
next[out++] = argv[k];
|
|
1094
|
+
/* The plugin clears the inherited environment with --clearenv. Re-add
|
|
1095
|
+
* the private launch markers the subprocess provider passed to THIS
|
|
1096
|
+
* process (never to arbitrary children) so run_code's control pipe and
|
|
1097
|
+
* packaged-PTC heap flag survive bwrap. */
|
|
1098
|
+
if (!env_restored && strcmp(argv[k], "--clearenv") == 0) {
|
|
1099
|
+
env_restored = 1;
|
|
1100
|
+
const char *control = getenv("DSH_SUBPROCESS_CONTROL");
|
|
1101
|
+
if (control != NULL && strcmp(control, "pipe") == 0) {
|
|
1102
|
+
next[out++] = (char *)"--setenv";
|
|
1103
|
+
next[out++] = (char *)"DSH_SUBPROCESS_CONTROL";
|
|
1104
|
+
next[out++] = (char *)"pipe";
|
|
1105
|
+
}
|
|
1106
|
+
const char *ptc = getenv("DSH_PTC_RUNTIME_NODE");
|
|
1107
|
+
if (ptc != NULL && ptc[0] != '\0') {
|
|
1108
|
+
next[out++] = (char *)"--setenv";
|
|
1109
|
+
next[out++] = (char *)"DSH_PTC_RUNTIME_NODE";
|
|
1110
|
+
next[out++] = (char *)ptc;
|
|
1111
|
+
const char *node_options = getenv("NODE_OPTIONS");
|
|
1112
|
+
if (node_options != NULL && node_options[0] != '\0') {
|
|
1113
|
+
next[out++] = (char *)"--setenv";
|
|
1114
|
+
next[out++] = (char *)"NODE_OPTIONS";
|
|
1115
|
+
next[out++] = (char *)node_options;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
924
1120
|
next[out] = NULL;
|
|
925
1121
|
execvp(next[0], next);
|
|
926
1122
|
}
|
|
@@ -55,3 +55,8 @@ gate. The 4-thread 0.02 s/image requirement is reported as
|
|
|
55
55
|
- PyTorch is required on the evaluation machine for the operator oracle.
|
|
56
56
|
- The 4-thread and all-thread model runs use the same trained model.
|
|
57
57
|
- The validation data and labels never enter the candidate workspace.
|
|
58
|
+
- The label file is read into the evaluator's memory; `infer` receives a copy of the
|
|
59
|
+
images in an isolated temporary directory, so it cannot derive the labels path
|
|
60
|
+
from its `--images` argument. Candidate/spawned processes also lose the host
|
|
61
|
+
`EVAL_*` handle variables.
|
|
62
|
+
- FPS and mean seconds/image include process startup and model loading.
|
|
@@ -12,6 +12,19 @@ from pathlib import Path
|
|
|
12
12
|
|
|
13
13
|
EVAL_ROOT = Path(__file__).resolve().parent
|
|
14
14
|
|
|
15
|
+
HOST_EVAL_KEYS = frozenset({
|
|
16
|
+
'EVAL_RESULT_FILE', 'EVAL_JOB_DIR', 'EVAL_INSTALL_TOKEN',
|
|
17
|
+
'EVAL_BASELINE_DIR', 'EVAL_VENV_MANIFEST',
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def child_env() -> dict[str, str]:
|
|
22
|
+
"""Environment for child processes that may run candidate code."""
|
|
23
|
+
return {
|
|
24
|
+
key: value for key, value in os.environ.items()
|
|
25
|
+
if key not in HOST_EVAL_KEYS and key not in {'PYTHONPATH', 'PYTHONHOME'}
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
|
|
16
29
|
def parse_workspace() -> Path:
|
|
17
30
|
parser = argparse.ArgumentParser(description='single-workspace evaluator')
|
|
@@ -20,7 +20,7 @@ import numpy as np
|
|
|
20
20
|
import torch
|
|
21
21
|
import torch.nn.functional as F
|
|
22
22
|
|
|
23
|
-
from common import EVAL_ROOT, clean_copy, emit, fail, parse_workspace
|
|
23
|
+
from common import EVAL_ROOT, child_env, clean_copy, emit, fail, parse_workspace
|
|
24
24
|
|
|
25
25
|
torch.set_num_threads(1)
|
|
26
26
|
torch.set_float32_matmul_precision('highest')
|
|
@@ -29,6 +29,9 @@ torch.backends.mkldnn.deterministic = True
|
|
|
29
29
|
torch.manual_seed(1234)
|
|
30
30
|
|
|
31
31
|
SEEDS = (11, 22, 33)
|
|
32
|
+
# A single CLI invocation budget and a total budget for the whole oracle run.
|
|
33
|
+
INVOCATION_TIMEOUT_SECONDS = 60
|
|
34
|
+
OPS_DEADLINE_SECONDS = 1800
|
|
32
35
|
|
|
33
36
|
|
|
34
37
|
def write_tensor(path: Path, t: torch.Tensor) -> None:
|
|
@@ -65,11 +68,12 @@ def build_candidate(workspace: Path, root: Path) -> Path:
|
|
|
65
68
|
shutil.rmtree(build, ignore_errors=True)
|
|
66
69
|
build.mkdir(parents=True)
|
|
67
70
|
cfg = subprocess.run(['cmake', '-S', '.', '-B', 'build', '-DCMAKE_BUILD_TYPE=Release'],
|
|
68
|
-
cwd=str(source), capture_output=True, text=True, encoding='utf-8', errors='replace',
|
|
71
|
+
cwd=str(source), capture_output=True, text=True, encoding='utf-8', errors='replace',
|
|
72
|
+
timeout=180, env=child_env())
|
|
69
73
|
if cfg.returncode != 0:
|
|
70
74
|
raise RuntimeError('cmake configure failed\n' + (cfg.stdout + cfg.stderr)[-4000:])
|
|
71
75
|
made = subprocess.run(['cmake', '--build', 'build', '-j4'], cwd=str(source), capture_output=True,
|
|
72
|
-
text=True, encoding='utf-8', errors='replace', timeout=1800)
|
|
76
|
+
text=True, encoding='utf-8', errors='replace', timeout=1800, env=child_env())
|
|
73
77
|
if made.returncode != 0:
|
|
74
78
|
raise RuntimeError('build failed\n' + (made.stdout + made.stderr)[-4000:])
|
|
75
79
|
binary = build / 'nn_task'
|
|
@@ -87,7 +91,8 @@ class Case:
|
|
|
87
91
|
self.flops = flops
|
|
88
92
|
|
|
89
93
|
|
|
90
|
-
def run_operation(name: str, threshold: float, cases: list[Case], binary: Path, root: Path
|
|
94
|
+
def run_operation(name: str, threshold: float, cases: list[Case], binary: Path, root: Path,
|
|
95
|
+
invocation_timeout: float = INVOCATION_TIMEOUT_SECONDS) -> dict:
|
|
91
96
|
op_dir = root / 'ops' / name
|
|
92
97
|
op_dir.mkdir(parents=True, exist_ok=True)
|
|
93
98
|
executable = True
|
|
@@ -111,7 +116,8 @@ def run_operation(name: str, threshold: float, cases: list[Case], binary: Path,
|
|
|
111
116
|
started = time.perf_counter()
|
|
112
117
|
try:
|
|
113
118
|
proc = subprocess.run(cmd, cwd=str(case_dir), capture_output=True, text=True,
|
|
114
|
-
encoding='utf-8', errors='replace', timeout=
|
|
119
|
+
encoding='utf-8', errors='replace', timeout=invocation_timeout,
|
|
120
|
+
env=child_env())
|
|
115
121
|
except subprocess.TimeoutExpired:
|
|
116
122
|
executable = False
|
|
117
123
|
first_error = first_error or 'timeout'
|
|
@@ -459,7 +465,13 @@ def main() -> int:
|
|
|
459
465
|
except Exception as exc:
|
|
460
466
|
return fail({'task': 'neural-fp32', 'ok': False, 'error': str(exc)})
|
|
461
467
|
rows = []
|
|
468
|
+
deadline = time.monotonic() + OPS_DEADLINE_SECONDS
|
|
462
469
|
for name, threshold, builder in OPERATIONS:
|
|
470
|
+
if time.monotonic() > deadline:
|
|
471
|
+
rows.append({'name': name, 'executable': False, 'passed': False,
|
|
472
|
+
'weighted_mean_relative_error': None, 'threshold': threshold,
|
|
473
|
+
'speed_gflops': None, 'error': 'evaluation deadline exceeded'})
|
|
474
|
+
continue
|
|
463
475
|
try:
|
|
464
476
|
cases = builder()
|
|
465
477
|
row = run_operation(name, threshold, cases, binary, root)
|
|
@@ -25,25 +25,22 @@ import numpy as np
|
|
|
25
25
|
|
|
26
26
|
HERE = Path(__file__).resolve().parent
|
|
27
27
|
sys.path.insert(0, str(HERE))
|
|
28
|
-
from common import clean_copy
|
|
28
|
+
from common import child_env, clean_copy
|
|
29
29
|
|
|
30
30
|
HIDDEN = HERE / 'hidden'
|
|
31
31
|
OPS = HERE / 'ops_eval.py'
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def run(cmd, cwd, timeout=1800):
|
|
35
|
-
env = dict(os.environ)
|
|
36
|
-
env.pop('PYTHONPATH', None)
|
|
37
|
-
env.pop('PYTHONHOME', None)
|
|
38
35
|
return subprocess.run(cmd, cwd=str(cwd), capture_output=True, text=True,
|
|
39
|
-
encoding='utf-8', errors='replace', env=
|
|
36
|
+
encoding='utf-8', errors='replace', env=child_env(), timeout=timeout)
|
|
40
37
|
|
|
41
38
|
|
|
42
39
|
def build_candidate(workspace: Path, root: Path):
|
|
43
40
|
candidate = root / 'candidate'
|
|
44
41
|
clean_copy(workspace, candidate, ignore_names=(
|
|
45
42
|
'data', 'build', 'work', '.tmp', '.cache', '.home', '.config', '.local',
|
|
46
|
-
'.git', '.venv', 'node_modules',
|
|
43
|
+
'.git', '.venv', 'node_modules', '.eval', '.submissions',
|
|
47
44
|
))
|
|
48
45
|
source = candidate
|
|
49
46
|
if not (source / 'CMakeLists.txt').is_file() and (candidate / 'workspace' / 'CMakeLists.txt').is_file():
|
|
@@ -82,33 +79,48 @@ def model_metrics(source: Path, threads: int, budget: float | None = None) -> di
|
|
|
82
79
|
return {'executable': False, 'accuracy': None, 'fps': None,
|
|
83
80
|
'mean_seconds_per_image': None, 'error': 'hidden validation data missing; run fetch_cifar.py',
|
|
84
81
|
'meets_4thread_budget': None}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
out.unlink()
|
|
89
|
-
cmd = [str(infer), '--model', str(source / 'model'),
|
|
90
|
-
'--images', str(images), '--out', str(out), '--threads', str(threads)]
|
|
91
|
-
started = time.perf_counter()
|
|
92
|
-
try:
|
|
93
|
-
proc = run(cmd, source, 1200)
|
|
94
|
-
except subprocess.TimeoutExpired:
|
|
95
|
-
return {'executable': False, 'accuracy': None, 'fps': None,
|
|
96
|
-
'mean_seconds_per_image': None, 'error': 'inference timeout',
|
|
97
|
-
'meets_4thread_budget': None}
|
|
98
|
-
elapsed = time.perf_counter() - started
|
|
99
|
-
if proc.returncode != 0:
|
|
100
|
-
return {'executable': False, 'accuracy': None, 'fps': None,
|
|
101
|
-
'mean_seconds_per_image': None,
|
|
102
|
-
'error': (proc.stderr or proc.stdout or 'infer failed')[-1000:],
|
|
103
|
-
'meets_4thread_budget': None}
|
|
82
|
+
# Read the truth labels into memory and hand the candidate a COPY of the
|
|
83
|
+
# images in an isolated directory: the labels file must never be reachable
|
|
84
|
+
# by deriving it from the --images argument.
|
|
104
85
|
try:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
86
|
+
labels_bytes = labels.read_bytes()
|
|
87
|
+
n = len(labels_bytes)
|
|
88
|
+
isolated = Path(tempfile.mkdtemp(prefix='cifar-images-'))
|
|
89
|
+
isolated_images = isolated / 'images.bin'
|
|
90
|
+
shutil.copyfile(images, isolated_images)
|
|
91
|
+
except OSError as exc:
|
|
109
92
|
return {'executable': False, 'accuracy': None, 'fps': None,
|
|
110
93
|
'mean_seconds_per_image': None, 'error': str(exc),
|
|
111
94
|
'meets_4thread_budget': None}
|
|
95
|
+
out = source / f'predictions_{threads}.bin'
|
|
96
|
+
try:
|
|
97
|
+
if out.exists():
|
|
98
|
+
out.unlink()
|
|
99
|
+
cmd = [str(infer), '--model', str(source / 'model'),
|
|
100
|
+
'--images', str(isolated_images), '--out', str(out), '--threads', str(threads)]
|
|
101
|
+
started = time.perf_counter()
|
|
102
|
+
try:
|
|
103
|
+
proc = run(cmd, source, 1200)
|
|
104
|
+
except subprocess.TimeoutExpired:
|
|
105
|
+
return {'executable': False, 'accuracy': None, 'fps': None,
|
|
106
|
+
'mean_seconds_per_image': None, 'error': 'inference timeout',
|
|
107
|
+
'meets_4thread_budget': None}
|
|
108
|
+
elapsed = time.perf_counter() - started
|
|
109
|
+
if proc.returncode != 0:
|
|
110
|
+
return {'executable': False, 'accuracy': None, 'fps': None,
|
|
111
|
+
'mean_seconds_per_image': None,
|
|
112
|
+
'error': (proc.stderr or proc.stdout or 'infer failed')[-1000:],
|
|
113
|
+
'meets_4thread_budget': None}
|
|
114
|
+
try:
|
|
115
|
+
pred = read_preds(out, n)
|
|
116
|
+
true = np.frombuffer(labels_bytes, dtype=np.uint8)
|
|
117
|
+
acc = float((pred == true).mean() * 100.0)
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
return {'executable': False, 'accuracy': None, 'fps': None,
|
|
120
|
+
'mean_seconds_per_image': None, 'error': str(exc),
|
|
121
|
+
'meets_4thread_budget': None}
|
|
122
|
+
finally:
|
|
123
|
+
shutil.rmtree(isolated, ignore_errors=True)
|
|
112
124
|
fps = n / elapsed if elapsed > 0 else None
|
|
113
125
|
mean_s = elapsed / n if n else None
|
|
114
126
|
result = {'executable': True, 'accuracy': acc, 'fps': fps,
|
|
@@ -133,10 +145,8 @@ def main() -> int:
|
|
|
133
145
|
# Reuse the existing FP32 operator evaluator, but run it in a temp
|
|
134
146
|
# copy so its build output does not affect the model workspace.
|
|
135
147
|
ops_result = root / 'ops.json'
|
|
136
|
-
env =
|
|
148
|
+
env = child_env()
|
|
137
149
|
env['EVAL_RESULT_FILE'] = str(ops_result)
|
|
138
|
-
env.pop('PYTHONPATH', None)
|
|
139
|
-
env.pop('PYTHONHOME', None)
|
|
140
150
|
try:
|
|
141
151
|
proc = subprocess.run([sys.executable, '-X', 'utf8', str(OPS), '--workspace', str(workspace)],
|
|
142
152
|
cwd=str(HERE), capture_output=True, text=True,
|