Rewritten 2026-08-11. The first version proposed bracketing the ACEE inside httppcgi() around the LINK. That fix is wrong — or rather, insufficient in a way that matters. It closes the abend case and leaves the structural problem standing, and it would have sent the next reader down the same dead end. The corrected analysis is below; see also mvslovers/ftpd#64, which reached part of this earlier.
Surfaced while validating mvsmf#223 against the new storage reclaim. #154/#174 made a CGI abend survivable — the storage is released and the server keeps running. That is what made this visible: leftover ambient state used to be moot because the server was dead anyway.
The structural fact
racf_set_acee() does not set a task field. It writes ASXBSENV (ASXB+0xC8, libc370/src/racf/racsacee.c:15) — address-space scope. libc370 says so (racauth.c:116):
ASXBSENV is address-space-wide: writing the ACEE there and back is visible to every other TCB in the address space for the length of the call
And on MVS 3.8j there is no per-task alternative — the TCB has no ACEE anchor, and libc370 accordingly never references one (grep: zero hits repo-wide). This is the point that changes the conclusion: per-request identity is not merely unbracketed, it is being kept in the only field available, and that field belongs to the whole address space. There is nowhere better to put it.
Meanwhile httpd runs one TCB per session (ATTACH EP=CTHREAD), default MINTASK=3 (httpprm.c:200).
So: a save/restore of a shared field cannot be made correct by moving who performs it. Any fix that keeps switching is serialization under another name.
Two concrete defects that follow
1. Abend leaves the identity behind. mvsMF sets it in identity_middleware (mvsmf/src/mvsmf.c:51) and restores at quit: (:151). httppcgi() brackets the storage (reclaim(HTTP_CGI_SUBPOOL) at :72, unconditional since #174) but never touches the ACEE — zero hits for acee in src/httppcgi.c. Abend between the two lines and the client's ACEE stays in ASXBSENV, address-space-wide, indefinitely. The comment at httppcgi.c:55 already names the precondition: "nothing runs its @@ExitA, the worker TCB survives by design".
2. Interleaving between concurrent workers.
worker 1: old1 = set(A) ASXBSENV = A
worker 2: old2 = set(B) ASXBSENV = B, old2 = A
worker 1: set(old1) ASXBSENV = old1 <-- request B still running
worker 2: set(old2) ASXBSENV = A <-- a finished request's identity, left behind
Who is exposed, and how badly
racf_auth() serializes its own set/RACHECK/restore under lock(asxb) (libc370/src/racf/racauth.c:68,120,135,148), so any authorization decision made through it is race-free regardless of the above. That is the dividing line:
| Consumer |
Pattern |
Consequence |
| httpd |
passes httpc->cred->acee explicitly (httpxauth.c:143) |
safe |
| ftpd |
explicit racf_auth() pre-check + an unserialized switch on the OPEN path |
entitlement safe, spurious OPEN failures — ftpd#64 |
| mvsmf |
no racf_auth() call anywhere; plain fopen(dsname,"r") (dsapi.c:916) |
the ambient value is the authorization |
| httprexx / httplua |
never touch an ACEE (grep: 0 files each) |
inherit whatever is in ASXBSENV |
ftpd#64 asked for exactly this enumeration — "any fopen not preceded by a racf_auth pre-check would rely on the racy OPEN check directly — higher concern; enumerate such sites". mvsMF is that site, across the whole dataset API. Nobody had gone through it.
The API for the right fix already exists, with zero consumers
httpxauth.c:137 already exports, through the HTTPX vector:
int http_check_auth(HTTPC *httpc, const char *classname,
const char *resource, int attr);
It calls racf_auth(httpc->cred->acee, ...) — ACEE in the parameter list, no ambient dependency, self-serializing — and normalizes SAF rc 4 to 0 so the published contract holds (0 permitted, 8+ refused, -1 unauthenticated). Its own comment notes: "no CGI calls http_check_auth()".
So the export landed and nothing adopted it; modules kept switching identity instead.
Proposed direction
Not "httpd performs the switch instead of the module" — that was the first draft and it leaves the shared field shared. Rather:
No module touches an ACEE. httpd answers "may this client access resource X with attribute Y."
With that, this issue does not get fixed so much as it becomes moot: nobody can leave an identity behind, because nobody sets one.
The trade-off, stated plainly, because it is a real one. Today the gate is implicit and universal — every fopen() goes through RACHECK, including the ones nobody thought about, just under an identity drawn from a shared field. Afterwards the gate is explicit and correct, but only where it was written. If a module opens under the server's identity, a forgotten http_check_auth() is not a weak gate, it is no gate. Whether to keep the ambient switch as a second, implicit net — accepting everything above as the price — is the decision this issue needs before code moves.
Work implied
- mvsmf: adopt the pre-check model, drop the
racf_set_acee() pair. Its own ticket; the largest of the three and the only one carrying real security weight. The fopen sites in dsapi.c are the work list.
- ftpd#64: its "VERIFY FIRST — does OPEN consult TCBSENV" is answered, negatively. Noted there.
- first consumer of
http_check_auth(): per its comment, that is also the moment to verify the dbgf() path under DEBUG 1, which has never run from a CGI.
Scope of the evidence
Code reading, plus a live check of which build is running (/.dsrv?target=MOD shows no route->reclaim row, so the stand is post-#175). Neither the abend leftover nor the interleaving has been reproduced on a live system — that means deliberately abending a CGI, which is worth arranging rather than doing in passing.
Surfaced while validating mvsmf#223 against the new storage reclaim. #154/#174 made a CGI abend survivable — the storage is released and the server keeps running. That is what made this visible: leftover ambient state used to be moot because the server was dead anyway.
The structural fact
racf_set_acee()does not set a task field. It writes ASXBSENV (ASXB+0xC8,libc370/src/racf/racsacee.c:15) — address-space scope. libc370 says so (racauth.c:116):And on MVS 3.8j there is no per-task alternative — the TCB has no ACEE anchor, and libc370 accordingly never references one (grep: zero hits repo-wide). This is the point that changes the conclusion: per-request identity is not merely unbracketed, it is being kept in the only field available, and that field belongs to the whole address space. There is nowhere better to put it.
Meanwhile httpd runs one TCB per session (
ATTACH EP=CTHREAD), defaultMINTASK=3(httpprm.c:200).So: a save/restore of a shared field cannot be made correct by moving who performs it. Any fix that keeps switching is serialization under another name.
Two concrete defects that follow
1. Abend leaves the identity behind. mvsMF sets it in
identity_middleware(mvsmf/src/mvsmf.c:51) and restores atquit:(:151).httppcgi()brackets the storage (reclaim(HTTP_CGI_SUBPOOL)at:72, unconditional since #174) but never touches the ACEE — zero hits foraceeinsrc/httppcgi.c. Abend between the two lines and the client's ACEE stays in ASXBSENV, address-space-wide, indefinitely. The comment athttppcgi.c:55already names the precondition: "nothing runs its @@ExitA, the worker TCB survives by design".2. Interleaving between concurrent workers.
Who is exposed, and how badly
racf_auth()serializes its own set/RACHECK/restore underlock(asxb)(libc370/src/racf/racauth.c:68,120,135,148), so any authorization decision made through it is race-free regardless of the above. That is the dividing line:httpc->cred->aceeexplicitly (httpxauth.c:143)racf_auth()pre-check + an unserialized switch on the OPEN pathracf_auth()call anywhere; plainfopen(dsname,"r")(dsapi.c:916)ftpd#64 asked for exactly this enumeration — "any
fopennot preceded by aracf_authpre-check would rely on the racy OPEN check directly — higher concern; enumerate such sites". mvsMF is that site, across the whole dataset API. Nobody had gone through it.The API for the right fix already exists, with zero consumers
httpxauth.c:137already exports, through the HTTPX vector:It calls
racf_auth(httpc->cred->acee, ...)— ACEE in the parameter list, no ambient dependency, self-serializing — and normalizes SAF rc 4 to 0 so the published contract holds (0 permitted, 8+ refused, -1 unauthenticated). Its own comment notes: "no CGI callshttp_check_auth()".So the export landed and nothing adopted it; modules kept switching identity instead.
Proposed direction
Not "httpd performs the switch instead of the module" — that was the first draft and it leaves the shared field shared. Rather:
With that, this issue does not get fixed so much as it becomes moot: nobody can leave an identity behind, because nobody sets one.
The trade-off, stated plainly, because it is a real one. Today the gate is implicit and universal — every
fopen()goes through RACHECK, including the ones nobody thought about, just under an identity drawn from a shared field. Afterwards the gate is explicit and correct, but only where it was written. If a module opens under the server's identity, a forgottenhttp_check_auth()is not a weak gate, it is no gate. Whether to keep the ambient switch as a second, implicit net — accepting everything above as the price — is the decision this issue needs before code moves.Work implied
racf_set_acee()pair. Its own ticket; the largest of the three and the only one carrying real security weight. Thefopensites indsapi.care the work list.http_check_auth(): per its comment, that is also the moment to verify thedbgf()path underDEBUG 1, which has never run from a CGI.Scope of the evidence
Code reading, plus a live check of which build is running (
/.dsrv?target=MODshows noroute->reclaimrow, so the stand is post-#175). Neither the abend leftover nor the interleaving has been reproduced on a live system — that means deliberately abending a CGI, which is worth arranging rather than doing in passing.