myxce/.wolf/bugs/bug-1781.md
Rongsong Shen d26ff8870d
feat(buglog): one Markdown record per bug, checked against a record standard
The buglog was one 27K-line JSON file whose fields covered only symptom,
root cause and fix, as free text: 2% of entries named the code version a
bug was found on, 12% the build profile, 36% a way to reproduce, and 58% of
the file was OpenWolf hook auto-detections ("Missing guard clause",
"Function not marked async" on .md files).

- .claude/skills/buglog/bug-record.md: the record standard. Eight
  sections (Summary as the title, Description, Environment, Reproduction;
  Root cause, Fix, Verification, References once fixed), each with the
  content it must contain -- e.g. Environment needs the OS, the libc the
  failing binary loads (from its ELF interpreter: glibc 2.42 in the nix
  shell vs 2.40 on the host), toolchain, build profile and commit.
- .wolf/bugs/<id>.md: 500 records migrated from buglog.json; every field
  verified to round-trip. They carry legacy: true and an explicit "not
  recorded" where the old log had no such field; component derived from
  file paths, severity from keywords (else unknown), fix_commits from
  hashes in status_note/fix that git resolves (91 of 463 fixed).
- .wolf/buglog-archive.json: the 710 hook auto-detections, verbatim.
- .wolf/buglog.json is now an index generated from the records, keeping
  the fields OpenWolf's hooks and `openwolf bug search` read.
- scripts/buglog.py rewritten: new (template + Environment collected from
  the machine and --binary), check, status (refuses a status the record
  doesn't support; fixed needs Root cause, Fix, Verification and
  --commit hashes git resolves), note, recur, set, show, list, triage,
  env, index. 29 unittest cases in scripts/tests/test_buglog.py.
- scripts/git-hooks/pre-commit runs `check --staged`: incomplete staged
  records, a legacy record hand-changed to fixed without the fix-time
  sections, a new record claiming legacy: true, or a stale index refuse
  the commit.
- scripts/gitea-sync.py reads the records: issue body = the record's
  sections plus linked fix commits (origin remote, GITEA_COMMIT_REPO to
  override), and a body hash so an issue's text is refreshed when the
  record changes.
- bug-1866 rewritten to the full standard as the worked example.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 03:40:11 +00:00

41 lines
4.9 KiB
Markdown

---
id: "bug-1781"
status: "fixed"
status_updated: "2026-09-24"
status_note: ""
component: "lib"
severity: "unknown"
timestamp: "2026-09-07T08:35:00Z"
last_seen: "2026-09-19T09:00:00Z"
occurrences: 1
files: ["src/frontend/apiserver/backend/clib.go (ControlJob)", "src/lib/request.c (xce_jobctrl_req)", "src/lib/controller.capnp.h (JobControl_u_which)", "src/include/xce/types.h (xce_jobctrl_t)", "src/controller/job.c (job_ctrl)"]
tags: ["apiserver", "capnp", "clib.go", "job-control", "kill", "stop", "resume", "checkpoint", "union-discriminant", "undefined-behavior", "gitea-45", "live-verified"]
related: ["bug-1742"]
legacy: true
gitea_issue: 45
gitea_synced_status: "fixed"
---
# bug-1781: Gitea issue #45 REOPENED after bug-1742's fix
## Description
Gitea issue #45 REOPENED after bug-1742's fix. User-reproduced: after bug-1742 fixed the earlier panic/empty-error-body symptom, `xcecli jobs kill <jobid>` (and stop/resume/checkpoint) now fails with a real, non-empty error instead: `request to /xce/v1/jobs/<id>/control failed: [failed to write data: xcecli_jobctrl failed: -22]` (-22 = -EINVAL). Reproduced live for kill/stop/resume/checkpoint, all failing.
## Environment
_Not recorded (migrated from buglog.json)._
## Reproduction
_Not recorded (migrated from buglog.json)._
## Root cause
ControlJob() (clib.go) set xce_jobctrl_t.action to the job_ctrl_type_t action code (models.JOB_STOP=1, JOB_RESUME=2, JOB_KILL=3, JOB_CHKPNT=8, src/include/xce/types.h job_ctrl_type_t) -- but that field is actually the capnp JobControl_u_which UNION DISCRIMINANT (JobControl_u_siginfo=0, JobControl_u_ckptoption=1, src/lib/controller.capnp.h), a completely different enum that says which branch of xce_jobctrl_t.u (siginfo vs ckptoption) is populated. xce_jobctrl_req() (src/lib/request.c:2200) and the controller's job_ctrl() (src/controller/job.c:1972, for a job with no allocs yet) both switch on ctrinfo->action expecting the discriminant values 0/1, not the job_ctrl_type_t codes. Consequences per action: kill(3) and resume(2) never match 0 or 1, so xce_jobctrl_req's `default: return -EINVAL` fires client-side before the request is even sent to the controller -- this is the exact -22 in the bug report. checkpoint(8) hits the same default-EINVAL path, so checkpoint was ALSO silently broken (never reached the controller's real checkpoint-eligibility check). stop(1) is the worst case: it numerically collides with JobControl_u_ckptoption(1), so xce_jobctrl_req took the ckptoption branch and read `ctrinfo->u.ckptoption->method` through a pointer that was actually populated as `xce_signal_t*` (via jobctrl_set_siginfo, clib.go) -- a wrong-typed union-member read, undefined behavior in the apiserver's own address space, not merely a clean rejection. The `signo`-based dispatch already correctly threaded through job_ctrl()'s inner switch (SIGCONT->resume, SIGSTOP/SIGTSTP->stop, SIGKILL/SIGTERM/default->kill on the not-yet-started-job branch) proves the intended design: `action` is the union tag, and `u.siginfo->signo` is what actually distinguishes stop/resume/kill -- ControlJob() conflated the two enums when translating the caller's action into the wire struct.
## Fix
FIXED and LIVE-VERIFIED (2026-09-07). Added jobCtrlUnionDiscriminant(action int) int to convert_pure.go (pure Go, no CGO types, alongside resolveExecUser from bug-1780) -- returns JobControl_u_ckptoption(1) for JOB_CHKPNT, JobControl_u_siginfo(0) for JOB_STOP/JOB_RESUME/JOB_KILL. ControlJob() (clib.go) now sets `ctrl.action = C.int(jobCtrlUnionDiscriminant(action))` instead of the raw action code; the existing jobctrl_set_siginfo/jobctrl_set_ckptoption C helpers (already correct, only ever touched ctrl.u) are unchanged. TDD: TestJobCtrlUnionDiscriminant_StopResumeKillUseSiginfo and TestJobCtrlUnionDiscriminant_ChkptUsesCkptOption (convert_test.go), passing under CGO_ENABLED=0 go test (RED before the function existed). Verified with a full /xce-build apiserver asan --incremental (exit 0, apiserver/backend real-CGO suite green). LIVE-VERIFIED beyond static analysis (learning from bug-1742's own gap, which was static-analysis-only and left this exact defect in place): deployed a real single-node cluster (controller+scheduler+worker+apiserver) via deploy-ha.sh, submitted a job that stays PEND (unsatisfiable select[] so it never dispatches -- exercises job_ctrl()'s not-yet-started branch, the exact code path this bug hits, without needing job execution), and confirmed via the real built xcecli binary: `jobs kill` succeeds (job removed from listing, previously -22), `jobs stop`/`jobs resume` succeed with no apiserver crash (previously read through the wrong union member -- confirmed apiserver process survived and stayed responsive), and `jobs checkpoint` on an unstarted job now correctly gets a clean business-rule rejection ("cannot be <checkpoint>") instead of a spurious client-side -EINVAL.
## Verification
_Not recorded (migrated from buglog.json)._
## References
- Related: bug-1742
- Gitea: #45