commit 8b75509e4b02afa8965d2b25a829398b4b3e400c Author: Lilac Date: Thu Oct 15 20:33:59 2026 -0400 initial commit 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..f9e88c9 --- /dev/null +++ b/flake.nix @@ -0,0 +1,100 @@ +{ + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = { self, nixpkgs }: + let + pkgs = nixpkgs.legacyPackages.x86_64-linux; + + toolchain = pkgs.buildEnv { + name = "pigeon-toolchain"; + paths = with pkgs; [ bash coreutils gcc gnumake util-linux ]; + }; + closure = pkgs.closureInfo { + rootPaths = [ toolchain ]; + }; + rootfs = pkgs.runCommand "pigeon-rootfs" { + nativeBuildInputs = with pkgs; [ e2fsprogs ]; + } '' + mkdir -p rootfs && cd rootfs + umask 022 + mkdir -p dev proc sys tmp usr nix/store root/student root/project root/results + chmod 777 tmp + + 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 + cp ${./init.sh} init + + cd .. + mkfs.ext4 -q -F -d rootfs $out "1G" + ''; + + 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" "MAGIC_SYSRQ" + "BINFMT_ELF" "BINFMT_SCRIPT" "MULTIUSER" "FUTEX" "EPOLL" "EVENTFD" + "SIGNALFD" "TIMERFD" "POSIX_TIMERS" "FILE_LOCKING" "ADVISE_SYSCALLS" + "NET" "UNIX" + ]; + 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 kernel-params} ; do + scripts/config --enable "$OPTION" + done + make olddefconfig + for OPTION in ${toString kernel-params} ; do + grep -qx "CONFIG_$OPTION=y" .config || exit 1 + done + runHook postConfigure + ''; + + buildFlags = [ "vmlinux" ]; + + installPhase = '' + runHook preInstall + cp vmlinux "$out" + runHook postInstall + ''; + }; + + pigeon = pkgs.writeShellApplication { + name = "pigeon"; + runtimeInputs = with pkgs; [ coreutils curl e2fsprogs firecracker git ]; + text = builtins.replaceStrings + [ "@kernel@" "@rootfs@" ] [ "${kernel}" "${rootfs}"] + (builtins.readFile ./pigeon.sh); + }; + + hooks = pkgs.writeText "pigeon-hooks.json" + (builtins.replaceStrings [ "@pigeon@" ] [ "${pigeon}" ] + (builtins.readFile ./hooks.json)); + + pigeon-webhook = pkgs.writeShellApplication { + name = "pigeon-webhook"; + runtimeInputs = with pkgs; [ webhook ]; + text = '' + : "''${PIGEON_GITLAB_SECRET:?}" + : "''${PIGEON_GITLAB_TOKEN:?}" + : "''${PIGEON_PROJECTS:?}" + exec webhook -hooks ${hooks} -template -port 3000 -verbose + ''; + }; + in { + packages.x86_64-linux.default = pigeon-webhook; + }; +} diff --git a/hooks.json b/hooks.json new file mode 100644 index 0000000..648009c --- /dev/null +++ b/hooks.json @@ -0,0 +1,39 @@ +[ + { + "id": "release", + "execute-command": "@pigeon@", + "include-command-output-in-response": false, + "pass-arguments-to-command": [ + { "source": "payload", "name": "project.id" }, + { "source": "payload", "name": "project.namespace" }, + { "source": "payload", "name": "project.name" }, + { "source": "payload", "name": "commit.id" }, + { "source": "payload", "name": "tag" } + ], + "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_SECRET` | js }}" + } + } + ] + } + } +] diff --git a/init.sh b/init.sh new file mode 100755 index 0000000..85c8c3d --- /dev/null +++ b/init.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +export PATH=/bin TMPDIR=/tmp +trap 'sync ; echo 1 >/proc/sys/kernel/sysrq ; echo b >/proc/sysrq-trigger' EXIT + +mount -t proc proc /proc +mount -t devtmpfs devtmpfs /dev +mount -t tmpfs tmp /tmp +mount -t ext4 -r /dev/vdb /root/student +mount -t ext4 -r /dev/vdc /root/project +mount -t ext4 /dev/vdd /root/results + +cp -a /root/student/. /tmp/ +cp -a --remove-destination /root/project/build/. /tmp/ +chmod -R 777 /tmp +cd /tmp + +( + ulimit -t 30 -u 256 -n 256 -f 2048 + timeout -s 9 30 \ + setpriv --reuid=1 --regid=1 --clear-groups --no-new-privs --inh-caps=-all \ + make >/root/results/build.log 2>&1 + echo $? >/root/results/build.status +) +[ "$(cat /root/results/build.status)" != 0 ] && exit 1 + +for TEST in /root/project/tests/*.sh ; do + ( + ulimit -t 2 -u 256 -n 256 -f 2048 + timeout -s 9 2 \ + setpriv --reuid=1 --regid=1 --clear-groups --no-new-privs --inh-caps=-all \ + "$TEST" >"/root/results/$(basename "$TEST" .sh)" 2>/dev/null + ) +done diff --git a/pigeon.sh b/pigeon.sh new file mode 100755 index 0000000..eee7fd1 --- /dev/null +++ b/pigeon.sh @@ -0,0 +1,59 @@ +ID="$1" +NAMESPACE="$2" +PROJECT="$3" +COMMIT="$4" +TAG="$5" + +: "${PIGEON_GITLAB_TOKEN:?}" +: "${PIGEON_PROJECTS:?}" + +DIRECTORY="$(mktemp -d /tmp/pigeon.XXXXXXXXXX)" +trap 'rm -rf "$DIRECTORY"' EXIT +cd "$DIRECTORY" + +git -c advice.detachedHead=false clone -q --rev "$COMMIT" --depth 1 \ + "https://oauth2:$PIGEON_GITLAB_TOKEN@gitlab.com/$NAMESPACE/$PROJECT.git" student/ +rm -rf student/.git + +mkdir -p project/tests project/build +cp -a "$PIGEON_PROJECTS/$PROJECT"/tests/*.sh project/tests/ +cp -a "$PIGEON_PROJECTS/$PROJECT"/build/. project/build/ + +mkfs.ext4 -q -F -d student student.img 16M +mkfs.ext4 -q -F -d project project.img 16M +mkfs.ext4 -q -F results.img 16M + +cat >vm.json <<'EOF' +{ + "boot-source": { + "kernel_image_path": "@kernel@", + "boot_args": "reboot=k panic=1 init=/init quiet nomodule i8042.noaux i8042.nomux i8042.dumbkbd swiotlb=noforce console=ttyS0" + }, + "drives": [ + { "drive_id": "rootfs", "path_on_host": "@rootfs@", "is_root_device": true, "is_read_only": true }, + { "drive_id": "student", "path_on_host": "student.img", "is_root_device": false, "is_read_only": true }, + { "drive_id": "project", "path_on_host": "project.img", "is_root_device": false, "is_read_only": true }, + { "drive_id": "results", "path_on_host": "results.img", "is_root_device": false, "is_read_only": false } + ], + "machine-config": { "vcpu_count": 1, "mem_size_mib": 512 } +} +EOF + +firecracker --no-api --level Error --config-file vm.json + +RESULT="" +TESTS="$PIGEON_PROJECTS/$PROJECT/tests" +for TEST in "$TESTS"/*.sh ; do + debugfs -R "cat /$(basename "$TEST" .sh)" results.img >scratch 2>/dev/null + if cmp scratch "$TESTS/$(basename "$TEST" .sh).expected" ; then + RESULT="${RESULT}PASS\n" + else + RESULT="${RESULT}FAIL: $(cat "$TESTS/$(basename "$TEST" .sh).hint")\n" + fi +done + +curl -s --show-error --out-null --request PUT \ + --header "PRIVATE-TOKEN: $PIGEON_GITLAB_TOKEN" \ + --header 'Content-Type: application/json' \ + --data "{\"description\": \"$RESULT\"}" \ + --url "https://gitlab.com/api/v4/projects/$ID/releases/$TAG"