Fix PDS member STOR/RETR: wrong dataset name, STC-identity allocation, S130 ABEND - #62
Merged
Merged
Conversation
…on, S130 guard Three interlocking bugs in the MVS store/retrieve path (src/ftpd#mvs.c), all surfaced by uploading a file into a PDS. Fixes #61. 1. resolve_dsn() PDS-member derivation. The member branch was guarded by !strchr(arg, '.'), so a client filename like "iefbr14.jcl" (which contains a dot) fell through to the relative-prefix branch and its extension became extra DSN qualifiers -> IBMUSER.CNTL.IEFBR14.JCL instead of IBMUSER.CNTL(IEFBR14). In PDS context an unquoted argument is always a member reference and a dot is a filename extension, not a qualifier separator. The branch now strips leading path and extension, uppercases, and truncates to 8 chars. Shared by RETR/STOR/DELE. 2. STOR allocation identity. __dsalcf() (SVC 99) ran between check_dataset_access() and fopen(), both of which use the logged-in user's ACEE, but with no ACEE switch of its own -- so new-dataset allocation executed under the STC identity (FTPD). Wrapped __dsalcf()/ __dsfree() in racf_set_acee(sess->acee), matching the fopen() idiom, with the ACEE always restored (including on the failure path). 3. S130 ABEND guard. With allocation running under the STC, an OPEN under the user ACEE on a failed/half-allocated dataset abended S130. Running __dsalcf() under the user's identity makes it fail cleanly for an unauthorized user; the existing early return then prevents ever reaching fopen()/OPEN. Confirmed STOR/RETR/DELE reply-and-return on every failure path (denial, allocation failure, fopen NULL).
Follow-up to the PDS member fix. The previous change corrected the
relative form (STOR iefbr14.jcl in a PDS), but a client that sends the
member explicitly in parentheses -- e.g. FileZilla's
STOR 'IBMUSER.CNTL(iefbr14.jcl)' -- bypassed member-name derivation.
That argument takes resolve_dsn()'s quoted-absolute branch verbatim, and
split_member() then copied the parenthesized text with only an 8-char
truncation: iefbr14.jcl -> IEFBR14. (trailing dot). The result was a
member physically named "IEFBR14." that no later DELE could match,
because any client input sanitizes the dot away.
Centralize member-name cleanup in one helper, sanitize_member() (strip
extension from the first '.', uppercase, truncate to FTPD_MAX_MBR_LEN),
and apply it at both extraction points:
- resolve_dsn() PDS-member branch (replacing the inline loop)
- split_member(), so any 'DSN(member.ext)' form is cleaned too
The resolve_dsn() guard still excludes arguments containing '(' on
purpose: such args are an explicit member form and are resolved as a
DSN, then cleaned by split_member(). Removing the guard would misparse a
bare DSN(member) argument -- sanitize_member() would stop at the first
qualifier's dot and yield the wrong member -- so it is kept.
Note: a member already created with a trailing dot (from before this
fix) is unreachable via FTP afterwards, since DELE input is sanitized
too; it needs manual IEHPROGM/ISPF cleanup on the target system.
The #61 RETR/STOR/DELE/MKD/RMD fix missed the LIST path, which had the same identity/authorization gap. __listpd() OPENs the PDS directory with BPAM under whatever ACEE is active, but ftpd_mvs_list() never switched to the logged-in user -- so directory listing was authorized as the STC (FTPD), not the user. On a PDS the user is denied but FTPD is not (or vice versa) the unprotected OPEN escalated a RAKF denial to ABEND S913 and killed the worker (e.g. ls of SYS1.SECURE.CNTL on TK5). Apply the established pattern to the PDS-member branch of ftpd_mvs_list(): - check_dataset_access(sess, prefix, RACF_ATTR_READ) before __listpd(), with an immediate return on denial (550 already sent) so the OPEN is never reached for a dataset the user cannot read -- same mechanism that eliminated the STOR S130. - Wrap __listpd() in racf_set_acee(sess->acee)/restore so the RAKF check evaluates against the logged-in user, both correcting the authorization identity and preventing the S913-escalating OPEN. __listpd() is the only call in the LIST path that OPENs a dataset; the entry formatters render already-fetched list entries, and __listds()/ __locate()/__dscbdv() are catalog/OBTAIN operations with no dataset OPEN. The dataset-listing branch's __listds() is therefore left unwrapped (a prefix like SYS1. is not a single RACF DATASET resource); a code comment records that decision and flags it for confirmation on TK5. CWD is intentionally left unchanged: ftpd_mvs_is_pds() uses only __locate() (SVC 26) + __dscbdv() (SVC 27), no OPEN, so cd of a protected PDS still returns a 250 PDS context and the denial correctly surfaces on the subsequent ls -- matching z/OS, where CWD sets context without reading contents.
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.
Closes #61.
Fixes three interlocking bugs in the MVS store/retrieve path (
src/ftpd#mvs.c), all surfaced by uploading a file into a PDS.What was wrong
cdinto a PDS, thenput iefbr14.jcl:resolve_dsn()'s PDS-member branch was guarded by!strchr(arg, '.'). A client filename likeiefbr14.jclcontains a dot, so it fell through to the relative-prefix branch (CWD carries a trailing dot in PDS context), yieldingIBMUSER.CNTL.IEFBR14.JCLinstead ofIBMUSER.CNTL(IEFBR14).check_dataset_access()andfopen()both run under the logged-in user's ACEE, but__dsalcf()(SVC 99) ran between them with no ACEE switch — so allocation executed under the STC identity (FTPD). This is why TK5 RAKF denials showedFTPD,FTPDinstead of the logged-in user.The fix
resolve_dsn()member derivation (~line 163). In PDS context an unquoted argument is a member reference and a dot is a filename extension, not a qualifier separator. The branch now strips leading path + extension, uppercases, and truncates to 8 chars. Shared by RETR/STOR/DELE, so all three get correct member handling. The quoted-absolute and relative-prefix (non-PDS) branches are unchanged.__dsalcf()/__dsfree()are now wrapped inracf_set_acee(sess->acee), matching thefopen()idiom, with the ACEE always restored — including on the failure path.__dsfree()runs only when allocation succeeded.__dsalcf()under the user's identity makes it fail cleanly for an unauthorized user; the existing early return then prevents ever reachingfopen()/OPEN. Audited STOR/RETR/DELE — each replies and returns immediately on access denial, allocation failure, andfopen() == NULL. No failure path reaches OPEN or an I/O loop with a NULL/invalidFILE *.Blast radius
resolve_dsn()'sin_pdsbranch is only reached by RETR/STOR/DELE (and MKD/RMD/RNFR/RNTO). LIST/NLST does not hit it — in PDS context it usessess->pds_namedirectly and treats the argument as a member filter, so listing behavior is unchanged. The only behavioral delta is a dotted argument inside a PDS, which now correctly resolves toPDS(MEMBER).Testing
make modules,-Wall -Werror).[[test]]becauseresolve_dsn()isstaticand the file has no host-buildable test seam —#include-ing the TU would drag in the full MVS runtime (racf,__locate,__dsalcf, dynalloc). Extracting the function for one assertion would be scope creep on a fix PR, so it's left for a follow-up if a test seam is added.On-MVS matrix to run before merge (needs live target)
Confirm on both MVS/CE (IBMUSER) and TK5 (HERC01) that the RAKF requestor/user in any denial is the logged-in user, not FTPD, and that a denial never crashes the worker.
Follow-up commit: sanitize quoted
DSN(member)member names (FileZilla)A second bug in the same PDS member path, found after the first fix. The first commit handled the relative form (
STOR iefbr14.jclin PDS context — CLIftp). But FileZilla sends the member explicitly in parentheses:That argument takes
resolve_dsn()'s quoted-absolute branch verbatim, andsplit_member()copied the parenthesized text with only an 8-char truncation → member physically namedIEFBR14.(trailing dot), which no laterDELEcould match.Fix: centralized member-name cleanup in one helper —
sanitize_member()(strip extension from the first., uppercase, truncate toFTPD_MAX_MBR_LEN) — applied at both extraction points:resolve_dsn()'s PDS branch (replacing the inline loop) andsplit_member()(the actual bug). NowSTOR 'IBMUSER.CNTL(iefbr14.jcl)'creates memberIEFBR14, matching the CLI result.Deliberate deviation from the proposed patch: the follow-up suggested removing the
!strchr(arg, '(')guard fromresolve_dsn()'s PDS branch. I kept it. FileZilla's arg is quoted so it never reaches that branch; removing the guard would only change the bareDSN(member)form and make it worse (sanitize_memberwould derive memberIBMUSERfromIBMUSER.CNTL(...), silently writing to a valid-but-wrong member instead of the current clean 550).(-containing args stay routed tosplit_member(), which matches the prose intent.Leftover cleanup: a member already created with a trailing dot (e.g.
IEFBR14.) is unreachable via FTP after this fix (DELEinput is sanitized too), so it needs manualIEHPROGM/ISPF cleanup on the target. Inherent, not a regression.Verification: host build clean;
sanitize_member()+ thesplit_member()path verified in isolation across the follow-up matrix (IEFBR14.JCL → IEFBR14,MyProg.cbl → MYPROG,IEFBR14. → IEFBR14, idempotent,.foo/empty rejected).Additional on-MVS checks:
Tracked under #61 (follow-up comment there).
Follow-up commit 2: LIST path — run
__listpd()under the user ACEE + authorize first (fixes S913)The first commit fixed RETR/STOR/DELE/MKD/RMD, but the LIST path had the same identity/authorization gap.
__listpd()OPENs the PDS directory with BPAM under whatever ACEE is active, andftpd_mvs_list()never switched tosess->acee— so directory listing was authorized as the STC (FTPD), not the user. On a PDS the user is denied but FTPD is not, the unprotected OPEN escalated a RAKF denial to ABEND S913 and killed the worker (lsofSYS1.SECURE.CNTLon TK5).Fix (PDS-member branch of
ftpd_mvs_list()):check_dataset_access(sess, prefix, RACF_ATTR_READ)before__listpd(), returning immediately on denial (S130-style guard) so the OPEN is never reached for a PDS the user can't read.__listpd()wrapped inracf_set_acee(sess->acee)/restore, so the OPEN-time RACHECK evaluates against the logged-in user.Both halves are required — the pre-check alone wouldn't stop an S913 for a PDS where the user is allowed but FTPD is denied.
Scope:
__listpd()is the only dataset OPEN in the LIST path (formatters render already-fetched entries;__listds()/__locate()/__dscbdv()are catalog/VTOC ops with no OPEN). The dataset-listing branch's__listds()is left unwrapped — a prefix likeSYS1.isn't a single RACF DATASET resource — with a code comment flagging it to confirm on TK5. CWD is unchanged by design (ftpd_mvs_is_pds()is__locate+__dscbdv, no OPEN), socdof a protected PDS still returns 250 and the denial surfaces onls, matching z/OS.Additional on-MVS checks (TK5 HERC01 + MVS/CE IBMUSER):
The
SYS1.MACLIBpositive control is the key regression check: LIST now callscheck_dataset_access()where it never did before.