Skip to content

Accept RACHECK rc=4 (resource not protected) as allowed - #136

Merged
mgrossmann merged 1 commit into
mainfrom
issue-135-racf-rc4-allowed
Aug 7, 2026
Merged

Accept RACHECK rc=4 (resource not protected) as allowed#136
mgrossmann merged 1 commit into
mainfrom
issue-135-racf-rc4-allowed

Conversation

@mgrossmann

@mgrossmann mgrossmann commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #135. Unblocks mvslovers/libc370#63 — httpd was the last consumer holding it up (ftpd landed its side in fc26874).

The correction to the issue

The issue states httpd "makes no decision on the value — it hands it to CGIs through the HTTPX vector". That is not the case: auth_gate() in src/httppc.c tests != 0 for its RES= route gate, added in 9e7014e (per-route auth policy) a month before the issue was written.

With the documented example from docs/configuration.md:

MOD=MVSMF /zosmf/*  AUTH=BASIC RES=FACILITY:MVSMF.ACCESS

on a system where MVSMF.ACCESS has no profile, RACHECK answers 4 after libc370#63 and every /zosmf/* request gets a 403. So this is an outage in the httpd core, not only a CGI contract question.

What was picked, and why

Option 2 from the issue (normalize 4 → 0 in http_check_auth()), but the deciding argument is not the one the issue gives.

The issue argues for option 2 because runtime-loaded modules make a contract change expensive. That is true in principle but currently hypothetical — grepping mvsmf, httplua, httprexx and every in-tree CGI (httpdsrv.c, httpjes2.c, httpdsl.c) for http_check_auth / HTTPCKAU finds zero consumers. httpd's own auth_gate() is the only caller today.

The load-bearing argument is httpd-specific: http_check_auth() returns -1 for unauthenticated. It was never a SAF pass-through — the published contract is httpd's, not SAF's. And the widened idiom libc370#63 itself suggests, rc <= 4, would read that -1 as allowed and let unauthenticated requests past the gate. Option 1 would have to renumber -1 as well, breaking existing == -1 checks. Option 2 sidesteps it, and because the normalization sits in httpxauth.c, httppc.c is correct unchanged.

rc 4 never means denied (0 permitted, 4 not protected, 8+ refused), so collapsing it into 0 cannot soften a denial.

This also explains why ftpd's opposite choice (ftpd_racf_allowed(), rc == 0 || rc == 4 at the call sites) is not an inconsistency: ftpd calls racf_auth() directly and has no sentinel value in the range.

Changes

  • src/httpracf.c + include/httpracf.hhttpracf(rc), the SAF decision table. Free of httpd.h (like httpbody()) so it unit-tests dual host/MVS; 8-char name, no asm alias needed.
  • src/httpxauth.chttp_check_auth() normalizes 4 → 0, plus the rationale and a DEBUG trace for the not-protected case (the only place the distinction is still visible).
  • src/httppc.c — comment pinning why != 0 is the whole test and must not be relaxed to <= 4.
  • include/httpd.h, include/httpcgi.h — the return contract documented at both declaration sites. httpcgi.h is the one CGI authors read.
  • test/tstracf.c + project.toml[[test]] TSTRACF, 12 assertions.
  • docs/configuration.md — a RES= resource with no profile permits access; a typo silently disables stage 2 rather than locking the route.

Verification

  • make test-host — 4 tests, 63 assertions, all pass (TSTRACF 12/12).
  • Regression guard confirmed: reverting httpracf() to the pre-fix rc == 0 makes TSTRACF fail on exactly the rc=4 assertion (11 pass / 1 fail), then passes again restored.
  • make (cc370, -Wall -Werror) — clean, all 6 modules link.
  • CI green (run 31155461716). Note the shared mbt workflow only runs make deps / make / make test / make libmake test builds the test load modules, it never executes assertions. The 63 passing assertions above are from a local make test-host.

Not verified on the live MVS system: the end-to-end path still needs libc370#63 to land before rc 4 can actually be observed. This change is safe against both the old and the new library, which is why it goes first.

To verify on MVS after libc370#63 lands

A route with RES= naming an undefined FACILITY profile must serve normally (not 403), and DEBUG 1 should show the rc=4: resource not protected trace for it.

Follow-up

The policy question is deliberately not settled here — whether an undefined RES= profile should mean "everyone passes" is a decision now that httpd can see the difference; today's permissiveness was an accident of the wrong flag bit. Filed separately so this PR stays behaviour-preserving.

Noticed, not touched

include/httpd.h:466extern http_set_env(...) is missing its int return type (implicit int, pre-existing). Out of scope here.

racf_auth() has been answering 0 for a resource with no profile because
libc370 set RACHECK flag byte 0x10 believing it meant LOG=NONE, when that
bit is DSTYPE=V (libc370 #63). With the flag corrected the same check
answers SAF rc 4 -- still "allowed", but visible to callers for the first
time.

httpd has two consumers of that rc, not one:

  - auth_gate() (httppc.c) tests != 0 for its RES= route gate. On a system
    without the profile named by, say, RES=FACILITY:MVSMF.ACCESS, every
    request to that route would start answering 403.
  - http_check_auth() publishes the rc to CGI modules through the HTTPX
    vector, and modules are LINKed at runtime -- a widened "0 or 4"
    contract would leave every not-yet-rebuilt module denying unprotected
    resources with no build-time signal to move.

So normalize rc 4 to 0 inside http_check_auth(). The published contract
"0 == permitted" keeps holding, no module needs rebuilding, and the route
gate is correct unchanged. rc 4 never means denied (0 permitted, 4 not
protected, 8+ refused), so this cannot soften a denial.

Deliberately not the shorthand "rc <= 4": http_check_auth() answers -1 for
an unauthenticated request, and <= 4 would read that as allowed and let
unauthenticated callers past the gate. The decision therefore lives in
httpracf(), which tests the two codes explicitly -- free of httpd.h so the
table is covered by a dual host/MVS test, including the -1 cases.

A resource with no profile passing the gate is SAF-correct but also the
shape of a misconfigured RES= route, so trace it under DEBUG and document
it in docs/configuration.md.

Safe against both the old and the new libc370, and httpd was the last
consumer blocking libc370 #63 (ftpd landed its side in fc26874).

Fixes #135
@mgrossmann
mgrossmann force-pushed the issue-135-racf-rc4-allowed branch from 1aff89c to a86eb9f Compare August 7, 2026 06:51
@mgrossmann
mgrossmann merged commit c1b84af into main Aug 7, 2026
1 check passed
@mgrossmann
mgrossmann deleted the issue-135-racf-rc4-allowed branch August 7, 2026 07:01
@mgrossmann

Copy link
Copy Markdown
Contributor Author

Correction to the Verification section above: the build does not pass -Wall -Werror. The actual cc370 invocation is cc370 -O1 -I include ... -c — no warning flags, contrary to what the root CLAUDE.md documents. So "clean" here means the target build compiles and all 6 modules link, not that it is warning-free. Filed as #138, along with the 593 warnings -Wall does surface (one of them a real two-%s/one-argument printf in httpdsl.c:43).

Nothing about this PR's own code changes: it adds no warnings under -Wall either.

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.

http_check_auth() passes the racf_auth() rc straight through and documents "0 == access permitted" — that stops being true with libc370#63

1 participant