Keep the codepage, the STC ACEE and the realm out of module storage (#197) - #218
Merged
Conversation
Fetched from an APF-authorized or LNKLST library a load module lands in key-0 storage while the program runs problem state key 8, so any store into the module's own storage is a protection exception. HTTPD had three: - http_xlate_init() assigned default_atoe/default_etoa/asc2ebc/ebc2asc. This is the measured S0C4, in http_config() during startup. - The REALM keyword and the httprlm() fallback both wrote cfg_realm_buf, a file-scope buffer in httpprm.c. It runs BEFORE http_xlate_init(), so on current main it is the first to fail, and it is written on both Parmlib paths -- the strcpy when REALM is coded, httprlm() when it is not. Three of its four store sites are in httprlm.c, writing through the out parameter. - stc_identity_restore() cleared stc_prev_acee outside the __super() window that covers the matching store in stc_identity(), so P HTTPD ended S0C4 instead of a clean shutdown. All three move into the HTTPD control block, which is a main() local and therefore key-8 automatic storage whatever key the module is in. The codepage pair and the ACEE reuse unused_80 and unused_88, so no offset moves; the 65-byte realm value appends and takes the block to 0x188. The translation tables themselves are only ever read and stay put. A __super() window would not have been a general fix: libc370's measured table (doc/consumer-notes.md) shows a module linked without AC(1) taking the same S0C4 from LNKLST, and that module cannot switch keys at all. It is the library the module is fetched from that decides, not AC(1) and not the RENT attribute. The asc2ebc/ebc2asc globals are gone. Their three consumers -- httpdeco, httpgets, dbgdump -- and TSTDECO now hoist the table out of the loop via http_codepage(), which resolves the HTTPD block through the CRT/GRT anchors. That also fixes a latent wrong answer: a CGI module's autocalled copy of http_etoa() read its own uninitialised default_etoa and was therefore always CP037 whatever the Parmlib configured. Two supporting changes. The memset of the HTTPD block moves from initialize() into main(), because stc_identity() now stores into it and the early goto-quit paths reach stc_identity_restore() without calling initialize() at all. And /.dsrv?target=HTTPD names the repurposed slots, so the field table does not label live pointers with dead names. Not yet verified on MVS: reproducing this needs an APF-authorized LINKLIB, which nothing host-side or in make test-* can stand in for. Fixes #197 Claude-Session: https://claude.ai/code/session_015E3cWm6a5ZV1jFd4sYginu
The load module behaves differently depending on how the STC got APF authorization, and nothing in the log said which route it took. Authorized by library means the job step was already authorized when program fetch ran, so MVS took the job pack area in subpool 252 key 0 and HTTPD's own module storage is read-only to it -- the condition behind #197. Authorized by SVC means SVC 244 ran after fetch, too late to relabel storage, so the module stays key 8 and the same build works whether or not the stores it makes are legal. Without the line a successful start proves nothing about #197: it may simply have taken the SVC route, where the old code was fine. UFSD007I and FTPD008I are the same message in the other two servers; this brings httpd in line. The flag reuses the value the auth_setup/unauth_setup branch already tests, rather than a second __isauth(), so the reported route and the taken route cannot disagree. HTTPD002 was the free id in the startup cluster. Claude-Session: https://claude.ai/code/session_015E3cWm6a5ZV1jFd4sYginu
HTTPD002I reported AUTHORIZED BY SVC (MODULE KEY 8) on an mvsdev STC whose module was demonstrably fetched key 0 -- the same library gave an S0C4 on a module-storage store before this branch fixed those. The flag was read from crt->crtopts, which is declared in libc370's clibcrt.h and tested in @@apfset.c but never assigned anywhere in libc370 or in any consumer. The CRT is zeroed, so CRTOPTS_AUTH is permanently clear and any message built on it always says SVC. Use __isauth() instead, captured before the authorization setup, which is one TESTAUTH and is what UFSD007I and FTPD008I already do. Comparing the three also showed httpd claiming a route when the setup had FAILED, where ftpd suppresses the line; HTTPD002I now stays silent there, since HTTPD012E has already reported it and naming a route would claim an authorization the task does not hold. The auth_setup/unauth_setup branch deliberately keeps testing crtopts. Because it is always clear, every start takes unauth_setup(), which is the only path that calls identify_cthread() -- and CTHREAD must be IDENTIFYed or ATTACH EP=CTHREAD finds nothing, it being an ENTRY inside this load module rather than a library member. auth_setup() is an empty stub, so "correcting" that test would leave a by-library authorized server unable to create a single thread. The dead field is what has been protecting httpd, ufsd and ftpd; the comment says so at both halves. Claude-Session: https://claude.ai/code/session_015E3cWm6a5ZV1jFd4sYginu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #197.
What happens
Fetched from an APF-authorized or LNKLST library a load module lands in key-0
storage while the program runs problem state key 8, so any store into the
module's own storage is a protection exception. A full audit of
src/,credentials/src/and bothinclude/directories — every file-scope object,every function-local
static, plus a hunt for in-place writes that could reacha string literal — found exactly three, and no others.
cfg_realm_buf(httpprm.c:83)httprlm.c:35,:43,:48throughout, andhttpprm.c:392strcpyhttp_config(), on both Parmlib pathsdefault_atoe/default_etoa/asc2ebc/ebc2aschttpxlat.c:415-420http_xlate_init(),httpprm.c:156stc_prev_aceehttpd.c:135stc_identity_restore(), onP HTTPD#1 corrects the issue text. It runs at
httpprm.c:139-141, beforehttp_xlate_init()at:156, so on currentmainit is the first to fail —and it is written whether or not the Parmlib codes
REALM(thestrcpywhenit does,
httprlm()when it does not). It did not show in the dump on theissue because
f4d094fpredatesdc6a3ce(REALM, #193) by a day. Fixing onlyhttpxlat.cwould have moved the abend, not removed it.Note that
httpprm.c:140is only the call: three of the four stores are inhttprlm.c, writing through itsoutparameter.httprlm.cis deliberatelyproject-header-free so it host-tests, so read on its own it looks entirely
innocent — grepping
httpprm.cfinds one writer where there are four.Why not a
__super()windowThe issue proposed one as the narrow fix for #3. It does not generalise:
libc370's measured table (
doc/consumer-notes.md, 2026-08-06) has a modulelinked without
AC(1), fetched from LNKLST, taking the same S0C4 — andthat module cannot MODESET to key 0 at all, it would take S047. Key-0 module
storage does not imply the authorization to write it.
The same measurement corrects the issue's framing:
AC(1)is not thediscriminator, the library the module is fetched from is.
AC(1),--norentand a plain
cc370driver link all failed identically from LNKLST and allworked through a private STEPLIB.
Changes
All three move into the HTTPD control block — a
main()local, hence key-8automatic storage whatever key the module is in.
httpd->xlate(reusesunused_80, no offset moves).http_xlate_init()takes the block; it is not in HTTPX and has one caller.httpd->stc_prev_acee(reusesunused_88).httpd->cfg_realm_val, 65 bytes appended, taking the block from0x140 to 0x188 (392 bytes — verified against cc370, not assumed). Safe:
struct httpdis opaque inhttpcgi.h, and the ABI assert inhttpx.cpinsthe
httpxandflagoffsets, not the size. Still inline rather thanmalloc'd, so no allocation can fail on that path.
The 256-byte translation tables are only ever read and stay where they are.
asc2ebc/ebc2ascare gone. Their three consumers —httpdeco.c,httpgets.c,dbgdump.c— andtest/tstdeco.cnow hoist the table out of theloop through
http_codepage(), which resolves the block via the CRT then GRTanchors (the quiet counterpart of
cgihttpd()) and answers CP037 when there isno server context. Explicit rather than a
#defineshim, so the test says whatit actually pins.
That also fixes a latent wrong answer: a CGI module's autocalled copy of
http_etoa()read its own uninitialiseddefault_etoaand was thereforealways CP037 whatever the Parmlib configured. Freezing the globals at CP037 was
rejected for the same reason — under
CODEPAGE LEGACY/IBM1047,|,[and]differ, so URI decoding would have silently regressed.Two supporting changes. The
memsetof the HTTPD block moves frominitialize()intomain():stc_identity()now stores into the block, andthe early
goto quitpaths reachstc_identity_restore()without callinginitialize()at all — with the memset where it was, those paths would havetested uninitialised stack. And
/.dsrv?target=HTTPDnames the repurposedslots (
xlate,stc_prev_acee) plus the new realm value, so the field tabledoes not label live pointers with dead names.
httpprtv.cgets a comment on whyUCHAR te[] = "..."is an array and not apointer — one character is the difference between a stack copy and
http_etoa()translating a literal in place, and it is the likeliest way thisbug class comes back.
CLAUDE.mdgets the rule under Off-Limits, with the block size correctedin both places it appears.
Second commit: HTTPD002I, which route authorized the STC
Added because the first MVS run exposed the gap: HTTPD reached
HTTPD001I READYonaf164ddwith the socket thread and three workers up —and that proved nothing, because the log never said whether the module had been
fetched key 0 or key 8. On the SVC route the pre-fix code was fine too.
UFSD007IandFTPD008Iare the same line in the other two servers; thisbrings httpd in line. The flag reuses the value the
auth_setup()/unauth_setup()branch already tests rather than a second__isauth(), so thereported route and the taken route cannot disagree.
Third commit: that message was wrong on its first run, and why
HTTPD002I reported
AUTHORIZED BY SVC (MODULE KEY 8)on an mvsdev STC whosemodule had demonstrably been fetched key 0 — the same library gave the S0C4
this PR fixes. The flag came from
crt->crtopts, andcrtoptsis declared inlibc370's
clibcrt.h:37and tested in@@apfset.c:15but assigned nowhere,in libc370 or in any consumer. The CRT is zeroed, so
CRTOPTS_AUTHispermanently clear and a message built on it can only ever say SVC.
Now measured with
__isauth()(one TESTAUTH) captured before the setup, whichis exactly what
UFSD007IandFTPD008Ido. Comparing the three side by sidealso showed httpd naming a route when the setup had failed, where ftpd
suppresses the line — HTTPD002I is now silent there too, since HTTPD012E has
already reported it.
The
auth_setup()/unauth_setup()branch deliberately keeps testing the deadfield. Because it is always clear, every start takes
unauth_setup()— the onlypath that calls
identify_cthread(), and CTHREAD must be IDENTIFYed orATTACH EP=CTHREADfinds nothing, it being an ENTRY inside the load modulerather than a library member.
auth_setup()is an empty stub, so "correcting"that test would leave a by-library authorized server unable to create a single
thread. Written up as mvslovers/libc370#122, which is now about the dead field
being a booby trap rather than about a live bug.
Audit: what was checked and cleared
mimes[]/default_mime(read-only,binaryis a flag not a counter);usage[],help_text[];vect/httpx;httpxincgistart.c;http_cp037/http_cp1047/http_legacy;wsakeyincredarr.c(passedby value to
__wsaget()); every__stklen; the threestatic const char *levels[]inhttpcons.c. No writable definitions in eitherinclude/directory. Allhttp_upcase/http_etoa/http_atoe/http_xlate/httpdecotargets are stack arrays orhttpc->buf.One near miss, recorded so it is not re-derived:
pgmname[8] = '\0'incgistart.c:230looks like a store into the HTTPD block, becausehttplink.c:27setsplist[1] = httpc->httpd. It is not —__startis notcalled positionally off httplink's plist.
@@crt1.asm:173passes the CRT's ownPARMLIST(ARGPTR/PGMNPTR/TYPE/PGMR1,:310-314),pgmnameisA(PGMNAME)inSTACK DSECTover theGETMAINat:55, andPGMNAMEN(
:316) is labelled "NUL BYTE FOR C" — reserved for exactly that store.pgmr1is R1 at entry, which is what the eyecatcher scan atcgistart.c:117-141consumes.Verification
makeclean frommake clean,-Wall -Werroron the cc370 target build.make testbuilds all 17 test modules;make test-host113/113 assertions.Note those 113 do not exercise the new code: TSTDECO is the only test
that reaches
http_codepage(), and it ishost = false, so itsno-server-context fallback is compile-verified here and runs for real on the
same
make test-mvsas the rest.check-module-data.pynow reports only__stklen, which is read by the CRTprologue and never stored.
Verified on MVS, all three stores, on a key-0 module. Run on mvsdev,
2026-08-21 02:51-02:52:
AUTHORIZED BY LIBRARY (MODULE KEY 0)is the whole point: the job step wasauthorized when program fetch ran, so the module's own storage is key 0 and
every store this PR moved would have abended S0C4 before it. The same
HTTPD.LINKLIBrefused to start on the previous build for exactly that reason.http_config()settles it before the listeneropens, so
HTTPD001Icannot appear without it.http_xlate_init()ran, and the one requestserved (6325 bytes, 0 errors) went through the translation tables for real.
P HTTPDcompleted:HTTPD098I→ socket threadand all three workers down →
HTTPD099I→IEF404I ENDED. No S0C4, and noRAKF0005/RAKF000A, which is the positive signal thatstc_identity_restore()actually put the STC account back before SVC 244released the authorization.
HTTPD002I is verified along with it: reading
crtoptsthis same STC reportedAUTHORIZED BY SVC (MODULE KEY 8); reading__isauth()it reportsBY LIBRARY, agreeing withFTPD008Ion the same system.The
E110305-DIRTYstamp is the working tree at the moment of that deploy —the
__isauth()fix was built and deployed before it was committed. Codeidentical to
39a30c6, which has since been rebuilt and redeployed so thestaging library carries a reproducible stamp.
Not covered by this run, and cheap to add if wanted: a
REALM-coded Parmlib(the run above took the
httprlm()default path) andCODEPAGE LEGACY.Out of scope
Filed as mvslovers/libc370#122. This will not reach
HTTPD001I READYon an APF LINKLIB by itself.auth_setup()(httpd.c:1016) is an empty stub and is the branch taken whencrt->crtopts & CRTOPTS_AUTH— the JSCB AUTH bit, i.e. exactly the APF-fetchedcase.
unauth_setup()doesauth_name()andidentify_cthread();auth_setup()does neither. HTTPD linksstartup = "crt1", which is@@crt0with the CTHREAD IDENTIFY commented out (
@@crt1.asm:169-171), and threads areattached by entry-point name (
@@ctcrtx.c:87,ATTACH EP=CTHREAD). Withoutthe IDENTIFY there is nothing to find. Prediction, not a measurement — the run
on the issue died in
http_config(), well before the firstcthread_create(). libc370 has the identical gap inclib_apf_setup(), andufsd and ftpd link
crt1too, so it belongs there first — seemvslovers/libc370#122 for the full trace.
tools/check-module-data.pyis worth adopting as a CI job but not in itscurrent form: it reads each
[[module]]'ssources, and HTTPD's name onlyhttpstrt.c,httpd.c,httpprm.c— so it finds #1 and #3 and misses #2entirely, because
httpxlat.carrives by autocall frombuild/httpdint.a.https://claude.ai/code/session_015E3cWm6a5ZV1jFd4sYginu