Skip to content

Keep the codepage, the STC ACEE and the realm out of module storage (#197) - #218

Merged
mgrossmann merged 3 commits into
mainfrom
issue-197-module-storage-writes
Aug 21, 2026
Merged

Keep the codepage, the STC ACEE and the realm out of module storage (#197)#218
mgrossmann merged 3 commits into
mainfrom
issue-197-module-storage-writes

Conversation

@mgrossmann

@mgrossmann mgrossmann commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

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 both include/ directories — every file-scope object,
every function-local static, plus a hunt for in-place writes that could reach
a string literal — found exactly three, and no others.

# object store sites when
1 cfg_realm_buf (httpprm.c:83) httprlm.c:35, :43, :48 through out, and httpprm.c:392 strcpy http_config(), on both Parmlib paths
2 default_atoe / default_etoa / asc2ebc / ebc2asc httpxlat.c:415-420 http_xlate_init(), httpprm.c:156
3 stc_prev_acee httpd.c:135 stc_identity_restore(), on P HTTPD

#1 corrects the issue text. It runs at httpprm.c:139-141, before
http_xlate_init() at :156, so on current main it is the first to fail —
and it is written whether or not the Parmlib codes REALM (the strcpy when
it does, httprlm() when it does not). It did not show in the dump on the
issue because f4d094f predates dc6a3ce (REALM, #193) by a day. Fixing only
httpxlat.c would have moved the abend, not removed it.

Note that httpprm.c:140 is only the call: three of the four stores are in
httprlm.c, writing through its out parameter. httprlm.c is deliberately
project-header-free so it host-tests, so read on its own it looks entirely
innocent — grepping httpprm.c finds one writer where there are four.

Why not a __super() window

The 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 module
linked without AC(1), fetched from LNKLST, taking the same S0C4 — and
that 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 the
discriminator, the library the module is fetched from is.
AC(1), --norent
and a plain cc370 driver link all failed identically from LNKLST and all
worked through a private STEPLIB.

Changes

All three move into the HTTPD control block — a main() local, hence key-8
automatic storage whatever key the module is in.

  • Codepagehttpd->xlate (reuses unused_80, no offset moves).
    http_xlate_init() takes the block; it is not in HTTPX and has one caller.
  • STC ACEEhttpd->stc_prev_acee (reuses unused_88).
  • Realmhttpd->cfg_realm_val, 65 bytes appended, taking the block from
    0x140 to 0x188 (392 bytes — verified against cc370, not assumed). Safe:
    struct httpd is opaque in httpcgi.h, and the ABI assert in httpx.c pins
    the httpx and flag offsets, not the size. Still inline rather than
    malloc'd, so no allocation can fail on that path.

The 256-byte translation tables are only ever read and stay where they are.

asc2ebc / ebc2asc are gone. Their three consumers — httpdeco.c,
httpgets.c, dbgdump.c — and test/tstdeco.c now hoist the table out of the
loop through http_codepage(), which resolves the block via the CRT then GRT
anchors (the quiet counterpart of cgihttpd()) and answers CP037 when there is
no server context. Explicit rather than a #define shim, so the test says what
it actually pins.

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. 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 memset of the HTTPD block moves from
initialize() into main(): stc_identity() now stores into the block, and
the early goto quit paths reach stc_identity_restore() without calling
initialize() at all — with the memset where it was, those paths would have
tested uninitialised stack. And /.dsrv?target=HTTPD names the repurposed
slots (xlate, stc_prev_acee) plus the new realm value, so the field table
does not label live pointers with dead names.

httpprtv.c gets a comment on why UCHAR te[] = "..." is an array and not a
pointer — one character is the difference between a stack copy and
http_etoa() translating a literal in place, and it is the likeliest way this
bug class comes back.

CLAUDE.md gets the rule under Off-Limits, with the block size corrected
in both places it appears.

Second commit: HTTPD002I, which route authorized the STC

Added because the first MVS run exposed the gap: HTTPD reached
HTTPD001I READY on af164dd with 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.

HTTPD002I AUTHORIZED BY LIBRARY (MODULE KEY 0)
HTTPD002I AUTHORIZED BY SVC (MODULE KEY 8)

UFSD007I and FTPD008I are the same line 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.

Third commit: that message was wrong on its first run, and why

HTTPD002I reported AUTHORIZED BY SVC (MODULE KEY 8) on an mvsdev STC whose
module had demonstrably been fetched key 0 — the same library gave the S0C4
this PR fixes. The flag came from crt->crtopts, and crtopts is declared in
libc370's clibcrt.h:37 and tested in @@apfset.c:15 but assigned nowhere,
in libc370 or in any consumer. The CRT is zeroed, so CRTOPTS_AUTH is
permanently clear and a message built on it can only ever say SVC.

Now measured with __isauth() (one TESTAUTH) captured before the setup, which
is exactly what UFSD007I and FTPD008I do. Comparing the three side by side
also 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 dead
field. Because it is always clear, every start takes unauth_setup() — the only
path that calls identify_cthread(), and CTHREAD must be IDENTIFYed or
ATTACH EP=CTHREAD finds nothing, it being an ENTRY inside the 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. 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, binary is a flag not a counter);
usage[], help_text[]; vect / httpx; httpx in cgistart.c;
http_cp037 / http_cp1047 / http_legacy; wsakey in credarr.c (passed
by value to __wsaget()); every __stklen; the three
static const char *levels[] in httpcons.c. No writable definitions in either
include/ directory. All http_upcase / http_etoa / http_atoe /
http_xlate / httpdeco targets are stack arrays or httpc->buf.

One near miss, recorded so it is not re-derived: pgmname[8] = '\0' in
cgistart.c:230 looks like a store into the HTTPD block, because
httplink.c:27 sets plist[1] = httpc->httpd. It is not — __start is not
called positionally off httplink's plist. @@crt1.asm:173 passes the CRT's own
PARMLIST (ARGPTR/PGMNPTR/TYPE/PGMR1, :310-314), pgmname is
A(PGMNAME) in STACK DSECT over the GETMAIN at :55, and PGMNAMEN
(:316) is labelled "NUL BYTE FOR C" — reserved for exactly that store.
pgmr1 is R1 at entry, which is what the eyecatcher scan at
cgistart.c:117-141 consumes.

Verification

  • make clean from make clean, -Wall -Werror on the cc370 target build.
  • make test builds all 17 test modules; make test-host 113/113 assertions.
    Note those 113 do not exercise the new code: TSTDECO is the only test
    that reaches http_codepage(), and it is host = false, so its
    no-server-context fallback is compile-verified here and runs for real on the
    same make test-mvs as the rest.
  • Line endings preserved per file (the CRLF sources stay CRLF).
  • check-module-data.py now reports only __stklen, which is read by the CRT
    prologue and never stored.

Verified on MVS, all three stores, on a key-0 module. Run on mvsdev,
2026-08-21 02:51-02:52:

HTTPD000I HTTPD 4.0.0-DEV (E110305-DIRTY) STARTING
HTTPD002I AUTHORIZED BY LIBRARY (MODULE KEY 0)      <- the failure condition
HTTPD004I STC IDENTITY SET TO HTTPD/USER VIA RACINIT
...
HTTPD001I HTTPD 4.0.0-DEV READY - SERVING /wwwroot
...
HTTPD098I HTTPD SHUTTING DOWN
HTTPD416I STATS: 1 REQUESTS, 0 ERRORS, 6325 BYTES
HTTPD099I HTTPD SHUTDOWN COMPLETE

AUTHORIZED BY LIBRARY (MODULE KEY 0) is the whole point: the job step was
authorized 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.LINKLIB refused to start on the previous build for exactly that reason.

HTTPD002I is verified along with it: reading crtopts this same STC reported
AUTHORIZED BY SVC (MODULE KEY 8); reading __isauth() it reports
BY LIBRARY, agreeing with FTPD008I on the same system.

The E110305-DIRTY stamp is the working tree at the moment of that deploy —
the __isauth() fix was built and deployed before it was committed. Code
identical to 39a30c6, which has since been rebuilt and redeployed so the
staging 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) and CODEPAGE LEGACY.

Out of scope

Filed as mvslovers/libc370#122. This will not reach HTTPD001I READY on an APF LINKLIB by itself.
auth_setup() (httpd.c:1016) is an empty stub and is the branch taken when
crt->crtopts & CRTOPTS_AUTH — the JSCB AUTH bit, i.e. exactly the APF-fetched
case. unauth_setup() does auth_name() and identify_cthread();
auth_setup() does neither. HTTPD links startup = "crt1", which is @@crt0
with the CTHREAD IDENTIFY commented out (@@crt1.asm:169-171), and threads are
attached by entry-point name (@@ctcrtx.c:87, ATTACH EP=CTHREAD). Without
the IDENTIFY there is nothing to find. Prediction, not a measurement — the run
on the issue died in http_config(), well before the first
cthread_create(). libc370 has the identical gap in clib_apf_setup(), and
ufsd and ftpd link crt1 too, so it belongs there first — see
mvslovers/libc370#122 for the full trace.

tools/check-module-data.py is worth adopting as a CI job but not in its
current form: it reads each [[module]]'s sources, and HTTPD's name only
httpstrt.c, httpd.c, httpprm.c — so it finds #1 and #3 and misses #2
entirely, because httpxlat.c arrives by autocall from build/httpdint.a.

https://claude.ai/code/session_015E3cWm6a5ZV1jFd4sYginu

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
@mgrossmann
mgrossmann merged commit 2c9cef7 into main Aug 21, 2026
1 check passed
@mgrossmann
mgrossmann deleted the issue-197-module-storage-writes branch August 21, 2026 07:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

stc_identity_restore() stores into module storage in key 8 -- S0C4 on P HTTPD with an APF-authorized LINKLIB

1 participant