Merge pull request 'Reuse corpus' (#7) from reuse-corpus into main

Reviewed-on: https://git.ts.mattnite.net/mattnite/cairn/pulls/7
This commit is contained in:
Matthew Knight 2026-03-06 04:11:33 +00:00
commit 90d61f1a33
4 changed files with 113 additions and 11 deletions

View File

@ -60,6 +60,15 @@ runs:
set -eu set -eu
command -v zig >/dev/null 2>&1 || { echo "ERROR: zig not found in PATH. Install Zig before using this action (e.g. https://codeberg.org/mlugg/setup-zig@v2)."; exit 1; } command -v zig >/dev/null 2>&1 || { echo "ERROR: zig not found in PATH. Install Zig before using this action (e.g. https://codeberg.org/mlugg/setup-zig@v2)."; exit 1; }
command -v afl-cc >/dev/null 2>&1 || { echo "ERROR: afl-cc not found in PATH after setup-afl step."; exit 1; } command -v afl-cc >/dev/null 2>&1 || { echo "ERROR: afl-cc not found in PATH after setup-afl step."; exit 1; }
if ! command -v jq >/dev/null 2>&1; then
if command -v apt-get >/dev/null 2>&1; then
apt-get update -qq
apt-get install -y -qq jq >/dev/null 2>&1
else
echo "ERROR: jq not found and apt-get unavailable"
exit 1
fi
fi
echo "zig $(zig version), afl-cc found" echo "zig $(zig version), afl-cc found"
- name: Setup Cairn CLI - name: Setup Cairn CLI
@ -86,11 +95,16 @@ runs:
# ── Start Cairn campaign ── # ── Start Cairn campaign ──
SHORT_SHA=$(printf '%.8s' "${COMMIT}") SHORT_SHA=$(printf '%.8s' "${COMMIT}")
if [ -n "${TARGET}" ]; then
CAMPAIGN_NAME="fuzz-${TARGET}"
else
CAMPAIGN_NAME="fuzz-${SHORT_SHA}"
fi
CAMPAIGN_OUTPUT=$(cairn campaign start \ CAMPAIGN_OUTPUT=$(cairn campaign start \
-server "${CAIRN_SERVER}" \ -server "${CAIRN_SERVER}" \
-repo "${REPO}" \ -repo "${REPO}" \
-owner "${OWNER}" \ -owner "${OWNER}" \
-name "fuzz-${SHORT_SHA}" \ -name "${CAMPAIGN_NAME}" \
-type fuzzing) -type fuzzing)
CAMPAIGN_ID="${CAMPAIGN_OUTPUT#Campaign started: }" CAMPAIGN_ID="${CAMPAIGN_OUTPUT#Campaign started: }"
echo "Campaign ${CAMPAIGN_ID} started" echo "Campaign ${CAMPAIGN_ID} started"
@ -115,6 +129,18 @@ runs:
echo "Target ${TARGET_NUM}: zig build ${BUILD_ARGS}" echo "Target ${TARGET_NUM}: zig build ${BUILD_ARGS}"
echo "==========================================" echo "=========================================="
# Special-case Zig fuzz targets: if build args contain -Dfuzz-target=<name>,
# use that as the effective Cairn target for metadata/track keying.
LINE_FUZZ_TARGET=$(printf '%s' "${BUILD_ARGS}" | sed -n 's/.*-Dfuzz-target=\([^[:space:]]*\).*/\1/p')
EFFECTIVE_TARGET="${TARGET}"
if [ -n "${LINE_FUZZ_TARGET}" ]; then
EFFECTIVE_TARGET="${LINE_FUZZ_TARGET}"
fi
TRACK_SOURCE="${OWNER}/${REPO}|${EFFECTIVE_TARGET}|${BUILD_ARGS}"
TRACK_KEY=$(printf '%s' "${TRACK_SOURCE}" | sha256sum | awk '{print $1}')
echo "Track key: ${TRACK_KEY}"
# ── Build ── # ── Build ──
rm -rf zig-out rm -rf zig-out
zig build ${BUILD_ARGS} zig build ${BUILD_ARGS}
@ -142,11 +168,27 @@ runs:
echo "Fuzz binary: ${FUZZ_BIN}" echo "Fuzz binary: ${FUZZ_BIN}"
# ── Seed corpus ── # ── Seed corpus ──
SEEDS="afl-seeds-${TARGET_NUM}"
rm -rf "${SEEDS}"
mkdir -p "${SEEDS}"
if [ -n "${CORPUS_DIR}" ] && [ -d "${CORPUS_DIR}" ]; then if [ -n "${CORPUS_DIR}" ] && [ -d "${CORPUS_DIR}" ]; then
SEEDS="${CORPUS_DIR}" cp -a "${CORPUS_DIR}/." "${SEEDS}/" 2>/dev/null || true
else fi
SEEDS="afl-seeds"
mkdir -p "${SEEDS}" LATEST_CORPUS_ID=$(curl -fsSL "${CAIRN_SERVER}/api/v1/artifacts?type=fuzz&limit=200" \
| jq -r --arg track "${TRACK_KEY}" --arg repo "${REPO}" \
'.artifacts[]? | select(.repo_name == $repo and (.metadata.kind // "") == "corpus" and (.metadata.track_key // "") == $track) | .id' \
| head -n1)
if [ -n "${LATEST_CORPUS_ID}" ] && [ "${LATEST_CORPUS_ID}" != "null" ]; then
echo "Downloading prior corpus artifact: ${LATEST_CORPUS_ID}"
if cairn download -server "${CAIRN_SERVER}" -id "${LATEST_CORPUS_ID}" -o "prior-corpus-${TARGET_NUM}.tar.gz"; then
tar xzf "prior-corpus-${TARGET_NUM}.tar.gz" -C "${SEEDS}" || true
rm -f "prior-corpus-${TARGET_NUM}.tar.gz"
fi
fi
if [ "$(find "${SEEDS}" -maxdepth 1 -type f | wc -l)" -eq 0 ]; then
printf 'A' > "${SEEDS}/seed-0" printf 'A' > "${SEEDS}/seed-0"
fi fi
@ -192,11 +234,12 @@ runs:
echo "Uploading crash: ${CRASH_NAME}" echo "Uploading crash: ${CRASH_NAME}"
set -- -server "${CAIRN_SERVER}" -repo "${REPO}" -owner "${OWNER}" \ set -- -server "${CAIRN_SERVER}" -repo "${REPO}" -owner "${OWNER}" \
-commit "${COMMIT}" -type fuzz -file "${crash_file}" \ -commit "${COMMIT}" -campaign-id "${CAMPAIGN_ID}" -type fuzz -file "${crash_file}" \
-kind crash -track-key "${TRACK_KEY}" \
-crash-message "AFL++ crash (${BUILD_ARGS}): ${CRASH_NAME}" -crash-message "AFL++ crash (${BUILD_ARGS}): ${CRASH_NAME}"
if [ -n "${TARGET}" ]; then if [ -n "${EFFECTIVE_TARGET}" ]; then
set -- "$@" -target "${TARGET}" set -- "$@" -target "${EFFECTIVE_TARGET}"
fi fi
if [ -n "${SIG}" ]; then if [ -n "${SIG}" ]; then
set -- "$@" -signal "${SIG}" set -- "$@" -signal "${SIG}"
@ -216,9 +259,11 @@ runs:
tar czf "corpus-${TARGET_NUM}.tar.gz" -C "${QUEUE_DIR}" . tar czf "corpus-${TARGET_NUM}.tar.gz" -C "${QUEUE_DIR}" .
set -- -server "${CAIRN_SERVER}" -repo "${REPO}" -owner "${OWNER}" \ set -- -server "${CAIRN_SERVER}" -repo "${REPO}" -owner "${OWNER}" \
-commit "${COMMIT}" -type fuzz -file "corpus-${TARGET_NUM}.tar.gz" -commit "${COMMIT}" -campaign-id "${CAMPAIGN_ID}" -type fuzz \
if [ -n "${TARGET}" ]; then -kind corpus -track-key "${TRACK_KEY}" \
set -- "$@" -target "${TARGET}" -file "corpus-${TARGET_NUM}.tar.gz"
if [ -n "${EFFECTIVE_TARGET}" ]; then
set -- "$@" -target "${EFFECTIVE_TARGET}"
fi fi
cairn upload "$@" cairn upload "$@"

View File

@ -133,6 +133,20 @@ func TestCLIUploadAndDownloadRoundTrip(t *testing.T) {
serverURL, db, cleanup, _ := setupCLIServer(t, false) serverURL, db, cleanup, _ := setupCLIServer(t, false)
defer cleanup() defer cleanup()
if err := cmdCampaign("start", []string{
"-server", serverURL,
"-repo", "demo",
"-owner", "acme",
"-name", "seed-campaign",
"-type", "fuzz",
}); err != nil {
t.Fatalf("cmdCampaign start failed: %v", err)
}
var campaign models.Campaign
if err := db.First(&campaign).Error; err != nil {
t.Fatalf("querying campaign: %v", err)
}
artifactFile := filepath.Join(t.TempDir(), "artifact.bin") artifactFile := filepath.Join(t.TempDir(), "artifact.bin")
original := []byte("artifact bytes") original := []byte("artifact bytes")
if err := os.WriteFile(artifactFile, original, 0o644); err != nil { if err := os.WriteFile(artifactFile, original, 0o644); err != nil {
@ -144,10 +158,13 @@ func TestCLIUploadAndDownloadRoundTrip(t *testing.T) {
"-repo", "demo", "-repo", "demo",
"-owner", "acme", "-owner", "acme",
"-commit", "abcdef1234567890", "-commit", "abcdef1234567890",
"-campaign-id", strconv.FormatUint(uint64(campaign.ID), 10),
"-type", "fuzz", "-type", "fuzz",
"-file", artifactFile, "-file", artifactFile,
"-crash-message", "boom", "-crash-message", "boom",
"-signal", "11", "-signal", "11",
"-kind", "crash",
"-track-key", "track-123",
}) })
if err != nil { if err != nil {
t.Fatalf("cmdUpload failed: %v", err) t.Fatalf("cmdUpload failed: %v", err)
@ -165,6 +182,15 @@ func TestCLIUploadAndDownloadRoundTrip(t *testing.T) {
if got := md["signal"]; got != "11" { if got := md["signal"]; got != "11" {
t.Fatalf("expected metadata.signal=11, got %#v", got) t.Fatalf("expected metadata.signal=11, got %#v", got)
} }
if got := md["kind"]; got != "crash" {
t.Fatalf("expected metadata.kind=crash, got %#v", got)
}
if got := md["track_key"]; got != "track-123" {
t.Fatalf("expected metadata.track_key=track-123, got %#v", got)
}
if a.CampaignID == nil || *a.CampaignID != campaign.ID {
t.Fatalf("expected campaign_id=%d, got %#v", campaign.ID, a.CampaignID)
}
outFile := filepath.Join(t.TempDir(), "downloaded.bin") outFile := filepath.Join(t.TempDir(), "downloaded.bin")
if err := cmdDownload([]string{"-server", serverURL, "-id", strconv.FormatUint(uint64(a.ID), 10), "-o", outFile}); err != nil { if err := cmdDownload([]string{"-server", serverURL, "-id", strconv.FormatUint(uint64(a.ID), 10), "-o", outFile}); err != nil {

View File

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
) )
var version = "dev" var version = "dev"
@ -69,10 +70,13 @@ Upload flags:
-repo NAME Repository name (required) -repo NAME Repository name (required)
-owner OWNER Repository owner (required) -owner OWNER Repository owner (required)
-commit SHA Commit SHA (required) -commit SHA Commit SHA (required)
-campaign-id ID Campaign ID (optional)
-type TYPE Artifact type: coredump, fuzz, sanitizer, simulation (required) -type TYPE Artifact type: coredump, fuzz, sanitizer, simulation (required)
-file PATH Path to artifact file (required) -file PATH Path to artifact file (required)
-crash-message MSG Crash message (optional) -crash-message MSG Crash message (optional)
-stack-trace TRACE Stack trace text (optional) -stack-trace TRACE Stack trace text (optional)
-kind VALUE Artifact kind label (optional, stored in metadata)
-track-key VALUE Fuzz track key (optional, stored in metadata)
-signal VALUE Crash signal number/name (optional, stored in metadata) -signal VALUE Crash signal number/name (optional, stored in metadata)
-seed VALUE Simulation seed for reproducibility (optional, stored in metadata) -seed VALUE Simulation seed for reproducibility (optional, stored in metadata)
-target NAME Target name/platform (optional, stored in metadata) -target NAME Target name/platform (optional, stored in metadata)
@ -85,10 +89,13 @@ func cmdUpload(args []string) error {
repo string repo string
owner string owner string
commitSHA string commitSHA string
campaignID string
artifactType string artifactType string
filePath string filePath string
crashMessage string crashMessage string
stackTrace string stackTrace string
kind string
trackKey string
signal string signal string
seed string seed string
target string target string
@ -108,6 +115,9 @@ func cmdUpload(args []string) error {
case "-commit": case "-commit":
i++ i++
commitSHA = args[i] commitSHA = args[i]
case "-campaign-id":
i++
campaignID = args[i]
case "-type": case "-type":
i++ i++
artifactType = args[i] artifactType = args[i]
@ -120,6 +130,12 @@ func cmdUpload(args []string) error {
case "-stack-trace": case "-stack-trace":
i++ i++
stackTrace = args[i] stackTrace = args[i]
case "-kind":
i++
kind = args[i]
case "-track-key":
i++
trackKey = args[i]
case "-signal": case "-signal":
i++ i++
signal = args[i] signal = args[i]
@ -144,6 +160,13 @@ func cmdUpload(args []string) error {
"commit_sha": commitSHA, "commit_sha": commitSHA,
"type": artifactType, "type": artifactType,
} }
if campaignID != "" {
id, err := strconv.ParseUint(campaignID, 10, 64)
if err != nil {
return fmt.Errorf("invalid campaign id: %w", err)
}
meta["campaign_id"] = id
}
if crashMessage != "" { if crashMessage != "" {
meta["crash_message"] = crashMessage meta["crash_message"] = crashMessage
} }
@ -151,6 +174,12 @@ func cmdUpload(args []string) error {
meta["stack_trace"] = stackTrace meta["stack_trace"] = stackTrace
} }
md := map[string]any{} md := map[string]any{}
if kind != "" {
md["kind"] = kind
}
if trackKey != "" {
md["track_key"] = trackKey
}
if signal != "" { if signal != "" {
md["signal"] = signal md["signal"] = signal
} }

View File

@ -25,6 +25,7 @@ type IngestRequest struct {
Repository string `json:"repository"` Repository string `json:"repository"`
Owner string `json:"owner"` Owner string `json:"owner"`
CommitSHA string `json:"commit_sha"` CommitSHA string `json:"commit_sha"`
CampaignID *uint `json:"campaign_id,omitempty"`
Type string `json:"type"` Type string `json:"type"`
CrashMessage string `json:"crash_message,omitempty"` CrashMessage string `json:"crash_message,omitempty"`
StackTrace string `json:"stack_trace,omitempty"` StackTrace string `json:"stack_trace,omitempty"`
@ -93,6 +94,7 @@ func (h *IngestHandler) Create(c *gin.Context) {
artifact, err := models.CreateArtifact(ctx, h.DB, models.CreateArtifactParams{ artifact, err := models.CreateArtifact(ctx, h.DB, models.CreateArtifactParams{
RepositoryID: repo.ID, RepositoryID: repo.ID,
CommitID: commit.ID, CommitID: commit.ID,
CampaignID: req.CampaignID,
Type: req.Type, Type: req.Type,
BlobKey: blobKey, BlobKey: blobKey,
BlobSize: header.Size, BlobSize: header.Size,