Skip to content

Keep FTPD out of its own module storage (#101) - #108

Merged
mgrossmann merged 2 commits into
mainfrom
issue-101-apf-key0-statics
Aug 22, 2026
Merged

Keep FTPD out of its own module storage (#101)#108
mgrossmann merged 2 commits into
mainfrom
issue-101-apf-key0-statics

Conversation

@mgrossmann

@mgrossmann mgrossmann commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Fixes #101.

What happens

FTPD is link-edited AC(1). Fetched from an APF-authorized library the job step is already authorized when program fetch runs, so MVS obtains the job pack area in subpool 252, key 0 — authorized code must not be patchable by problem-key code. The STC runs problem state key 8, so any store into the module's own storage is a protection exception.

FTPD's first such store was the first statement of main(), which is why the reported JESMSGLG is one line:

12.34.30 STC 1124  IEF450I FTPD FTPD - ABEND S0C4 U0000 - TIME=12.34.30

No FTPD000I, no banner — the abend lands ahead of the first WTO and long before clib_apf_setup(). The SVC 244 route is unaffected: it sets JSCBAUTH after the fetch and cannot relabel storage program fetch already allocated, which is why a stock TK4-/TK5 never sees this.

Confirmed at assembler level. cc370 -S on the pre-fix src/ftpd.c:

* X-var ftpd_server
         ENTRY FTPD@SER
FTPD@SER EQU   *
         DC    F'0'
...
         L     2,=A(FTPD@SER)
         ST    4,0(2)              <-- the S0C4; R4 holds &server
         LA    1,88(,13)
         L     15,=V(@@GTCOM)      <-- __gtcom(), still before any WTO

One correction to the issue

The issue reads "ftpd_server is extern in include/ftpd.h:182 with ~25 references across three files, so threading it through parameters is the expensive option." That count is of ftpd_server_t, the type. The pointer itself is written once and never read:

src/ftpd.c:28:ftpd_server_t *ftpd_server = NULL;
src/ftpd.c:53:    ftpd_server = &server;
include/ftpd.h:182:extern ftpd_server_t *ftpd_server;

So the store that kills FTPD is a deletion, not a migration. The GRT anchor is still here — but it is load-bearing for the log/trace state, not for this.

Changes

  • ftpd_server deleted. The server is published in grt->grtapp1 as a dump anchor (no code reads it), matching httpd's grtapp1 = httpd.
  • Log level and trace ring state move into struct ftpd_server — a main() local, hence key 8 — published through grt->grtapp2 by ftpd_log_anchor(). The eleven-function API in ftpd#log.h is unchanged, so the 84 ftpd_log()/ftpd_trace() call sites are untouched. ftpd#log.c gains clibgrt.h and stays off ftpd.h: it has no business with sockets, RACF or the thread manager.
  • ftpd#xlt.c tables and pointers become const. Read-only today, but a codepage switch assigning asc2ebc is the same bug — it is what killed HTTPD in http_xlate_init (stc_identity_restore() stores into module storage in key 8 -- S0C4 on P HTTPD with an APF-authorized LINKLIB httpd#197). The pointer shape is kept deliberately: libc370 (src/dyn75/asc2ebc.c) and httpd export the same names as unsigned char *, and an array here would silently become garbage to anything declaring them the usual way. Nothing in libc370 references them today — both externs in @@75gabn.c / @@75ghba.c are unused — but the name is ecosystem-wide.
  • tools/check-module-data.py (from Keep the DD and session counters out of module storage (#64) ufsd#66, which found this) rejects mutable file-scope data and function-local statics in AC(1) modules, as its own CI job. Docstring and failure message point at ftpd's anchor rather than UFSD_STC.
  • Docs: an APF section in doc/installation.md explaining why the two authorization routes differ in storage key, a troubleshooting row for the bare S0C4, and the constraint in CLAUDE.md.

Why the GRT works from a worker thread

The one assumption the design rests on, checked rather than assumed. CTHREAD, the subtask driver in libc370/asm/@@crt1.asm, calls @@CRTSET — not @@GRTSET. @@CRTSET walks the PPA for the mother task's CLIBCRT (TCBOTC) and copies its crtgrt, so the GRT is one per process and inherited transitively: main → thdmgr dispatch thread → session workers. Independently, cthread_create_ex() fails a thread create outright when __grtget() returns NULL, so every level that successfully creates threads demonstrably has it.

Had this not held, ftpd_trace() from a worker would have become a silent no-op — trace enabled, TRACE DUMP showing only main's entries, nothing in error.

Two things a reviewer will ask about the anchor

Who else owns grtapp1/grtapp2. Both are free here, checked beyond ftpd's own sources: grep -rn grtapp finds nothing in ufsd's client/ (libufs is linked into FTPD and is designed to be linked into arbitrary consumers, so it is the one that could plausibly have claimed a slot) and nothing in libc370. httpd uses both, but httpd is a different address space. A collision would have been silent in both directions — trace going dead, or UFS commands failing as if UFSD were down.

The PPA lock. __grtget()__crtget() takes a shared PPA lock (lock(ppa,1)), so ftpd_log() and ftpd_trace() now take one where they read a static before. That is not new work on these paths: stdout is (*(stdio->___gtout()))__grtget(), so every printf() — including the one ftpd_log() ends in — already takes it, as does every errno access via __errno()__crtget(). The marginal cost is one extra shared acquisition on a path that already had at least one, and the two calls that can skip the work are cheap either way: there are exactly 2 ftpd_log(LOG_DEBUG, ...) sites and 10 ftpd_trace() sites, all per-command or per-connection, none in a per-block transfer loop. Nothing here goes near the exclusive-vs-shared hazard the comment four lines above the diff describes: that one is about main() calling __cibset() while a thread is starting, which is why the ordering it guards is untouched.

FTPD008I — which route authorized the STC

Both routes end in an authorized task and neither says a word on its own: clib_apf_setup() returns 0 whether it authorized us or found us already authorized, and libc370's diagnostics on that path are commented out. That silence is a good part of why this issue was hard to place — the route is exactly what decides whether the module's own storage is writable — so it now goes in the startup log next to the version banner:

FTPD008I AUTHORIZED BY LIBRARY (MODULE KEY 0)     APF list entry
FTPD008I AUTHORIZED BY SVC (MODULE KEY 8)         SVC 244, from RAKF

__isauth() (one TESTAUTH, no supervisor state) is asked before clib_apf_setup(), since that call destroys the distinction. The key is inferred from the route rather than measured — an authorized job step has its module fetched key 0, an unauthorized one key 8, and SVC 244 arrives too late to change either — and the installation guide says so, so nobody reads it as an ISK result.

Where authorization fails outright, FTPD003W already carries that and FTPD008I stays out of the way: one line about authorization per start, always. This is FTPD's counterpart to UFSD007I (mvslovers/ufsd#66).

One latent bug fixed on the way past

ftpd_trace_enable(1) set trace_on and then called ftpd_trace_init(), which resets it — so TRACE ON on an unallocated ring answered FTPD080I TRACE ENABLED with tracing off. Unreachable today because initialize() allocates the ring at startup; now allocate-then-enable, so it stays correct if that changes.

Exposure audit

FTPD's own module: the guard is clean across all 15 sources, and it is only a text proxy — it cannot tell a const pointer from a pointer to const. The real check is cc370 -S. Post-fix, ftpd.c has no X-var at all, ftpd#log.c has one =A(@V1) and it is a read of level_names[], and ftpd#xlt.c's ASC2EBC/EBC2ASC are DC A(@Vn) address constants resolved at load time with no runtime store. In the four ftpd_xlat_* loops the only store is STC 2,0(15,3) into the caller's buffer.

libufs.a, linked into FTPD and not covered by either repo's CI: scanned with this same detector, client/libufs.c has no module-resident data.

libc370, also linked in and out of reach of the guard. Its module-resident statics are real, so rather than inherit ufsd#66's hand audit I checked which members are actually in build/FTPD (ESD names, cp037):

Member Module data In FTPD Verdict
STRTOK static_old yes uses crt->crtstrtk when a CRT exists; the static is the no-CRT fallback
@@ERRNO static_errno yes same shape — crt->crterrno
MALLOC __lastsup yes inside #if USE_MEMMGR, which is off — __LASTSUP is not in the module
@@PERM, @@STDOUT FILE __perm[3] no stdout resolves via @@GTOUTgrt->grtout, on the heap
@@USEREX __userex[] no no atexit()
ISBUF, TOUPPER __isbufR[], __toupR[] + pointers yes tables and their pointers are never written after fetch
@@75VECT, ASCTIME, PERROR, SETLOCAL, STRFTIME, OSXCALC various no not linked

So nothing on FTPD's paths stores into libc370's module storage either.

Verification

  • make clean (-Wall -Werror), make test-host 72/72 assertions.
  • Guard clean on the fixed tree, and it still reports all 13 pre-fix sites when pointed at a git archive of HEAD~1 — so the CI job would have caught this.
  • The cc370 -S scan above, before and after.
  • Not verified on MVS. Nothing reproduces key-0 protection off-target: that needs /S FTPD with an APF-authorized FTPD.LINKLIB reaching FTPD001I. The path worth exercising there is the console trace — /F FTPD,TRACE ON, a transfer, /F FTPD,TRACE DUMP — which has no automated coverage in either direction and is also the empirical answer to the GRT-inheritance question above.

Follow-up, not in scope here

tools/check-module-data.py now exists twice, here and in ufsd, and httpd needs it too (mvslovers/httpd#197 is the same defect). Its natural home is mbt, where the reusable build workflow would give every consumer the job without a copy. Worth a separate issue against mbt.

FTPD is link-edited AC(1). Fetched from an APF-authorized library the job
step is authorized before program fetch runs, so MVS obtains the job pack
area in subpool 252 key 0 -- authorized code must not be patchable by
problem-key code. The STC runs problem state key 8, which makes every
store into the module's own storage a protection exception.

FTPD's first such store was the first statement of main(), so a start on
an APF-authorized FTPD.LINKLIB produced one line and nothing else:

    12.34.30 STC 1124  IEF450I FTPD FTPD - ABEND S0C4 U0000 - TIME=12.34.30

No FTPD000I, no banner: the abend lands ahead of the first WTO and long
before clib_apf_setup(). That route is unaffected because SVC 244 sets
JSCBAUTH after the fetch and cannot relabel storage already allocated,
which is why a stock TK4-/TK5 never sees this.

  - ftpd_server, the module-scope anchor, is gone. It was never read --
    all ~25 "references" the issue counted are the ftpd_server_t type.
    The server is published in grtapp1 instead, as a dump anchor.

  - The log level and the whole trace ring state move out of file scope
    in ftpd#log.c into struct ftpd_server, published through grtapp2 by
    ftpd_log_anchor(). The GRT is heap, process-level and inherited by
    every subtask (@@CRTSET copies crtgrt from the mother task), so a
    session worker resolves what main published. The API is unchanged;
    with no anchor yet the callers fall back to LOG_INFO and no tracing.

  - The four translate tables and the asc2ebc/ebc2asc pointers become
    const. They are only read today, but a codepage switch assigning one
    of the pointers is the same bug -- it is what killed HTTPD in
    http_xlate_init (mvslovers/httpd#197). The pointer shape is kept
    deliberately: libc370 and httpd export the same names as
    unsigned char *, and an array here would silently become garbage to
    anything that declared them the usual way.

TRACE ON now enables tracing when the ring is not yet allocated;
ftpd_trace_init() resets the flag, so the old order reported FTPD080I
with tracing still off. Unreachable today -- initialize() allocates the
ring at startup.

tools/check-module-data.py (from mvslovers/ufsd#66, which found this)
rejects mutable file-scope data and function-local statics in AC(1)
modules, and runs as its own CI job.

Verified: clean build, 72/72 host assertions, guard clean and still
flagging the pre-fix source. At assembler level (cc370 -S) the store

    L     2,=A(FTPD@SER)
    ST    4,0(2)

is gone from ftpd.c, ftpd#log.c has no module-storage store left, and
the ftpd#xlt.c tables are address constants resolved at load time. Not
verified on MVS: key-0 protection does not exist off-target.

Fixes #101
Both routes end in an authorized task and neither says a word on its
own: clib_apf_setup() returns 0 whether it authorized us or found us
already authorized, and libc370's diagnostics on that path are commented
out.

That silence is what made #101 hard to place. The route is what decides
whether FTPD's own module storage is writable -- an APF list entry
authorizes the job step before program fetch, so the module is fetched
into subpool 252 key 0, while SVC 244 arrives after the fetch and leaves
it key 8 -- so it belongs in the startup log next to the version banner:

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

__isauth() (one TESTAUTH, no supervisor state) is asked before
clib_apf_setup() runs, since that call destroys the distinction.

The key is inferred from the route rather than measured, and the
installation guide says so, so nobody reads it as an ISK result. Where
authorization fails outright FTPD003W already carries that and FTPD008I
stays out of the way -- one line about authorization per start, always.

Follows UFSD007I (mvslovers/ufsd#66).
@mgrossmann
mgrossmann merged commit 7954c6b into main Aug 22, 2026
2 checks passed
@mgrossmann
mgrossmann deleted the issue-101-apf-key0-statics branch August 22, 2026 17:22
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.

FTPD abends S0C4 at startup when its LINKLIB is APF-authorized

1 participant