//go:build cgo package backend /* #include #include #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 }