Skip to content

STOR/RETR in PDS context creates wrong dataset + runs allocation under STC identity + S130 ABEND #61

Description

@mgrossmann

Three related bugs in the MVS dataset store/retrieve path (src/ftpd#mvs.c), all triggered by uploading a file into a PDS.

Reproduction

cd cntl
250 The working directory "IBMUSER.CNTL" is a partitioned data set
put iefbr14.jcl
125 Storing data set IBMUSER.CNTL.IEFBR14.JCL      ← WRONG: should be IBMUSER.CNTL(IEFBR14)

On TK5 this escalates to a crash:

RAKF0005 INVALID ATTEMPT TO ACCESS RESOURCE
RAKF000A  FTPD    ,FTPD    ,DATASET ,HERC01.TEST.CNTL.IEF
worker ... ended ABEND S130

Bug 1 — PDS member name derivation. In resolve_dsn() (~line 163) the PDS-member branch is guarded by !strchr(arg, '.'). A client filename like iefbr14.jcl contains a dot, so it falls through to the relative-prefix branch and the extension becomes extra DSN qualifiers → IBMUSER.CNTL.IEFBR14.JCL instead of IBMUSER.CNTL(IEFBR14).

Bug 2 — allocation runs under STC identity, not the logged-in user. In the STOR handler, check_dataset_access() correctly authorizes with sess->acee (the logged-in user), and fopen() is wrapped in racf_set_acee(sess->acee). But __dsalcf() (SVC 99 allocation, ~line 1510) runs BETWEEN them with NO ACEE switch — so new-dataset allocation executes under the STC identity (FTPD). This is why the RAKF denial shows FTPD,FTPD rather than the logged-in user. Allocation must run under the user's ACEE, exactly like fopen().

Bug 3 — S130 ABEND after RAKF denial. When authorization or allocation fails, the handler continues into fopen()/OPEN with an invalid DCB state, causing ABEND S130. Failure paths must reply and return immediately, never reaching OPEN.

Fix

1. Member name derivation in resolve_dsn() (src/ftpd#mvs.c ~line 163). In PDS context an unquoted argument is always a member reference; a dot is a filename extension, not a qualifier separator. Replace the current else if branch:

} else if (sess->in_pds && arg[0] != '\'' && !strchr(arg, '(')) {
    /* Inside a PDS: unquoted argument is a MEMBER reference.
    ** Derive an MVS member name from the client filename:
    **   strip leading path, strip extension (from first '.'),
    **   uppercase, truncate to 8 chars.
    */
    char member[9];
    const char *base = arg;
    const char *slash;
    int m = 0;

    slash = strrchr(base, '/');
    if (slash) base = slash + 1;

    for (i = 0; base[i] && base[i] != '.' && m < 8; i++)
        member[m++] = (char)toupper((unsigned char)base[i]);
    member[m] = '\0';

    if (m == 0)
        return -1;   /* no usable member name, e.g. ".foo" */

    len = snprintf(buf, bufsz, "%s(%s)", sess->pds_name, member);
    if (len >= bufsz)
        return -1;

    return 0;        /* member name already clean; skip generic uppercase/dot-strip */
}

Keep the existing quoted-absolute branch and the relative-prefix (non-PDS) branch unchanged. This one function is shared by RETR, STOR, and DELE, so all three get the correct member handling.

2. Wrap __dsalcf()/__dsfree() in the user ACEE in the STOR handler. The allocation must run under the same identity as the subsequent fopen():

if (/* dataset does not exist, needs allocation */) {
    ACEE *oldacee = sess->acee ? racf_set_acee(sess->acee) : NULL;
    rc = __dsalcf(ddname, "%s", opts);
    if (rc == 0)
        __dsfree(ddname);
    if (sess->acee) racf_set_acee(oldacee);   /* restore STC identity */

    if (rc != 0) {
        ftpd_log(LOG_ERROR, "STOR: __dsalcf failed rc=%d", rc);
        ftpd_session_reply(sess, 550, "Dataset allocation failed");
        return;                                /* do NOT fall through to fopen */
    }
}

Make sure the ACEE is always restored, including on the failure path (don't leak the user ACEE as the STC's active environment).

3. Guarantee early return on every failure path (S130 guard). Audit the STOR/RETR/DELE handlers so that:

  • check_dataset_access(...) != 0 → reply 550 Access denied and return immediately.
  • allocation failure → reply 550 and return.
  • fopen() returning NULL → reply 550/550 Cannot open and return; never continue to read/write.

No failure may reach an OPEN or an I/O loop with a NULL/invalid FILE *. The S130 is OPEN running on a failed allocation state — these early returns eliminate it.

Test matrix (please verify all)

cd 'IBMUSER.CNTL'          (PDS context)
put iefbr14.jcl            → 250, creates IBMUSER.CNTL(IEFBR14), RAKF shows IBMUSER not FTPD
get IEFBR14                → downloads the member
get iefbr14.jcl            → downloads IBMUSER.CNTL(IEFBR14) (extension stripped)
dir                        → listing shows member IEFBR14
delete IEFBR14             → removes the member

cd 'IBMUSER.'              (prefix mode, not a PDS)
put test.data             → creates sequential dataset IBMUSER.TEST.DATA (unchanged)

# Negative / crash-regression:
cd 'SYS1.PARMLIB'          (a PDS the user may not have ALTER on)
put foo.jcl               → 550 Access denied, NO S130 ABEND, STC stays up

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.

Ref: resolve_dsn() PDS member branch, ACEE consistency for __dsalcf vs fopen, TK5 ABEND S130

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions