dsh-eval-guard 0.7.2 → 0.7.3
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 +235 -0
- package/README.md +46 -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 +39 -32
- package/assets/neural-pendulum/TASK.md +137 -32
- package/assets/neural-pendulum/workspace.zip +0 -0
- package/eval/README.md +19 -9
- package/eval/__pycache__/bomberman.cpython-310.pyc +0 -0
- package/eval/__pycache__/commit0.cpython-310.pyc +0 -0
- package/eval/__pycache__/common.cpython-310.pyc +0 -0
- package/eval/__pycache__/ctf.cpython-310.pyc +0 -0
- package/eval/__pycache__/featurebench.cpython-310.pyc +0 -0
- package/eval/__pycache__/land.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-cifar.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-cpp.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-fp32.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-pendulum.cpython-310.pyc +0 -0
- package/eval/__pycache__/swe-bench-pro.cpython-310.pyc +0 -0
- package/eval/__pycache__/tank.cpython-310.pyc +0 -0
- package/eval/__pycache__/warehouse.cpython-310.pyc +0 -0
- 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/__pycache__/bomberman.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/commit0.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/ctf.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/featurebench.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/land.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/swe_bench_pro.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/tank.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/warehouse.cpython-310.pyc +0 -0
- 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 +878 -297
- package/native/dsh-eval-supervisor +0 -0
- package/native/supervisor.c +190 -26
- package/neural-cifar-eval/README.md +5 -0
- package/neural-cifar-eval/__pycache__/cifar_cache.cpython-310.pyc +0 -0
- package/neural-cifar-eval/__pycache__/common.cpython-310.pyc +0 -0
- package/neural-cifar-eval/__pycache__/fetch_cifar.cpython-310.pyc +0 -0
- package/neural-cifar-eval/__pycache__/ops_eval.cpython-310.pyc +0 -0
- package/neural-cifar-eval/__pycache__/run_eval.cpython-310.pyc +0 -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/__pycache__/ops_eval.cpython-310.pyc +0 -0
- package/neural-pendulum-eval/__pycache__/run_eval.cpython-310.pyc +0 -0
- package/neural-pendulum-eval/ops_eval.py +603 -0
- package/neural-pendulum-eval/run_eval.py +218 -137
- 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
|
@@ -57,6 +57,17 @@
|
|
|
57
57
|
#ifndef SYS_open_by_handle_at
|
|
58
58
|
#define SYS_open_by_handle_at 304
|
|
59
59
|
#endif
|
|
60
|
+
#ifndef SYS_fchmodat2
|
|
61
|
+
#define SYS_fchmodat2 452
|
|
62
|
+
#endif
|
|
63
|
+
|
|
64
|
+
#if defined(__x86_64__)
|
|
65
|
+
#define SUPERVISOR_AUDIT_ARCH AUDIT_ARCH_X86_64
|
|
66
|
+
#elif defined(__aarch64__)
|
|
67
|
+
#define SUPERVISOR_AUDIT_ARCH AUDIT_ARCH_AARCH64
|
|
68
|
+
#else
|
|
69
|
+
#error "dsh-eval-supervisor: unsupported architecture"
|
|
70
|
+
#endif
|
|
60
71
|
|
|
61
72
|
#define MAX_RULES 512
|
|
62
73
|
#define MAX_PATH 4096
|
|
@@ -77,13 +88,31 @@ static Rule g_rules[MAX_RULES];
|
|
|
77
88
|
static int g_rule_count;
|
|
78
89
|
static int g_audit_fd = -1;
|
|
79
90
|
static volatile sig_atomic_t g_stop;
|
|
91
|
+
/* The sandbox mounted a private tmpfs at /tmp (workspace under host /tmp). */
|
|
92
|
+
static int g_tmpfs_tmp;
|
|
93
|
+
static char g_requested_cwd[MAX_PATH];
|
|
80
94
|
/* bwrap itself needs AF_UNIX and AF_NETLINK sockets during setup. The target
|
|
81
95
|
* command is allowed no sockets at all once bwrap has exec'd it. */
|
|
82
96
|
static int g_command_started;
|
|
83
97
|
static pid_t g_current_pid;
|
|
84
98
|
static int g_tmpbind;
|
|
85
99
|
|
|
86
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Whether the notifying process is bubblewrap itself (setup phase).
|
|
102
|
+
*
|
|
103
|
+
* The bwrap command may be a package-manager shim that re-execs several times,
|
|
104
|
+
* so identification is by executable name (as upstream did). The setup-only
|
|
105
|
+
* exemptions it enables are restricted to paths that do not exist after
|
|
106
|
+
* pivot_root, and the command-started gate closes them for the target.
|
|
107
|
+
*/
|
|
108
|
+
static int is_bwrap_name(const char *name) {
|
|
109
|
+
if (name == NULL || name[0] == '\0') return 0;
|
|
110
|
+
/* Debian/Fedora ship /usr/bin/bwrap; the project/package is also commonly
|
|
111
|
+
* called bubblewrap. Accept both spellings so bwrap's own setup writes
|
|
112
|
+
* (uid_map/gid_map under /proc) are recognized regardless of binary name. */
|
|
113
|
+
return strstr(name, "bwrap") != NULL || strstr(name, "bubblewrap") != NULL;
|
|
114
|
+
}
|
|
115
|
+
|
|
87
116
|
static int is_bwrap(pid_t pid) {
|
|
88
117
|
char proc[64];
|
|
89
118
|
char exe[MAX_PATH];
|
|
@@ -93,7 +122,7 @@ static int is_bwrap(pid_t pid) {
|
|
|
93
122
|
exe[n] = '\0';
|
|
94
123
|
const char *base = strrchr(exe, '/');
|
|
95
124
|
base = base == NULL ? exe : base + 1;
|
|
96
|
-
return
|
|
125
|
+
return is_bwrap_name(base);
|
|
97
126
|
}
|
|
98
127
|
|
|
99
128
|
static void on_signal(int sig) {
|
|
@@ -302,6 +331,9 @@ static int rules_override_read(const char *path, int initial) {
|
|
|
302
331
|
|
|
303
332
|
static int path_allowed(const char *path, int write_access) {
|
|
304
333
|
if (g_workspace[0] != '\0' && starts_with_seg(path, g_workspace)) return 1;
|
|
334
|
+
/* A private tmpfs at /tmp is ephemeral by construction: it never exposes
|
|
335
|
+
* host files, so both read and write are safe. */
|
|
336
|
+
if (g_tmpfs_tmp && starts_with_seg(path, "/tmp")) return 1;
|
|
305
337
|
/* bubblewrap builds its new root under /tmp and /newroot before pivot_root.
|
|
306
338
|
* Those transient paths exist only during setup and are removed on exit. */
|
|
307
339
|
if ((!g_command_started || (g_current_pid > 0 && is_bwrap(g_current_pid))) &&
|
|
@@ -393,6 +425,17 @@ __attribute__((unused)) static int should_notify(long nr) {
|
|
|
393
425
|
case SYS_faccessat2:
|
|
394
426
|
case SYS_readlink:
|
|
395
427
|
case SYS_readlinkat:
|
|
428
|
+
case SYS_symlink:
|
|
429
|
+
case SYS_symlinkat:
|
|
430
|
+
case SYS_link:
|
|
431
|
+
case SYS_linkat:
|
|
432
|
+
case SYS_mknod:
|
|
433
|
+
case SYS_mknodat:
|
|
434
|
+
case SYS_utimensat:
|
|
435
|
+
case SYS_statx:
|
|
436
|
+
#ifdef SYS_fchmodat2
|
|
437
|
+
case SYS_fchmodat2:
|
|
438
|
+
#endif
|
|
396
439
|
case SYS_socket:
|
|
397
440
|
case SYS_connect:
|
|
398
441
|
case SYS_bind:
|
|
@@ -460,6 +503,17 @@ static const char *syscall_name(long nr) {
|
|
|
460
503
|
case SYS_faccessat2: return "faccessat2";
|
|
461
504
|
case SYS_readlink: return "readlink";
|
|
462
505
|
case SYS_readlinkat: return "readlinkat";
|
|
506
|
+
case SYS_symlink: return "symlink";
|
|
507
|
+
case SYS_symlinkat: return "symlinkat";
|
|
508
|
+
case SYS_link: return "link";
|
|
509
|
+
case SYS_linkat: return "linkat";
|
|
510
|
+
case SYS_mknod: return "mknod";
|
|
511
|
+
case SYS_mknodat: return "mknodat";
|
|
512
|
+
case SYS_utimensat: return "utimensat";
|
|
513
|
+
case SYS_statx: return "statx";
|
|
514
|
+
#ifdef SYS_fchmodat2
|
|
515
|
+
case SYS_fchmodat2: return "fchmodat2";
|
|
516
|
+
#endif
|
|
463
517
|
case SYS_socket: return "socket";
|
|
464
518
|
case SYS_connect: return "connect";
|
|
465
519
|
case SYS_bind: return "bind";
|
|
@@ -507,7 +561,7 @@ static int check_notification(pid_t pid, const struct seccomp_notif *req, int *w
|
|
|
507
561
|
(void)read_remote_string(pid, nr == SYS_execve ? a0 : a1, executable, sizeof(executable));
|
|
508
562
|
const char *base = strrchr(executable, '/');
|
|
509
563
|
base = base == NULL ? executable : base + 1;
|
|
510
|
-
if (
|
|
564
|
+
if (!is_bwrap_name(base)) g_command_started = 1;
|
|
511
565
|
*write_access = 0;
|
|
512
566
|
snprintf(path_out, MAX_PATH, "<exec>");
|
|
513
567
|
return 1;
|
|
@@ -530,20 +584,10 @@ static int check_notification(pid_t pid, const struct seccomp_notif *req, int *w
|
|
|
530
584
|
return 1;
|
|
531
585
|
}
|
|
532
586
|
if (nr == SYS_io_uring_setup || nr == SYS_ptrace || nr == SYS_open_by_handle_at) {
|
|
587
|
+
/* io_uring worker threads bypass a process's seccomp filter, so it can never
|
|
588
|
+
* be allowed inside the sandbox (libuv stays on epoll via UV_USE_IO_URING=0). */
|
|
533
589
|
*write_access = 0;
|
|
534
590
|
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
591
|
return 0;
|
|
548
592
|
}
|
|
549
593
|
|
|
@@ -606,15 +650,59 @@ static int check_notification(pid_t pid, const struct seccomp_notif *req, int *w
|
|
|
606
650
|
case SYS_readlinkat:
|
|
607
651
|
case SYS_fchmodat:
|
|
608
652
|
case SYS_fchownat:
|
|
653
|
+
case SYS_fchmodat2:
|
|
654
|
+
dirfd = (int)a0;
|
|
655
|
+
path_ptr = a1;
|
|
656
|
+
write = (nr == SYS_unlinkat || nr == SYS_mkdirat || nr == SYS_fchmodat || nr == SYS_fchownat || nr == SYS_fchmodat2);
|
|
657
|
+
break;
|
|
658
|
+
case SYS_symlink:
|
|
659
|
+
path_ptr = a1;
|
|
660
|
+
write = 1;
|
|
661
|
+
break;
|
|
662
|
+
case SYS_symlinkat:
|
|
663
|
+
dirfd = (int)a1;
|
|
664
|
+
path_ptr = a2;
|
|
665
|
+
write = 1;
|
|
666
|
+
break;
|
|
667
|
+
case SYS_link:
|
|
668
|
+
path_ptr = a0;
|
|
669
|
+
write = 0;
|
|
670
|
+
break;
|
|
671
|
+
case SYS_linkat:
|
|
672
|
+
dirfd = (int)a0;
|
|
673
|
+
path_ptr = a1;
|
|
674
|
+
write = 0;
|
|
675
|
+
break;
|
|
676
|
+
case SYS_mknod:
|
|
677
|
+
path_ptr = a0;
|
|
678
|
+
write = 1;
|
|
679
|
+
break;
|
|
680
|
+
case SYS_mknodat:
|
|
681
|
+
dirfd = (int)a0;
|
|
682
|
+
path_ptr = a1;
|
|
683
|
+
write = 1;
|
|
684
|
+
break;
|
|
685
|
+
case SYS_utimensat:
|
|
686
|
+
dirfd = (int)a0;
|
|
687
|
+
path_ptr = a1;
|
|
688
|
+
write = 1;
|
|
689
|
+
break;
|
|
690
|
+
case SYS_statx:
|
|
609
691
|
dirfd = (int)a0;
|
|
610
692
|
path_ptr = a1;
|
|
611
|
-
write =
|
|
693
|
+
write = 0;
|
|
612
694
|
break;
|
|
613
695
|
default:
|
|
614
696
|
*write_access = 0;
|
|
615
697
|
return 1;
|
|
616
698
|
}
|
|
617
699
|
|
|
700
|
+
if (path_ptr == 0) {
|
|
701
|
+
/* fd-only form (for example utimensat with AT_EMPTY_PATH): no new path is touched. */
|
|
702
|
+
snprintf(path_out, MAX_PATH, "<fd>");
|
|
703
|
+
*write_access = write;
|
|
704
|
+
return 1;
|
|
705
|
+
}
|
|
618
706
|
if (read_remote_string(pid, path_ptr, raw, sizeof(raw)) != 0) {
|
|
619
707
|
snprintf(path_out, MAX_PATH, "<unreadable>");
|
|
620
708
|
*write_access = write;
|
|
@@ -641,13 +729,38 @@ static int check_notification(pid_t pid, const struct seccomp_notif *req, int *w
|
|
|
641
729
|
}
|
|
642
730
|
snprintf(path_out, MAX_PATH, "%s", path);
|
|
643
731
|
*write_access = write;
|
|
732
|
+
/* renames and hard links also name a second, mutated path; it must pass the
|
|
733
|
+
* same write policy even though only the first name was resolved above. */
|
|
734
|
+
if (nr == SYS_rename || nr == SYS_renameat || nr == SYS_renameat2 || nr == SYS_link || nr == SYS_linkat) {
|
|
735
|
+
unsigned long second_ptr;
|
|
736
|
+
int second_dirfd = AT_FDCWD;
|
|
737
|
+
if (nr == SYS_rename || nr == SYS_link) {
|
|
738
|
+
second_ptr = a1;
|
|
739
|
+
} else {
|
|
740
|
+
second_dirfd = (int)a2;
|
|
741
|
+
second_ptr = a3;
|
|
742
|
+
}
|
|
743
|
+
char raw2[MAX_PATH];
|
|
744
|
+
char path2[MAX_PATH];
|
|
745
|
+
int existed2 = 0;
|
|
746
|
+
raw2[0] = '\0';
|
|
747
|
+
if (read_remote_string(pid, second_ptr, raw2, sizeof(raw2)) == 0 && raw2[0] != '\0') {
|
|
748
|
+
if (resolve_path(pid, second_dirfd, raw2, &existed2, path2, sizeof(path2)) != 0
|
|
749
|
+
|| !path_allowed(path2, 1)) {
|
|
750
|
+
snprintf(path_out, MAX_PATH, "%s", path2);
|
|
751
|
+
*write_access = 1;
|
|
752
|
+
return 0;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
644
756
|
/* `mkdir -p` walks every existing ancestor and calls mkdir on each one.
|
|
645
757
|
* Creating an already-existing workspace ancestor is a kernel no-op, not an
|
|
646
758
|
* out-of-workspace write. */
|
|
647
759
|
if (write && existed && (nr == SYS_mkdir || nr == SYS_mkdirat) && is_exact_policy_path(path)) return 1;
|
|
648
760
|
/* bwrap writes the user-namespace uid/gid maps under /proc before it execs
|
|
649
761
|
* the target. Nothing the target runs may write under /proc. */
|
|
650
|
-
if (write &&
|
|
762
|
+
if (write && starts_with_seg(path, "/proc") &&
|
|
763
|
+
(!g_command_started || (g_current_pid > 0 && is_bwrap(g_current_pid)))) return 1;
|
|
651
764
|
return path_allowed(path, write) ? 1 : 0;
|
|
652
765
|
}
|
|
653
766
|
|
|
@@ -704,6 +817,14 @@ static void reply_loop(int fd) {
|
|
|
704
817
|
char syscall_buf[64];
|
|
705
818
|
snprintf(syscall_buf, sizeof(syscall_buf), "%s", syscall_name((long)req.data.nr));
|
|
706
819
|
audit_record((pid_t)req.pid, syscall_buf, path, write_access);
|
|
820
|
+
/* io_uring is a soft denial: libuv probes it despite UV_USE_IO_URING=0
|
|
821
|
+
* and must receive EPERM and fall back to epoll instead of taking the
|
|
822
|
+
* whole command group down. No io_uring instance can exist, so the
|
|
823
|
+
* path-check bypass this syscall would otherwise enable stays closed. */
|
|
824
|
+
if ((long)req.data.nr == SYS_io_uring_setup) {
|
|
825
|
+
respond(fd, &req, 0);
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
707
828
|
char debug_exe[512]; char debug_proc[64];
|
|
708
829
|
snprintf(debug_proc, sizeof(debug_proc), "/proc/%d/exe", (int)req.pid);
|
|
709
830
|
ssize_t debug_n = readlink(debug_proc, debug_exe, sizeof(debug_exe) - 1);
|
|
@@ -747,6 +868,8 @@ static void parse_policy(const char *file) {
|
|
|
747
868
|
if (g_dev_count < MAX_RULES) snprintf(g_dev[g_dev_count++], MAX_PATH, "%s", value);
|
|
748
869
|
} else if (strcmp(kind, "tmpbind") == 0) {
|
|
749
870
|
if (strcmp(value, g_workspace) != 0) g_tmpbind = 1;
|
|
871
|
+
} else if (strcmp(kind, "tmpfs") == 0) {
|
|
872
|
+
if (strcmp(value, "/tmp") == 0) g_tmpfs_tmp = 1;
|
|
750
873
|
} else if (strcmp(kind, "rule") == 0) {
|
|
751
874
|
char *sub = value;
|
|
752
875
|
char *arg = strchr(sub, ' ');
|
|
@@ -772,6 +895,10 @@ int main(int argc, char **argv) {
|
|
|
772
895
|
if (strcmp(argv[i], "--") == 0) { i++; break; }
|
|
773
896
|
if (strcmp(argv[i], "--policy") == 0 && i + 1 < argc) { policy = argv[++i]; continue; }
|
|
774
897
|
if (strcmp(argv[i], "--audit") == 0 && i + 1 < argc) { audit = argv[++i]; continue; }
|
|
898
|
+
if (strcmp(argv[i], "--cwd") == 0 && i + 1 < argc) {
|
|
899
|
+
snprintf(g_requested_cwd, sizeof(g_requested_cwd), "%s", argv[++i]);
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
775
902
|
if (strcmp(argv[i], "--label") == 0 && i + 1 < argc) { i++; continue; }
|
|
776
903
|
fprintf(stderr, "dsh-eval-supervisor: unknown argument %s\n", argv[i]);
|
|
777
904
|
return 125;
|
|
@@ -825,7 +952,7 @@ int main(int argc, char **argv) {
|
|
|
825
952
|
|
|
826
953
|
/* arch gate */
|
|
827
954
|
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,
|
|
955
|
+
filter[n++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SUPERVISOR_AUDIT_ARCH, 1, 0);
|
|
829
956
|
filter[n++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS);
|
|
830
957
|
filter[n++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr));
|
|
831
958
|
/* One comparison per intercepted syscall; long but far below the 4096 limit. */
|
|
@@ -861,6 +988,17 @@ int main(int argc, char **argv) {
|
|
|
861
988
|
ADD_SYSCALL(SYS_faccessat2);
|
|
862
989
|
ADD_SYSCALL(SYS_readlink);
|
|
863
990
|
ADD_SYSCALL(SYS_readlinkat);
|
|
991
|
+
ADD_SYSCALL(SYS_symlink);
|
|
992
|
+
ADD_SYSCALL(SYS_symlinkat);
|
|
993
|
+
ADD_SYSCALL(SYS_link);
|
|
994
|
+
ADD_SYSCALL(SYS_linkat);
|
|
995
|
+
ADD_SYSCALL(SYS_mknod);
|
|
996
|
+
ADD_SYSCALL(SYS_mknodat);
|
|
997
|
+
ADD_SYSCALL(SYS_utimensat);
|
|
998
|
+
ADD_SYSCALL(SYS_statx);
|
|
999
|
+
#ifdef SYS_fchmodat2
|
|
1000
|
+
ADD_SYSCALL(SYS_fchmodat2);
|
|
1001
|
+
#endif
|
|
864
1002
|
ADD_SYSCALL(SYS_socket);
|
|
865
1003
|
ADD_SYSCALL(SYS_connect);
|
|
866
1004
|
ADD_SYSCALL(SYS_bind);
|
|
@@ -908,19 +1046,45 @@ int main(int argc, char **argv) {
|
|
|
908
1046
|
/* bwrap pivots into a fresh root and would leave the target at /. Preserve
|
|
909
1047
|
* the cwd the consumer chose (the tool's workdir) with bwrap --chdir. */
|
|
910
1048
|
{
|
|
911
|
-
char
|
|
1049
|
+
char inherited[MAX_PATH];
|
|
912
1050
|
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 *));
|
|
1051
|
+
if (g_requested_cwd[0] != '\0' && starts_with_seg(g_requested_cwd, g_workspace)) use_cwd = g_requested_cwd;
|
|
1052
|
+
else if (getcwd(inherited, sizeof(inherited)) != NULL && starts_with_seg(inherited, g_workspace)) use_cwd = inherited;
|
|
1053
|
+
char **next = (char **)calloc((size_t)(argc + 16), sizeof(char *));
|
|
918
1054
|
if (next == NULL) _exit(125);
|
|
919
1055
|
int out = 0;
|
|
920
|
-
|
|
1056
|
+
next[out++] = argv[i]; /* bwrap */
|
|
921
1057
|
next[out++] = (char *)"--chdir";
|
|
922
1058
|
next[out++] = (char *)use_cwd;
|
|
923
|
-
|
|
1059
|
+
int env_restored = 0;
|
|
1060
|
+
for (int k = i + 1; k < argc; k++) {
|
|
1061
|
+
next[out++] = argv[k];
|
|
1062
|
+
/* The plugin clears the inherited environment with --clearenv. Re-add
|
|
1063
|
+
* the private launch markers the subprocess provider passed to THIS
|
|
1064
|
+
* process (never to arbitrary children) so run_code's control pipe and
|
|
1065
|
+
* packaged-PTC heap flag survive bwrap. */
|
|
1066
|
+
if (!env_restored && strcmp(argv[k], "--clearenv") == 0) {
|
|
1067
|
+
env_restored = 1;
|
|
1068
|
+
const char *control = getenv("DSH_SUBPROCESS_CONTROL");
|
|
1069
|
+
if (control != NULL && strcmp(control, "pipe") == 0) {
|
|
1070
|
+
next[out++] = (char *)"--setenv";
|
|
1071
|
+
next[out++] = (char *)"DSH_SUBPROCESS_CONTROL";
|
|
1072
|
+
next[out++] = (char *)"pipe";
|
|
1073
|
+
}
|
|
1074
|
+
const char *ptc = getenv("DSH_PTC_RUNTIME_NODE");
|
|
1075
|
+
if (ptc != NULL && ptc[0] != '\0') {
|
|
1076
|
+
next[out++] = (char *)"--setenv";
|
|
1077
|
+
next[out++] = (char *)"DSH_PTC_RUNTIME_NODE";
|
|
1078
|
+
next[out++] = (char *)ptc;
|
|
1079
|
+
const char *node_options = getenv("NODE_OPTIONS");
|
|
1080
|
+
if (node_options != NULL && node_options[0] != '\0') {
|
|
1081
|
+
next[out++] = (char *)"--setenv";
|
|
1082
|
+
next[out++] = (char *)"NODE_OPTIONS";
|
|
1083
|
+
next[out++] = (char *)node_options;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
924
1088
|
next[out] = NULL;
|
|
925
1089
|
execvp(next[0], next);
|
|
926
1090
|
}
|
|
@@ -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.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# neural-pendulum-eval
|
|
2
|
+
|
|
3
|
+
Standalone evaluator for the **Neural-Pendulum** task. Copy this whole folder to
|
|
4
|
+
a more performance-stable Linux machine to rerun the evaluation without the DSH
|
|
5
|
+
plugin UI.
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- `run_eval.py` — builds the latest submission, runs the operator oracle, then
|
|
10
|
+
drives `build/control` against the fixed-initial-state simulator and emits a
|
|
11
|
+
staged result (`no_submission` / `compile` / `infra` / `controller` /
|
|
12
|
+
`simulation` / `scoring` / `completed`).
|
|
13
|
+
- `ops_eval.py` — operator oracle: PyTorch references computed in-process,
|
|
14
|
+
weighted mean relative error compared with per-operation thresholds. Same
|
|
15
|
+
operations, cases, seeds and thresholds as Neural-FP32 / Neural-CIFAR.
|
|
16
|
+
- `sim_eval` — trusted simulator binary for the evaluation initial state.
|
|
17
|
+
Identical to the workspace `tools/sim`.
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Python 3 with `numpy` and `torch` (CPU) for the operator oracle.
|
|
22
|
+
- `cmake`, `g++`, `unzip`.
|
|
23
|
+
|
|
24
|
+
## Run
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
python3 run_eval.py --workspace /path/to/staged/workspace
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The workspace must contain a host-staged `.eval/` directory:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
.eval/
|
|
34
|
+
meta.json # {"started_at": <unix seconds>, "t_tgt": 10}
|
|
35
|
+
events.jsonl # host submit/test events, used for the time integral
|
|
36
|
+
submissions/<id>/ # snapshot with CMakeLists.txt, src/, ...
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
When `EVAL_RESULT_FILE` is set, the JSON result is written there; otherwise it
|
|
40
|
+
is printed to stdout. The first stdout lines always summarize the operator
|
|
41
|
+
oracle:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
all_operations_executable = ...
|
|
45
|
+
all_operations_passed = ...
|
|
46
|
+
max_weighted_mean_relative_error = ...
|
|
47
|
+
speed_geomean = ...
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
followed by one row per operation. Only when every operation is executable and
|
|
51
|
+
within threshold does the evaluator run the controller. Operator speed is
|
|
52
|
+
reported but never gates completion.
|
|
53
|
+
|
|
54
|
+
Budgets: build 8 min, operator oracle 4 min total (60 s per CLI invocation),
|
|
55
|
+
controller 6 min. The host-side `eval_test` tool kills the evaluator after
|
|
56
|
+
20 minutes.
|
|
57
|
+
|
|
58
|
+
The evaluator reads simulator/controller responses through a buffered binary
|
|
59
|
+
line reader with per-read deadlines (a text wrapper can hide already-buffered
|
|
60
|
+
lines from a selector). Simulator/controller stderr is discarded so a verbose
|
|
61
|
+
controller cannot deadlock on a full pipe, and those children never inherit the
|
|
62
|
+
host `EVAL_*` variables.
|
|
Binary file
|
|
Binary file
|