Skip to content

v1.0.2

Choose a tag to compare

@github-actions github-actions released this 05 Sep 13:47
· 268 commits to main since this release

FTPD 1.0.2 is a relink: the release artifacts are built against libc370
1.0.4 instead of 1.0.3. One issue closed, no new feature, no breaking change —
and one behaviour change that is not labelled breaking but should be read.

If you saw an occasional ABEND S001 under parallel transfers, this is the
release that fixes it.
If you also run UFSD, take ufsd 1.2.2 with it: the
same library fixes reach it, and it is the same kind of relink.

The headline: #117, an S001 that was never in the transfer path

Rob Prins reported an ABEND S001 that hit
occasionally during uploads to UFS, mostly under parallel transfers, and sent
the dump. The dump was all it took — and the answer was somewhere else
entirely.

The abend is not in the UFS path, or the upload path, or FTPD at all. It is a
locking defect in libc370, the C runtime statically linked into every one of
our servers. vvprintf()'s %s and %eEgGfF branches called the public
fputs(), and every __examin() conversion went through the public fputc().
Those wrappers lock and unconditionally unlock the same FILE, and ENQ/DEQ
RET=HAVE carries no nesting count — so the inner unlock released the caller's
hold at the first conversion, and everything after it ran unserialized,
including the \n-triggered QSAM PUT.

Two session threads writing a log line at the same moment therefore merged and
truncated records, and the broken PUT reached the JES2 spool as an I/O error —
which MVS, with no SYNAD exit on the DCB, turned into S001 and took the whole
address space with it. Rob's dump shows the collision itself: the last log
record before the abend is two log lines merged into one truncated record. That
is why it was occasional, and why parallel transfers made it likely.

Fixed as libc370#145 and #147,
and measured there on demand: a writer wedged after 8 of 400 lines against the
unfixed library, and 800 of 800 records intact against the fixed one. libc370
ships through the cc370 sysroot rather than as a pinned dependency, so this
relink is the entire delivery mechanism
— the fix reaches a running FTPD no
other way. Thank you, Rob.

fprintf() was never affected: it formats into a private buffer and issues one
fwrite().

Three neighbours came with it: puts() locked twice and could be split at its
newline by a concurrent printf; fclose() flushed, closed the DCB and freed
both buffer and FILE with no hold on the lock; and __enqdeq()'s DEQ branch
overwrote pl.opt instead of OR-ing into it, so every DEQ went out
SCOPE=STEP and could never release a SCOPE=SYSTEM ENQ.

Not labelled breaking, but read it (#119)

libc370 1.0.4 now plants a SYNAD on the DCBs its stdio layer opens, so an
uncorrectable I/O error — bad track, wrong-length record, device error — becomes
ferror() + errno EIO instead of ABEND S001. For a server that is plainly the
right trade; it is what turns #117's corrupted PUT from a dead address space
into a recoverable condition.

The other side of it is that feof() is deliberately not set, so a loud
failure becomes a silent one wherever a read loop treats the resulting
NULL/0 as end of data. FTPD had three such places. None of them is a new
defect — each used to end in S001, and would now have ended in a wrong answer:

Before Now
Read error on DD:FTPDPRM S001 FTPD009E, start abandoned
Read error during RETR S001 451, with the byte count actually sent
Write error during STOR S001 451, and the transfer stops at the failing record

Automation or a runbook that reads a 250 as "the transfer is complete" now
has a second outcome to know about
, and it is the one that matters: before
this release a bad track mid-RETR ended the send loop exactly like the end of
the data set and fell through to 250 Transfer completed successfully. The
client stored a fragment and was told it had the whole data set, with nothing on
the wire to tell the difference. STOR was the same in the other direction, and
worse in one respect: after the first failed write it kept issuing PUTs against
a DCB that had already failed, then reported 250 over an incomplete data set
on DASD.

The parmlib case is quieter and reaches further. A read error stopped the
fgets() loop indistinguishably from a clean end of member, kept whatever had
been parsed, left the rest at its default, and still reported a successful load
— so FTPD came up looking healthy with DASD volumes missing from its list and
SITE/JES settings silently back at their defaults. It now abandons the start:

FTPD009E PARMLIB (DD:FTPDPRM) READ FAILED -- CONFIGURATION INCOMPLETE,
         SHUTTING DOWN

Measured on MVS, with the error vector libc370's own probe established — the
same data set behind two DDs that disagree about BLKSIZE, so the first READ
meets a 3120-byte block with an 80-byte buffer. TSTPRM runs it on both the
batch and the TSO leg:

guard removed   round 2: rc=0 errno=5 port=2121 maxsess=10
shipped         round 2: rc=4 errno=5 port=2121 maxsess=10

The first line is the defect in one row. Both rounds read the same member;
SRVPORT is its first keyword and comes back parsed, MAXSESSIONS is its
second and comes back at the default 10 rather than the 17 that is in the file.
A truncated parse, reported as a successful load. The shipped build refuses it.

The RETR and STOR guards are not measured. The lying DD cannot be
injected through the fopen() those paths use, and building a DSCB that lies
about its own blocks would be guesswork. They are a reviewed ferror() check,
and this is the honest description of them.

Deliberately not fixed here

Both recorded in #119:

  • FTPD's log goes to STDOUT through printf(). With the SYNAD in place, an I/O
    error on SYSOUT now leaves the started task running with logging silently
    dead
    rather than abending. Fixing that properly needs the fail-fast /
    clearerr() decision that is still open as libc370#149.
  • In STOR, a negative return from the data connection also still ends in
    250. Pre-existing, unrelated to this relink, and a different fix.

What is running

FTPD005I stamps the library the module was actually linked against, so a
deploy that did not take cannot hide:

FTPD000I FTPD 1.0.2 (6CCB125) STARTING
FTPD005I LIBC370 1.0.4 (1CA9E04)

If that second line still says 1.0.3, the STC is running the old module —
the point of the relink is exactly that line.

Installing

Download ftpd-1.0.2-dist.zip (or .tar.gz); the guide and every file you
need are inside, and the procedure is unchanged from 1.0.1:

README.md the installation guide
ftpd-1.0.2-load.xmit the FTPD load module
ftpd-1.0.2-samplib.xmit the STC procedure and the configuration pattern
ftpd-1.0.2-alloc.jcl allocate the datasets — run once
ftpd-1.0.2-inst.jcl receive, apply, accept

Upload the two XMITs in binary, submit the allocation job once, then the
install job — the only thing to edit is the two dataset names you uploaded to.
The SYSMOD travels inline in the job, so there is no third file to transfer.
Everything lands under FTPD.V1R0M2.*, so it does not collide with an existing
V1R0M0 or V1R0M1 installation. FMID is TFTP100.

The two caveats from 1.0.1 still apply to the shipped material:

  • On TK5, SYS2.PARMLIB does not exist — put FTPDPRM0 in SYS1.PARMLIB
    and start with /S FTPD,D='SYS1.PARMLIB'.
  • The two JES DDs in the procedure, HASPCKPT and HASPACE1, carry the unit
    and volume of the system FTPD was built on. Point them at your own JES2
    checkpoint and spool, or delete both if you do not want the JES interface.
    Both are required together: with HASPACE1 missing, every JES command
    answers 451.

Upgrading from 1.0.1

The same shape as 1.0.0 → 1.0.1, for the same reason. An FMID names a
functional level, one per minor release, so 1.0.2 ships as TFTP100 again —
the id 1.0.1 already applied and accepted, and SMP will not apply a function
SYSMOD that is already applied.

Keeping it SMP-managed Free the FMID first with the UCLIN job in doc/uninstall.md, then run the 1.0.2 allocation and install jobs as for a fresh install
Without SMP Receive ftpd-1.0.2-load.xmit into a library and point the procedure's STEPLIB at it

Run only the UCLIN step of that page, not its DELETE step. The libraries
are versioned per patch — FTPD.V1R0M2.* here — so the new install never
touches 1.0.1's, and the DELETE names the old release's datasets. Scratching
them is a choice about the predecessor, not a step of this upgrade; after the
UCLIN they are simply orphans with no inventory record, and leaving them is
fine.

Check the inventory before and after with the zone operand — it is mandatory,
LIST SYSMODS . is SMP/E syntax and gets HMA2033:

LIST CDS  SYSMOD(TFTP100) .
LIST ACDS SYSMOD(TFTP100) .

RC 04 with an empty list means the id is free.

Either way, copy the 1.0.2 procedure from the new SAMPLIB into your PROCLIB —
it is what points STEPLIB at the new LINKLIB. On the APF route, remember
the list is read at IPL only: until FTPD.V1R0M2.LINKLIB is in IEAAPF00 and
the system has IPLed, FTPD falls back to SVC 244 rather than failing. Your
FTPDPRM0 and your RAKF definitions are unaffected — no configuration keyword
changed in this release.

A correction shipped with it. doc/uninstall.md said <vrm> was V1R0M0
for all of 1.0.x. It is not: the qualifier carries the patch level, and that
page is exactly where a patch upgrade is sent. Following the old sentence
scratched the predecessor's libraries and left the live installation standing,
with SMP reporting accepted throughout. Corrected on both pages, with the reason
the FMID and the libraries move at different rates spelled out.

Still open

1.0.2 fixes one defect and hardens three code paths against a library change; it
does not touch the identity model, so the warning that came with 1.0.0 stands
unchanged. The STC's resting identity is still STC/STCGROUP, which holds ALTER
on every data set (#97); JES
access still has no authorization model, JESOWNER being a client-set display
filter rather than a control (#90);
and the ACEE switch is still not fully serialized against concurrent sessions
(#64). Keep the FTPD identity
scoped to what the started task itself needs — never give it broad data set
authority.
Closing those three is what 1.1.0 is for.

The known limits are unchanged: STOR allocates a hardcoded 10/5-track SPACE,
capping an upload at about 85 tracks
(#100); shutdown can take up to
~5 seconds to notice idle sessions
(#66); and a NULL from
__listpd()/__listds() is still read as an empty listing rather than checked
for a storage shortage (#118).


Full Changelog: v1.0.1...v1.0.2