diff options
-rw-r--r-- | default.nix | 427 | ||||
-rw-r--r-- | evaluators.nix | 401 |
2 files changed, 828 insertions, 0 deletions
diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..d6ed821 --- /dev/null +++ b/default.nix @@ -0,0 +1,427 @@ +{ pkgs }: + +with pkgs; +with lib; + +rec { + qemu = (pkgs.qemu.override { + sdlSupport = false; + vncSupport = false; + spiceSupport = false; + pulseSupport = false; + smbdSupport = true; + hostCpuOnly = true; + }).overrideAttrs (old: rec { + name = "qemu-${version}"; + version = "3.0.0"; + src = fetchurl { + url = "http://wiki.qemu.org/download/qemu-${version}.tar.bz2"; + sha256 = "1s7bm2xhcxbc9is0rg8xzwijx7azv67skq7mjc58spsgc2nn4glk"; + }; + }); + + baseKernelPackages = linuxPackages; + + kconfig = kernel-config.override { + linux = baseKernelPackages.kernel; + } { + config = { + X86 = true; + "64BIT" = true; + #"PRINTK" "BUG" # extra debugging + + LOCAL_VERSION = "qeval"; + DEFAULT_HOSTNAME = "qeval"; + + SWAP = false; + + TTY = true; + SERIAL_8250 = true; + SERIAL_8250_CONSOLE = true; + + # execute elf and #! scripts + BINFMT_ELF = true; + BINFMT_SCRIPT = true; + + # enable ramdisk with gzip + BLK_DEV_INITRD = true; + RD_GZIP = true; + + # allow for userspace to shut kernel down + PROC_FS = true; + MAGIC_SYSRQ = true; + + # needed for guest to tell qemu to shutdown + PCI = true; + ACPI = true; + + # allow unix domain sockets + NET = true; + UNIX = true; + + # enable block layer + BLOCK = true; + BLK_DEV = true; + BLK_DEV_LOOP = true; + + MISC_FILESYSTEMS = true; + SQUASHFS = true; + SQUASHFS_LZ4 = true; + LZ4_DECOMPRESS = true; + SQUASHFS_DECOMP_SINGLE = true; + # SQUASHFS_DECOMP_MULTI = true; + # SQUASHFS_FILE_DIRECT = true; + SQUASHFS_FILE_CACHE = true; + + PROC_SYSCTL = true; + KERNFS = true; + SYSFS = true; + DEVTMPFS = true; + TMPFS = true; + + OVERLAY_FS = true; + + # support passing in various things + VIRTIO_PCI = true; + VIRTIO_BLK = true; + VIRTIO_INPUT = true; + VIRTIO_CONSOLE = true; + + FUTEX = true; + + # stop on kernel panic + PVPANIC = true; + X86_PLATFORM_DEVICES = true; + + # enable timers (ghc needs them) + POSIX_TIMERS = true; + TIMERFD = true; + EVENTFD = true; + EPOLL = true; + + # tsc scaling, maybe + # "X86_TSC" + + ADVISE_SYSCALLS = true; + + # "FSCACHE" + # "CACHEFILES" + + # TODO: disable IR_SANYO_DECODER, etc. + RC_CORE = false; + + # required for guest to gather entropy, some applications + # will otherwise block forever (e.g. rustc) + HW_RANDOM = true; + HW_RANDOM_VIRTIO = true; + }; + }; + + kernelPackages = linuxPackages_custom { + inherit (baseKernelPackages.kernel) version src; + configfile = kconfig; + }; + inherit (kernelPackages) kernel; + + initrdUtils = runCommand "initrd-utils" + { buildInputs = [ nukeReferences ]; + allowedReferences = [ "out" ]; # prevent accidents like glibc being included in the initrd + } + '' + mkdir -p $out/bin $out/lib + + # Copy what we need from Glibc. + cp -p ${stdenv.glibc.out}/lib/ld-linux*.so.? $out/lib + cp -p ${stdenv.glibc.out}/lib/libc.so.* $out/lib + cp -p ${stdenv.glibc.out}/lib/libm.so.* $out/lib + cp -p ${stdenv.glibc.out}/lib/libresolv.so.* $out/lib + + # Copy BusyBox. + cp -pd ${busybox}/bin/* $out/bin + + # Run patchelf to make the programs refer to the copied libraries. + for i in $out/bin/* $out/lib/*; do if ! test -L $i; then nuke-refs $i; fi; done + + for i in $out/bin/*; do + if [ -f "$i" -a ! -L "$i" ]; then + echo "patching $i..." + patchelf --set-interpreter $out/lib/ld-linux*.so.? --set-rpath $out/lib $i || true + fi + done + ''; + + closurePaths = path: + let closure = closureInfo { rootPaths = path; }; + text = lib.fileContents "${closure}/store-paths"; + in lib.splitString "\n" text; + + stage1 = writeScript "vm-run-stage1" '' + #! ${initrdUtils}/bin/ash -e + export PATH=${initrdUtils}/bin + + mkdir /etc + echo -n > /etc/fstab + + mount -t proc none /proc + mount -t sysfs none /sys + + # Does this even work with the current config? + echo 2 > /proc/sys/vm/panic_on_oom + + for o in $(cat /proc/cmdline); do + case $o in + jobDesc=*) + set -- $(IFS==; echo $o) + jobDesc=$2 + ;; + mountVirtfs=*) + set -- $(IFS==; echo $o) + mountVirtfs=$2 + ;; + esac + done + + mount -t devtmpfs devtmpfs /dev + + # ifconfig lo up + stty -icrnl -igncr # necessary? + + mkdir -p /dev/shm /dev/pts + mount -t tmpfs -o "mode=1777" none /dev/shm + mount -t devpts none /dev/pts + + mkdir -p /tmp /run /var + mount -t tmpfs -o "mode=1777" none /tmp + mount -t tmpfs -o "mode=755" none /run + ln -sfn /run /var/run + + mkdir -p /etc + ln -sf /proc/mounts /etc/mtab + echo "127.0.0.1 localhost" > /etc/hosts + echo "root:x:0:0:root:/:/bin/sh" > /etc/passwd + + mkdir -p /bin + ln -s ${initrdUtils}/bin/ash /bin/sh + + for store in /dev/vd*; do + if [ -e "$store" ]; then + name=$(basename $store) + mkdir -p /mnt/store/$name + mount -o ro,loop /dev/$name /mnt/store/$name + fi + done + + stores="$( ( find /mnt/store -mindepth 1 -maxdepth 1; echo /nix/store ) | paste -sd :)" + echo stores: $stores + mount -t overlay overlay -o "ro,lowerdir=$stores" /nix/store + + if [ -n "$jobDesc" ]; then + . "$jobDesc" + fi + + "$preCmd" + + echo ready > /dev/vport2p1 + read -r input < /dev/vport2p1 + echo "$input" > /input + + "$cmd" /input + + exec poweroff -f + ''; + + initrd = initrdPath: makeInitrd { + contents = [ + { object = stage1; + symlink = "/init"; } + { object = linkFarm "extra" + (map (p: { name = p.name; path = toString p; }) initrdPath); + symlink = "/tmp/extra"; } + ]; + }; + + squashfsTools = pkgs.squashfsTools.override { lz4Support = true; }; + mkSquashFs = settings: contents: bitflip.flipTwice (stdenv.mkDerivation { + name = "squashfs.img"; + nativeBuildInputs = [ squashfsTools ]; + buildCommand = '' + closureInfo=${closureInfo { rootPaths = contents; }} + mksquashfs $(cat $closureInfo/store-paths) $out \ + -keep-as-directory -all-root -b 1048576 ${settings} + ''; + }); + + mkSquashFsXz = mkSquashFs "-comp xz -Xdict-size 100%"; + mkSquashFsLz4 = mkSquashFs "-comp lz4 -Xhc"; + mkSquashFsGz = mkSquashFs "-comp gzip -Xcompression-level 9"; + + prepareJob = args@{ + name, aliases ? [], initrdPath ? [ initrdUtils ], storeDrives ? {}, mem ? 50, command, preCommand ? "", + doCheck ? true, testInput ? "", testOutput ? "success" }: + let + fullPath = (concatLists (builtins.attrValues storeDrives)) ++ initrdPath; + mkScript = cmd: writeScript "run" '' + #!/bin/sh -e + PATH=${lib.makeBinPath (map builtins.unsafeDiscardStringContext fullPath)} + ${cmd} + ''; + + desc = writeText "desc" '' + cmd=${mkScript command} + preCmd=${mkScript preCommand} + ''; + run' = run { + inherit name initrdPath fullPath mem desc; + storeDrives = (mapAttrs (k: mkSquashFsLz4) storeDrives) // { + desc = mkSquashFsLz4 [ desc initrdUtils ]; + }; + }; + + description = writeText "desc" (builtins.toJSON { + inherit name aliases mem; + available = map (p: p.name) fullPath; + }); + + self = stdenv.mkDerivation rec { + inherit name aliases; + + src = writeShellScriptBin "run" '' + set -e + PATH=${coreutils}/bin + job=$(mktemp -d) + ${run'}/bin/run-qemu "$job" "$@" + rm -rf "$job" + ''; + + installPhase = '' + mkdir -p $out/bin $out/desc + for n in $name $aliases; do + ln -s $src/bin/run "$out/bin/$n" + done + + ln -s ${description} "$out/desc/$name" + ''; + + inherit doCheck; + checkPhase = '' + EXPECTED="$(printf ${escapeShellArg testOutput})" + ${xxd}/bin/xxd <<<"$EXPECTED" + RESULT="$($src/bin/run ${escapeShellArg testInput})" + ${xxd}/bin/xxd <<<"$RESULT" + [ "$RESULT" = "$EXPECTED" ] + ''; + }; + in self // { + inherit desc; + run = run'; + + # Nix itself can't do it, because it can't check if something + # is a file or a directory (exportReferencesGraph doesn't tell), + # but apparmor rules differ based on that distinction + apparmor = stdenv.mkDerivation rec { + name = "apparmor.profile"; + + closureItems = [ + self self.src run' + ]; + + buildCommand = '' + ( + echo '${self.src}/bin/run flags=(complain) {' + echo ' signal, ptrace,' + echo ' /dev/kvm' wr, + echo ' /tmp/**' wr, + echo ' /proc/** r,' + echo ' /sys/devices/system/** r,' + + closure=${closureInfo { rootPaths = closureItems; }} + while read -r path; do + if [ -f "$path" ]; then + echo " $path mkrix," + elif [ -d "$path" ]; then + echo " $path** mkrix," + fi + done < $closure/store-paths + echo } + ) > $out + ''; + }; + }; + + # -drive if=virtio,readonly,format=qcow2,file="$disk" \ + + # -enable-kvm -cpu Haswell-noTSX-IBRS,vmx=on \ + # -cpu IvyBridge \ + # -net none -m "$mem" \ + # -virtfs local,readonly,path=/nix/store,security_model=none,mount_tag=store \ + commonQemuOptions = '' + -only-migratable \ + -nographic -no-reboot \ + -cpu IvyBridge \ + -enable-kvm \ + -net none -m "$mem" \ + -device virtio-rng-pci,max-bytes=1024,period=1000 \ + -device virtio-serial-pci \ + -device virtio-serial \ + -device pvpanic \ + -chardev pipe,path="$job"/control,id=control \ + -device virtserialport,chardev=control,id=control \ + -qmp-pretty unix:"$job"/qmp,nowait \''; + + qemuDriveOptions = lib.concatMapStringsSep " " (d: "-drive if=virtio,readonly,format=raw,file=${d}"); + + run = args@{ name, fullPath, initrdPath, storeDrives, mem, desc, ... }: writeShellScriptBin "run-qemu" '' + # ${name} + # needs ''${concatStringsSep ", " fullPath} + job="$1" + shift + mkfifo "$job"/control + mem="${toString mem}" + + ( echo '{ "execute": "qmp_capabilities" }' + ) | ${netcat}/bin/nc -lU "$job"/qmp >/dev/null & + + ( echo "$@" + echo ". /input" + ) > "$job"/control & + + timeout --foreground 10 \ + ${qemu}/bin/qemu-system-x86_64 \ + ${commonQemuOptions} + ${qemuDriveOptions (builtins.attrValues storeDrives)} \ + -incoming 'exec:${lz4}/bin/lz4 -d ${suspension args}' | ${dos2unix}/bin/dos2unix -f | head -c 500 + + # ^ qemu incorrectly does crlf conversion, check in the future if still necessary + '' // args; + + # if this doesn't build, and just silently sits there, try increasing memory + suspension = { name, initrdPath, fullPath, storeDrives, mem, desc }: bitflip.flipTwice (stdenv.mkDerivation { + name = "${name}-suspension"; + requiredSystemFeatures = [ "kvm" ]; + nativeBuildInputs = [ qemu netcat lz4 ]; + + inherit fullPath mem desc; + + buildCommand = '' + mkdir job + job=$PWD/job + mkfifo job/control + + ( read ready < job/control + echo '{ "execute": "qmp_capabilities" }' + echo '{ "execute": "migrate", "arguments": { "uri": "exec:lz4 -9 - '$out'" } }' + sleep 15 # FIXME + echo '{ "execute": "quit" }' + ) | ${netcat}/bin/nc -lU job/qmp & + + qemu-system-x86_64 \ + ${commonQemuOptions} + ${qemuDriveOptions (builtins.attrValues storeDrives)} \ + -kernel ${kernel}/bzImage \ + -initrd ${initrd initrdPath}/initrd \ + -append "console=ttyS0,38400 tsc=unstable jobDesc=${desc}" + ''; + }); + + evaluators = callPackage ./evaluators.nix { inherit prepareJob; }; +} diff --git a/evaluators.nix b/evaluators.nix new file mode 100644 index 0000000..1d07c28 --- /dev/null +++ b/evaluators.nix @@ -0,0 +1,401 @@ +{ pkgs, prepareJob }: +with pkgs; + +let + self = { + perl = prepareJob { + name = "perl"; + mem = 50; + aliases = [ "pl" ]; + storeDrives.perl = [ pkgsCross.musl64.perl ]; + preCommand = '' + perl -e 'print "Hello world!"' + ''; + + command = '' + perl "$1" + ''; + + testInput = "print \"success\""; + }; + + rust = + let + opts = lib.concatStringsSep " " [ + "--color never" + "-C opt-level=0" + "-C prefer-dynamic" + "-C debuginfo=0" + "-v" "--error-format short" + "-C codegen-units=1" + ]; + in prepareJob { + name = "rust"; + aliases = [ "rs" ]; + mem = 200; + storeDrives.rust = [ + (rustChannelOf { + channel = "nightly"; + date = "2018-08-06"; + }).rust + gcc + ]; + + preCommand = '' + echo 'fn main() {}' > /tmp/sample + rustc ${opts} -o /tmp/sample.out /tmp/sample + /tmp/sample.out + rm /tmp/sample /tmp/sample.out + ''; + + command = '' + mv "$1" /input.raw + cat > /input <<EOF + #![allow(unreachable_code, dead_code)] + fn main() { + println!("{:?}", { + $(cat /input.raw) + }) + } + EOF + rustc ${opts} -o /input.out /input && /input.out + ''; + + testInput = "\"success\""; + testOutput = "\"success\""; + }; + + go = prepareJob { + name = "go"; + mem = 100; + storeDrives.go = [ go ]; + + command = '' + mv "$1" /input.raw + cat > /input.go <<EOF + package main + import "fmt" + func main() { + fmt.Println($(cat /input.raw)) + } + EOF + go run /input.go + ''; + + testInput = ''"success"''; + }; + + c = prepareJob { + name = "c"; + mem = 100; + aliases = [ "gcc" ]; + storeDrives.gcc = [ gcc ]; + preCommand = '' + echo 'int main() { return 0; }' > /tmp/sample + gcc -x c -o /tmp/sample.out /tmp/sample + /tmp/sample.out + ''; + + command = '' + gcc -x c -o /input.out -w -fdiagnostics-color=never "$1" && /input.out + ''; + + testInput = '' + void main() { printf("success\n"); } + ''; + }; + + cpp = prepareJob { + name = "cpp"; + mem = 100; + storeDrives.gcc = [ gcc ]; + + command = '' + mv "$1" /input.raw + cat > /input <<EOF + #include <iostream> + using namespace std; + int main() { + $(cat /input.raw) + return 0; + }; + EOF + g++ -x 'c++' -o /input.out /input && /input.out + ''; + + testInput = "cout << \"success\" << endl;"; + }; + + tcc = prepareJob { + name = "tcc"; + mem = 50; + storeDrives.tcc = [ tinycc ]; + preCommand = '' + echo 'int main() { return 0; }' > /tmp/sample + tcc -run /tmp/sample + ''; + + command = '' + tcc -run "$1" 2>/dev/null + ''; + + testInput = '' + void main() { printf("success\n"); } + ''; + }; + + java = prepareJob rec { + name = "java"; + mem = 150; + + storeDrives.jdk = [ openjdk ]; + + preCommand = '' + cat > /tmp/Main.java <<EOF + ${testInput} + EOF + javac /tmp/Main.java + cd /tmp && java Main + ''; + + command = '' + mv "$1" /Main.java + javac Main.java && java Main + ''; + + # TODO: multi-line tests/input + testInput = '' + public class Main { public static void main(String... args) { System.out.println("success"); } } + ''; + }; + + kotlin = prepareJob rec { + name = "kotlin"; + mem = 300; + storeDrives.kotlin = [ kotlin ]; + + preCommand = '' + cat > /tmp/Main.kts <<EOF + ${testInput} + EOF + kotlinc -script /tmp/Main.kts + ''; + + command = '' + mv "$1" /input.kts + kotlinc -script /input.kts + ''; + + testInput = ''println("success")''; + }; + + python = prepareJob { + name = "python"; + aliases = [ "python3" "py" "py3" ]; + mem = 75; + storeDrives.python = [ python3 ]; + preCommand = '' + ${python3}/bin/python3 -c "print(42)" + ''; + + command = '' + ${python3}/bin/python3 "$1" + ''; + + testInput = "print(\"success\")"; + }; + + ruby = prepareJob { + name = "ruby"; + aliases = [ "rb" ]; + mem = 100; + storeDrives.ruby = [ ruby_2_5 ]; + + preCommand = '' + echo 42 | ruby + ''; + + command = '' + ruby "$1" + ''; + + testInput = "puts \"success\""; + }; + + sh = prepareJob { + name = "bash"; + aliases = [ "shell" "sh" ]; + mem = 60; + storeDrives.bash = [ bash coreutils gnused gnugrep gawk file bsdgames ]; + + command = '' + bash "$1" + ''; + + testInput = "echo success"; + }; + + ash = prepareJob { + name = "ash"; + command = '' + /bin/sh "$1" + ''; + + testInput = "echo success"; + }; + + nodejs = prepareJob { + name = "nodejs"; + aliases = [ "node" "js" ]; + mem = 100; + storeDrives.node = [ nodejs ]; + + preCommand = '' + node -e "console.log(42)" + ''; + + command = '' + node -p "$(cat "$1")" | tr -s '\n ' ' ' + ''; + /* + mv "$1" /input.raw + cat > /input <<EOF + function debug(val) { + return require("util").inspect(val, { depth: 1, colors: false }) + .replace(/\s+/g, ' ') + } + console.log(debug($(cat /input.raw))) + EOF + + node /input + '';*/ + + testInput = "'success'"; + testOutput = "success "; + }; + + lua = prepareJob { + name = "lua"; + mem = 50; + storeDrives.lua = [ lua5_3 ]; + + command = '' + lua "$1" + ''; + + testInput = "print(\"success\")"; + }; + + brainfuck = prepareJob { + name = "brainfuck"; + aliases = [ "bf" ]; + storeDrives.brainfuck = [ + (runCommand "just-brainfuck" {} '' + mkdir -p $out/bin + cp ${haskellPackages.brainfuck}/bin/bf $out/bin/ + '') + ]; + + preCommand = '' + echo '+[-[<<[+[--->]-[<<<]]]>>>-]>-.---.>..>.<<<<-.<+.>>>>>.>.<<.<-.' > /tmp/sample + bf < /tmp/sample + ''; + + command = '' + bf < "$1" + ''; + + testInput = "+[-[<<[+[--->]-[<<<]]]>>>-]>-.---.>..>.<<<<-.<+.>>>>>.>.<<.<-."; + testOutput = "hello world"; + }; + + php = prepareJob { + name = "php"; + mem = 100; + storeDrives.php = [ php ]; + + command = '' + php -r "$(cat "$1")" + ''; + + testInput = ''echo "success";''; + }; + + racket = prepareJob { + name = "racket"; + aliases = [ "rkt" "r" ]; + mem = 200; + storeDrives.racket = [ racket ]; + + preCommand = '' + racket -e '(+ 40 2)' + ''; + + command = '' + ( echo "#lang racket" + cat "$1" + ) | racket /proc/self/fd/0 + ''; + + testInput = "(displayln 'success)"; + }; + + guile = prepareJob { + name = "guile"; + mem = 100; + storeDrives.guile = [ guile ]; + + command = '' + guile --no-auto-compile -s "$1" + ''; + + testInput = ''(display "success")''; + }; + + haskell = prepareJob { + name = "haskell"; + aliases = [ "hask" "hs" "h" ]; + mem = 200; + storeDrives.ghc = [ ghc ]; + + preCommand = '' + echo '"foo":[]:[]' > /tmp/sample + ghci -v0 < /tmp/sample + ''; + + command = '' + ghci -v0 < "$1" + ''; + + testInput = "putStrLn \"success\""; + }; + + listAll = with self; [ + ash + sh + python + ruby + perl + lua + nodejs + haskell + rust + c tcc + cpp + java + # kotlin + racket + guile + brainfuck + php + go + ]; + + all = symlinkJoin { + name = "all-evaluators"; + paths = self.listAll; + }; + + apparmorAll = map (p: p.apparmor) self.listAll; + }; +in self |