From 1b9f8e0b592bd1df57e1d4ba6f9fb3892f92051d Mon Sep 17 00:00:00 2001 From: Lilac Date: Wed, 14 Oct 2026 23:44:34 -0400 Subject: [PATCH] initial commit --- flake.lock | 27 +++++++++++ flake.nix | 15 ++++++ init.sh | 31 +++++++++++++ pigeon.sh | 30 ++++++++++++ run.sh | 5 ++ vm.nix | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++++ webhook.nix | 42 +++++++++++++++++ 7 files changed, 278 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 init.sh create mode 100644 pigeon.sh create mode 100644 run.sh create mode 100644 vm.nix create mode 100644 webhook.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..cd19318 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1791734076, + "narHash": "sha256-i5qk1kLiit98rRbh1AXmWBNT9QXC/y4l5Wk/u5EdpDQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "7fd4004d0511991fff3a0524856a9be09e28c5f0", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..40c8b74 --- /dev/null +++ b/flake.nix @@ -0,0 +1,15 @@ +{ + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = { self, nixpkgs }: + let + pkgs = import nixpkgs { + system = "x86_64-linux"; + }; + in { + packages.x86_64-linux.pigeon-kernel = pkgs.callPackage ./kernel/kernel.nix { }; + packages.x86_64-linux.pigeon-rootfs = pkgs.callPackage ./rootfs/rootfs.nix { + pigeon-toolchain = with pkgs; [ bash coreutils gcc gnumake util-linux ]; + }; + }; +} diff --git a/init.sh b/init.sh new file mode 100644 index 0000000..1cdbd8a --- /dev/null +++ b/init.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +trap 'sync ; echo 1 >/proc/sys/kernel/sysrq ; echo b >/proc/sysrq-trigger' EXIT +export PATH=/bin:/usr/bin HOME=/tmp/build TMPDIR=/tmp + +mount -t proc proc /proc +mount -t devtmpfs devtmpfs /dev +mount -t tmpfs -o mode=1777,size=64m tmp /tmp +mount -t ext4 -r /dev/vdb /root/student +mount -t ext4 -r /dev/vdc /root/grader +mount -t ext4 /dev/vdd /root/results + +mkdir -p /tmp/build +cp -a /root/student/. /tmp/build/ +cp -a --remove-destination /root/grader/build/. /tmp/build/ +chown -R 65534:65534 /tmp/build + +( + cd /tmp/build + ulimit -t 30 -u 256 -n 256 -f 2048 + setpriv --reuid=65534 --regid=65534 --clear-groups \ + timeout -s 9 30 make >/root/results/build.log 2>&1 + echo $? >/root/results/build.status +) + +[ "$(cat /root/results/build.status)" != 0 ] && exit + +for TEST in /root/grader/tests/*.sh ; do + NAME="$(basename "$TEST" .sh)" + timeout -s 9 10 "$TEST" 2>/dev/null >"/root/results/$NAME" +done diff --git a/pigeon.sh b/pigeon.sh new file mode 100644 index 0000000..d4d25a4 --- /dev/null +++ b/pigeon.sh @@ -0,0 +1,30 @@ +STUDENT_URL="$1" +PROJECT_ID="$2" +RELEASE_TAG="$3" +COMMIT_ID="$4" + +: ${PIGEON_GITLAB_TOKEN:?} +: ${PIGEON_GRADER_URL:?} + +DIRECTORY=`mktemp -d /tmp/pigeon.XXXXXXXXXX` +trap 'rm -rf "$DIRECTORY"' EXIT +cd "$DIRECTORY" + +git clone "$STUDENT_URL" student +git clone "$ASSIGNMENT_URL" assignment + +ASSIGNMENT="${STUDENT_URL:#*\.}" +cp -a "$TESTS"/"$ASSIGNMENT"/. "$DIRECTORY"/ +mkfs.ext4 -q -F -d "$STUDENT" 'student.img' 64M +mkfs.ext4 -q -F -d "$DIRECTORY" 'tests.img' 16M +mkfs.ext4 -q -F 'results.img' 16M + +@pigeon-vm@ student.img tests.img results.img + + + +curl --request PUT \ + --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \ + --header 'Content-Type: application/json' \ + --data "{\"description\": \"$RESULTS\"}" \ + --url "https://gitlab.com/api/v4/projects/$PROJECT_ID/releases/$TAG_NAME" diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..fe0ba90 --- /dev/null +++ b/run.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +cd /tmp/build +ulimit -t 1 -u 256 -n 256 -f 2048 +exec setpriv --reuid=65534 --regid=65534 --clear-groups timeout -s 9 2 ./program "$@" diff --git a/vm.nix b/vm.nix new file mode 100644 index 0000000..5c25c17 --- /dev/null +++ b/vm.nix @@ -0,0 +1,128 @@ +{ + pkgs, + pigeon-toolchain, + pigeon-vcpu-count ? 1, + pigeon-memory-size ? 512, + pigeon-disc-size ? "1G", + pigeon-kernel-params ? [ + "64BIT" "TTY" "SERIAL_8250" "SERIAL_8250_CONSOLE" "PRINTK" + + "HYPERVISOR_GUEST" "PARAVIRT" "KVM_GUEST" + + "BLOCK" "VIRTIO_MENU" "VIRTIO" "VIRTIO_PCI" "VIRTIO_MMIO" "VIRTIO_BLK" + "EXT4_FS" "SHMEM" "TMPFS" "PROC_FS" "PROC_SYSCTL" "SYSFS" "DEVTMPFS" + + "ACPI" "PCI" "PCI_MSI" "MAGIC_SYSRQ" + + "BINFMT_ELF" "BINFMT_SCRIPT" "MULTIUSER" "FUTEX" "EPOLL" "EVENTFD" + "SIGNALFD" "TIMERFD" "POSIX_TIMERS" "FILE_LOCKING" "ADVISE_SYSCALLS" + "NET" "UNIX" + ] +}: + +let + kernel = pkgs.stdenv.mkDerivation { + inherit (pkgs.linux_6_1) src version; + pname = "pigeon-kernel"; + + nativeBuildInputs = with pkgs; [ bc bison elfutils flex perl ]; + hardeningDisable = [ "all" ]; + enableParallelBuilding = true; + + configurePhase = '' + runHook preConfigure + patchShebangs scripts tools + make tinyconfig + for OPTION in ${toString pigeon-kernel-params}; do + scripts/config --enable $OPTION + done + make olddefconfig + for OPTION in ${toString pigeon-kernel-config}; do + grep -qx "CONFIG_$OPTION=y" .config || exit 1 + done + runHook postConfigure + ''; + + buildFlags = [ "vmlinux" ]; + + installPhase = '' + runHook preInstall + cp vmlinux "$out" + runHook postInstall + ''; + }; + + toolchain = pkgs.buildEnv { name = "pigeon-toolchain"; paths = pigeon-toolchain; }; + closure = pkgs.closureInfo { rootPaths = [ toolchain ]; }; + rootfs = pkgs.runCommand "pigeon-rootfs" { nativeBuildInputs = [ pkgs.e2fsprogs ]; } '' + mkdir -p rootfs && cd rootfs + mkdir -p dev proc sys tmp usr nix/store root/student root/grader root/results + chmod 700 root + + for FILE in $(cat ${closure}/store-paths); do + cp -a "$FILE" nix/store + done + ln -s ${toolchain}/bin bin + ln -s ${toolchain}/bin usr/bin + install -m 755 ${./init.sh} init + install -m 755 ${./run.sh} run + + cd .. + mkfs.ext4 -q -F -d rootfs $out ${pigeon-disc-size} + ''; + + vmconfig = pkgs.writeText "pigeon-vm.json" (builtins.toJSON { + boot-source = { + kernel_image_path = "${kernel}"; + boot_args = "reboot=k panic=1 quiet init=/init nomodule 8250.nr_uarts=0 i8042.noaux i8042.nomux i8042.dumbkbd swiotlb=noforce"; + }; + drives = [ + { + drive_id = "rootfs"; + path_on_host = "${rootfs}"; + is_root_device = true; + is_read_only = true; + } + { + drive_id = "student"; + path_on_host = ""; + is_root_device = false; + is_read_only = true; + } + { + drive_id = "grader"; + path_on_host = ""; + is_root_device = false; + is_read_only = true; + } + { + drive_id = "results"; + path_on_host = ""; + is_root_device = false; + is_read_only = false; + } + ]; + machine-config = { + vcpu_count = pigeon-vcpu-count; + mem_size_mib = pigeon-memory-size; + }; + }); +in + pkgs.writeShellApplication { + name = "pigeon-vm"; + runtimeInputs = with pkgs; [ coreutils firecracker jq ]; + text = '' + [ "$#" -ne 3 ] && exit 1 + + CONFIG="$(mktemp)" + trap 'rm -rf "$CONFIG"' EXIT + + jq --arg s "$1" --arg g "$2" --arg r "$3" ' + (.drives[] | select(.drive_id == "student").path_on_host) = $s | + (.drives[] | select(.drive_id == "grader").path_on_host) = $g | + (.drives[] | select(.drive_id == "result").path_on_host) = $r + ' ${vmconfig} >"$CONFIG" + + firecracker --no-api --config-file "$CONFIG" + ''; + } diff --git a/webhook.nix b/webhook.nix new file mode 100644 index 0000000..5f2bb88 --- /dev/null +++ b/webhook.nix @@ -0,0 +1,42 @@ +{ pkgs }: + +let + hooks = pkgs.writeText "pigeon-hooks.json" (builtins.toJSON [{ + id = "release"; + execute-command = "{./pigeon.sh}"; + include-command-output-in-response = false; + pass-arguments-to-command = [ + { source = "payload"; name = "project.git_http_url"; } + { source = "payload"; name = "project.id"; } + { source = "payload"; name = "tag"; } + { source = "payload"; name = "commit.id"; } + ]; + trigger-rule.and = [ + { + match = { + type = "value"; + parameter = { source = "header"; name = "X-Gitlab-Event"; }; + value = "Release Hook"; + }; + match = { + type = "value"; + parameter = { source = "payload"; name = "action"; }; + value = "create"; + }; + match = { + type = "value"; + parameter = { source = "header"; name = "X-Gitlab-Token"; }; + value = "{{ getenv `PIGEON_GITLAB_TOKEN` | js }}"; + }; + } + ]; + }]); +in + pkgs.writeShellApplication { + name = "pigeon"; + runtimeInputs = [ pkgs.webhook ]; + text = '' + : ''${PIGEON_GITLAB_TOKEN:?} + exec webhook -hooks ${hooks} -template -port "''${PIGEON_PORT:-9000}" -verbose + ''; + }