fix(apiserver): reject a malformed rusage[] at job submit (bug-1744)

Gitea #47: `xcecli jobs submit -R "rusage[mem=100000000a0]"` was accepted
with a normal "Job <...> is submitted" reply. The apiserver only
length-checked resreq; the real PEG grammar ran later, asynchronously, in
the scheduler's core_build_job(), which logged a server-side WARN and
dropped the job -- it then sat PEND forever with no sign of why.

- libxce: xce_check_rusage_syntax(resreq, &errmsg) runs parse_rusage() on
  every rusage[...] clause (extracted the way section.peg does: up to the
  first ']'), so primary, compound and alternative requests are all
  covered. No resource manager needed, so select[]/order[] resource names
  are still only checked by the scheduler (the documented scope).
- apiserver/backend: ValidateResreqSyntax() cgo wrapper (resreq_check.go);
  the !cgo stub returns nil (no job can be submitted without CGO anyway).
- controllers/jobs.go preCheckJobRequest(): calls it after the length
  check via a package var, so a malformed request is refused up front.

Tests: lib test_check_rusage_syntax (RED before: the reported input was
accepted) and backend TestValidateResreqSyntax (RED before) through the
real grammar; controllers TestPreCheckJobRequest_RejectsMalformedResreq
checks the call site with an injected validator (written with the call
site, so no separate RED). libxce, apiserver backend (cgo) and controllers
tests pass. apiserver/middleware TestLoadJobs fails here and on the
baseline alike: it needs a live apiserver on :8080.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-09-27 03:27:32 +00:00
parent ac20b2441f
commit b970ab75b1
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
8 changed files with 209 additions and 0 deletions

View file

@ -185,6 +185,68 @@ int parse_rusage(xlist_t* rusages, char* data, char** errmsg)
return rc;
}
/* bug-1744: see resreq.h. Mirrors section.peg's own extraction -- a clause
* runs from "rusage[" to the first "]" -- so every rusage clause of a
* primary, compound or alternative request is checked exactly as the
* scheduler's parse_resreq() will later parse it. */
int xce_check_rusage_syntax(const char* resreq, char** errmsg)
{
const char* p;
if ((resreq == NULL) || (errmsg == NULL)) {
return -EINVAL;
}
*errmsg = NULL;
for (p = strstr(resreq, "rusage["); p != NULL;
p = strstr(p, "rusage[")) {
const char* start = p + strlen("rusage[");
const char* end = strchr(start, ']');
char* body;
const char* q;
xlist_t rusages;
int rc, i;
if (end == NULL) {
*errmsg = strdup("unterminated rusage[ clause");
return -EINVAL;
}
for (q = start; (q < end) && ((*q == ' ') || (*q == '\t')); q++)
;
if (q == end) { /* empty clause: parse_resreq_primary skips it too */
p = end + 1;
continue;
}
body = strndup(start, (size_t)(end - start));
if (body == NULL) {
return -ENOMEM;
}
xlist_init(&rusages, 4);
rc = parse_rusage(&rusages, body, errmsg);
for (i = 0; i < rusages.count; i++) {
rusage_value_t* v = (rusage_value_t*)xlist_get(&rusages, i);
kill_rusage_value(v);
FREEUP(v);
}
xlist_fini(&rusages);
free(body);
if (rc < 0) {
if (*errmsg == NULL) {
*errmsg = strdup("invalid rusage[] clause");
}
return rc;
}
p = end + 1;
}
return 1;
}
/* Depth pre-scan: count '!' and '(' as depth increments.
* Returns 0 if depth is within limit, -1 if exceeded. */
static int check_select_depth(const char* input, int max_depth)

View file

@ -1,3 +1,4 @@
#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
@ -481,11 +482,49 @@ void test_parse_section_span_not_in_select(void)
FREEUP(errmsg);
}
/* bug-1744: the apiserver's pre-submit check. Valid rusage clauses (and
* requests with none) pass; a malformed one -- the reported
* "rusage[mem=100000000a0]" -- is rejected with a message. */
void test_check_rusage_syntax(void)
{
const char* good[] = {
"rusage[mem=100]",
"select[type==any] rusage[mem=100, swp=2]",
"select[type==any] order[r15m]",
"",
"{rusage[mem=1]} || {rusage[mem=2]}",
};
const char* bad[] = {
"rusage[mem=100000000a0]",
"select[type==any] rusage[mem=]",
"{rusage[mem=1]} || {rusage[mem=x]}",
"rusage[mem=1",
};
char* errmsg;
size_t i;
for (i = 0; i < sizeof(good) / sizeof(good[0]); i++) {
errmsg = NULL;
TEST_ASSERT_EQUAL_INT_MESSAGE(1, xce_check_rusage_syntax(good[i], &errmsg),
good[i]);
TEST_ASSERT_NULL(errmsg);
}
for (i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
errmsg = NULL;
TEST_ASSERT_EQUAL_INT_MESSAGE(-EINVAL,
xce_check_rusage_syntax(bad[i], &errmsg),
bad[i]);
TEST_ASSERT_NOT_NULL_MESSAGE(errmsg, bad[i]);
free(errmsg);
}
}
void test_resreq(void)
{
printf("\n>> resreq test\n\n");
RUN_TEST(test_parse_section1);
RUN_TEST(test_check_rusage_syntax);
RUN_TEST(test_parse_section2);
RUN_TEST(test_parse_section_fail);
RUN_TEST(test_parse_section3);