myxce/src/frontend/apiserver/backend/wire_dispatch_support.go
Rongsong Shen e53c086ec5 fix(schema): rename struct Data -> XceData to unshadow capnp builtin blob type
controller.capnp declared `struct Data $C.mapname("xce_data_t")` (an
OPCode_get query-result tagged union) at file scope, which shadows
Cap'n Proto's builtin Data blob type for every later `: Data` field in
the same file. This silently miscompiled 7 fields across 6 structs as
a struct-pointer type instead of a raw byte blob: UpgradeConfigRequest
.sha256/.bytes, ReplConfigReply.sha256/.bytes, ReplConfigPushRequest
.sha256/.bytes, ReplEnvelope.payload, SnapshotTransfer's chunk
payload, WalCatchupEntry.payload/.batchPayloads, and CtrlHelloRequest
.key. Production C code had already routed around it for years via
hand-rolled raw capn_ptr manipulation (see request.c's own comments);
nothing before now needed the generated codec/accessors to actually
work for these fields.

Renamed the local struct to XceData (capnp-level name only; the
$C.mapname("xce_data_t") C typedef is unchanged) and explicitly
repointed getdata/pushmsg at XceData by name, since those two fields
correctly want the local tagged union. Updated every derived
generated-identifier reference across 19 hand-written files spanning
controller, lib, scheduler, and the Go apiserver/xcecli CGo bindings.
Fixed 5 call sites in lib/request.c that had been typed against the
old workaround alias but now legitimately receive the correct
capn_data type (a pure type-name swap -- Data_ptr/XceData_ptr and
capn_data share the identical {capn_ptr p} layout).

Verified via full rebuild+test: lib (1/1), controller (3/3, incl. the
job-array push regression tests), scheduler (15/15), Go apiserver +
xcecli (all packages green), and a direct pycapnp round-trip proving
UpgradeConfigRequest.sha256/.bytes now encode/decode correctly.

Bug: bug-1461 (.wolf/buglog.json). Unblocks admin-config-tool task 3.3.
2026-07-22 09:22:04 +00:00

193 lines
8.3 KiB
Go

//go:build cgo
package backend
/*
#include <stdlib.h>
#include <stdint.h>
#include "client.h"
#include "netio.h"
#include "request.h"
#include "controller.capnp.h"
// job-array-single-wal-record task 5.5: xce_data_t.data and xce_request_t.body
// are C unions -- cgo represents these as opaque byte arrays, not named
// fields, so Go code cannot write/read them directly. These test-support
// helpers mirror the established pattern this package already uses for the
// SAME problem (clib.go's jobctrl_set_siginfo/jobctrl_set_ckptoption,
// val_helpers.h's xce_val_t accessors) -- new test-support C code, no
// production file touched.
static void wire_test_data_set_job_array(xce_data_t *d, xce_job_submit_t *ja) {
d->data.job_array = ja;
}
static void wire_test_request_set_pushmsg(xce_request_t *r, xce_data_t *d) {
r->body_which = Request_body_pushmsg;
r->body.pushmsg = d;
}
static xce_data_t* wire_test_request_get_pushmsg(xce_request_t *r) {
return r->body.pushmsg;
}
static void wire_test_request_set_datamsg(xce_request_t *r, dataset_t *ds) {
r->body_which = Request_body_datamsg;
r->body.datamsg = ds;
}
*/
import "C"
import (
"fmt"
"unsafe"
)
// wire_dispatch_support.go backs wire_dispatch_test.go (job-array-single-
// wal-record task 5.5). It is NOT named *_test.go despite being test-only in
// purpose: the Go toolchain hard-rejects `import "C"` in any file whose name
// ends in _test.go ("use of cgo in test ... not supported", golang/go#13938
// -- confirmed against this repo's actual go toolchain source,
// cmd/go/internal/modindex/read.go). Every function here is unexported and
// never called by production code (RegisterToBackend, SubmitJob, etc.) --
// it compiles into the production apiserver binary whenever CGO is enabled
// (same as any other file in this package) but is inert dead code there,
// the standard accepted trade-off for driving a real CGO struct through a
// Go test. wire_dispatch_test.go itself imports nothing from "C" -- it only
// calls the plain-Go-signature wrappers below.
// wireDispatchBuildPush constructs the exact wire message
// apiserver_push_jobarray() (apiserver.c, task 5.2) sends -- OPCode_push /
// Request_body_pushmsg carrying one Data.jobArray descriptor -- and encodes
// it via the REAL C.xce_encode(), mirroring the C-side test's
// build_encoded_push() (test_job_array_wire_e2e.c, task 5.5) and
// test_request.c's test_extract_data_job_array_passes_through_intact (task
// 5.3b). Returns an opaque handle to the encoded io_packet_t (owned by the
// caller until wireDispatchDeliver consumes it) usable only with
// wireDispatchDeliver.
func wireDispatchBuildPush(baseJobID, arraySize uint64) (unsafe.Pointer, error) {
submit := (*C.job_submit_data_t)(C.calloc(1, C.size_t(unsafe.Sizeof(C.job_submit_data_t{}))))
submit.queue = C.CString("normal")
submit.user = C.CString("tester")
submit.resreq = C.CString("type == local")
submit.mintasks = 1
submit.maxtasks = 1
jobArray := (*C.xce_job_submit_t)(C.calloc(1, C.size_t(unsafe.Sizeof(C.xce_job_submit_t{}))))
jobArray.jobid = C.uint64_t(baseJobID)
jobArray.array_size = C.uint64_t(arraySize)
jobArray.submit = submit
data := (*C.xce_data_t)(C.calloc(1, C.size_t(unsafe.Sizeof(C.xce_data_t{}))))
data.table = C.XceData_data_jobArray
C.wire_test_data_set_job_array(data, jobArray)
var request C.xce_request_t
request.opcode = C.OPCode_push
C.wire_test_request_set_pushmsg(&request, data)
pkt := (*C.io_packet_t)(C.calloc(1, C.size_t(unsafe.Sizeof(C.io_packet_t{}))))
if rc := C.xce_encode(&request, pkt); rc != 1 {
C.free(unsafe.Pointer(pkt))
C.free(unsafe.Pointer(submit.queue))
C.free(unsafe.Pointer(submit.user))
C.free(unsafe.Pointer(submit.resreq))
C.free(unsafe.Pointer(submit))
C.free(unsafe.Pointer(jobArray))
C.free(unsafe.Pointer(data))
return nil, fmt.Errorf("xce_encode failed: rc=%d", int(rc))
}
// The hand-built C structs are no longer needed once encoded into pkt's
// wire bytes -- xce_decode() will build entirely fresh copies from that
// buffer. Free them now so this test doesn't leak.
C.free(unsafe.Pointer(submit.queue))
C.free(unsafe.Pointer(submit.user))
C.free(unsafe.Pointer(submit.resreq))
C.free(unsafe.Pointer(submit))
C.free(unsafe.Pointer(jobArray))
C.free(unsafe.Pointer(data))
return unsafe.Pointer(pkt), nil
}
// wireDispatchDeliver mirrors vtask_entry()'s (xcectx.c, static, not
// exported) push-handling glue: decode -> extract_data() (task 5.3b's real
// fix) -> reassemble into Request_body_datamsg exactly as vtask_entry()'s
// MOVE_PTR does -> call the REAL go_push_handler() (clib.go), the top-level
// CGO entry point xce_backend_handler calls from C, never exercised by any
// prior apiserver test.
//
// ds is allocated via C.calloc (C heap), never a Go stack/heap value,
// because it is embedded into decoded.body.datamsg and decoded is then
// handed to C -- storing a Go pointer inside Go memory passed to C violates
// cgo's pointer-passing rules and panics under the (default-enabled) cgo
// pointer checker.
func wireDispatchDeliver(pktHandle unsafe.Pointer) (int, error) {
pkt := (*C.io_packet_t)(pktHandle)
var decoded C.xce_request_t
if rc := C.xce_decode(pkt, &decoded); rc != 0 {
return 0, fmt.Errorf("xce_decode failed: rc=%d", int(rc))
}
if decoded.body_which != C.Request_body_pushmsg {
return 0, fmt.Errorf("decoded body_which = %d, want Request_body_pushmsg", decoded.body_which)
}
pushmsg := C.wire_test_request_get_pushmsg(&decoded)
if pushmsg == nil {
return 0, fmt.Errorf("decoded pushmsg is nil")
}
ds := (*C.dataset_t)(C.calloc(1, C.size_t(unsafe.Sizeof(C.dataset_t{}))))
ds.table = pushmsg.table
callnext := C.extract_data(ds, pushmsg)
if callnext == 0 {
C.free(unsafe.Pointer(ds))
return 0, fmt.Errorf("extract_data() returned 0 (reassembly incomplete) -- test harness broken, not exercising the bug class")
}
// FREEUP(decoded.body.pushmsg) equivalent: extract_data() already
// transferred/nulled the payload pointer inside pushmsg, so this is a
// safe shallow free of the now-hollow xce_data_t container -- mirrors
// vtask_entry() (xcectx.c) exactly.
C.free(unsafe.Pointer(pushmsg))
C.wire_test_request_set_datamsg(&decoded, ds)
// go_push_handler is defined as a plain (albeit //export-annotated) Go
// function in clib.go, in the SAME package -- called directly as a
// normal same-package Go function, not via C.go_push_handler (it has no
// extern declaration in this file's own cgo preamble; cgo symbol
// resolution is per-file, so C.go_push_handler here would fail to
// resolve even though both files are in the same package and both carry
// //go:build cgo). *C.xce_request_t from this file's preamble is the
// same underlying C type as clib.go's parameter (both derive from the
// same request.h/controller.capnp.h), so passing &decoded across the
// file boundary is type-correct.
// Capture ds.rows BEFORE calling go_push_handler: its internal
// `defer C.free_Request(req)` runs free_Request(&decoded), whose
// Request_body_datamsg case calls free_Dataset_ptr(&decoded.body.datamsg)
// -- free_Dataset_ptr() unconditionally free()s the dataset_t pointer it
// is given (confirmed by reading controller.capnp.c's generated bodies),
// even though free_Dataset() ITSELF is an empty no-op (never touches
// ->rows). So `ds` (the container) is freed automatically the instant
// go_push_handler returns -- ds is a dangling pointer after that call.
// Reading ds.rows afterward would be a use-after-free; separately
// free()-ing ds itself would be a double-free (this exact double-free,
// on the 16-byte dataset_t region, was caught by ASan -- bug-1442).
rows := ds.rows
rc := go_push_handler(&decoded, nil)
// go_push_handler already freed *ds (see above) -- only the payload
// xce_job_submit_t it pointed to still needs freeing: free_Dataset()
// never touches ->rows, and handleDataset()'s XceData_data_jobArray case
// (clib.go) never nulls d.rows after use (every element is its own
// independent Go-native copy via cJobArrayToModels, mirroring the C
// scheduler side's sched_handle_dataset() jobArray case). Freeing it now
// is safe since cJobArrayToModels already fully copied every field into
// Go-native models.JobInfo before go_push_handler returned.
if rows != nil {
jobArrayPtr := (*C.xce_job_submit_t)(rows)
C.free_JobSubmit_ptr(&jobArrayPtr)
}
return int(rc), nil
}