Skip to content

Fix Java TLS socket reads for chunked HTTPS response bodies #1957

Open

Description

@fglock

Summary

PerlOnJava's Java-backed IO::Socket::SSL path can mark an HTTPS socket closed while Net::HTTP::NB is reading a chunked response body. The same request completes under system Perl with native IO::Socket::SSL/Net::SSLeay.

Reproduction and evidence

On PerlOnJava source/JAR commit 4b3ef9770 (Perl 5.44.1, Java 24.0.2, macOS arm64), a WWW::Mechanize GET to http://www.pearsoned.co.uk/Bookshop/ followed redirects to https://www.pearson.com/en-gb/higher-education/educators.html. Both runtimes sent the same User-Agent and Accept-Encoding: gzip request headers. The final response was 200 OK, Transfer-Encoding: chunked, and Content-Encoding: gzip.

With system Perl 5.42.2, LWP read the response to completion (about 379 KB). With PerlOnJava, the response body read consumed 13–15 bytes already buffered with the response headers, then the next sysread failed with sysread() on closed filehandle; the returned body was empty. PerlOnJava loads the bundled Java-backed IO::Socket::SSL shim and wraps the socket with Java SSLSocket; system Perl loaded its native SSL modules.

The CPAN run 20261103-123832-91742 showed a related visible failure in WWW-Scraper-ISBN-Pearson_Driver 0.24, t/10object.t: the expected Pearson search form was unavailable. The endpoint currently redirects to a modern Pearson page with no forms, so that live-site test is not a stable regression test. A deterministic local TLS/chunked-body regression should cover the socket read failure.

Expected and actual

Expected: PerlOnJava reads the full chunked TLS response body, matching system Perl.

Actual: a later sysread sees a closed filehandle after a small buffered prefix, and LWP returns an empty/partial body.

Related work

Issue #1435 / PR #1805 covered HTTPS response metadata and local TLS bridge coverage, not body reads. PR #1856 covered Unix datagram socket owner retention; no duplicate for this TLS/chunked read failure was found.

Activity

  1. added
    bugSomething isn't working
    area:ioFiles handles paths and networking
    on Nov 3, 2026
  2. fglock commented on Nov 3, 2026

    @fglock
    OwnerAuthor

    State: released
    Agent/session: codex-2026-11-03-ssl-chunked-resume
    Scope: #1957, investigate the Java TLS premature-close regression and provide a focused ownership reproducer.
    Work: fix/1957-java-ssl-chunked-body, source b43a652507d836bfbd78e3e6c250021c0b3355aa; test-only local change, no commit or PR.
    Checkpoint: 2026-11-03 17:03 UTC; the HTTP/TLS fixture was narrowed to a Perl-only closure-owner regression and posted as a separate issue comment.
    Evidence: src/test/resources/unit/deferred_capture_live_argument.t passes all three assertions with system Perl. timeout 60 ./jperl ... and timeout 60 ./jperl --interpreter ... both fail the first assertion (destroy_count is 1 while the temporary referent remains in active @_). The earlier Pearson CPAN probe also showed system Perl reading the response to completion while POJ closes after a 32-byte prefix.
    Blocked by: none.
    Next: user plans to add the reproducer to #1649's ownership-migration work; #1957 remains open and has no fix PR.

  3. fglock commented on Nov 3, 2026

    @fglock
    OwnerAuthor

    A Perl-only reproducer isolates the closure-capture ownership case behind this failure, without HTTP or TLS. It passes with system Perl and fails on PerlOnJava's JVM and interpreter backends: the temporary referent is destroyed at the inner scope boundary while it is still the active @_ argument.

    use strict;
    use warnings;
    use Test::More;
    
    {
        package Local::ClosureOwnerProbe;
        our $destroy_count = 0;
        our $destroyed_while_argument_live = -1;
    
        sub new {
            return bless { token => 'alive' }, shift;
        }
    
        sub DESTROY {
            ++$destroy_count;
        }
    }
    
    sub read_after_capture_scope_exits {
        {
            my $captured = $_[0];
            my $callback = sub { $captured->{token} };
        }
    
        $Local::ClosureOwnerProbe::destroyed_while_argument_live =
            $Local::ClosureOwnerProbe::destroy_count;
        return $_[0]->{token};
    }
    
    my $value = read_after_capture_scope_exits(Local::ClosureOwnerProbe->new);
    is($Local::ClosureOwnerProbe::destroyed_while_argument_live, 0,
        'the active call argument keeps its referent alive after the captured lexical leaves scope');
    is($value, 'alive', 'the active call argument remains usable');
    is($Local::ClosureOwnerProbe::destroy_count, 1,
        'the temporary referent is destroyed after the call returns');
    
    done_testing;

    Results on source b43a652507d836bfbd78e3e6c250021c0b3355aa:

    • perl src/test/resources/unit/deferred_capture_live_argument.t: all 3 assertions pass.
    • timeout 60 ./jperl src/test/resources/unit/deferred_capture_live_argument.t: assertion 1 fails (got 1, expected 0).
    • timeout 60 ./jperl --interpreter src/test/resources/unit/deferred_capture_live_argument.t: assertion 1 fails (got 1, expected 0).

    The test is prepared locally at src/test/resources/unit/deferred_capture_live_argument.t and is not committed.

  4. fglock commented on Nov 4, 2026

    @fglock
    OwnerAuthor

    State: released
    Agent/session: codex-2026-11-04-io-regressions
    Scope: #1957; fix temporary blessed filehandle destruction during chunked TLS body reads and add deterministic local coverage.
    Work: draft stacked PR #1976, branch fix/1957-temporary-filehandle-owner, commit 51e7b31873f315a860af40273f26e575ea008cce, based on draft PR #1938 head 95b7c0cd8abbec3d55e9ee377f67a9c1a3c8ca05.
    Checkpoint: 2026-11-04; RuntimeCode.retainArgumentAliasOwners now retains temporary strong GLOBREFERENCE values for the active @_ frame. The TLS fixture keeps its callback alive through the body read and verifies full chunk preservation.
    Evidence: the exact fixture fails on parent eee26cd6e on JVM and interpreter with sysread() on closed filehandle; system Perl passes. It passes on the candidate on system Perl, JVM, and interpreter. Related ArgumentAliasOwnerSlotTest and weak blessed-tree/closure tests pass after narrowing the change to GLOBREFERENCE arguments. nice -n 19 timeout 3600 make passes on 51e7b3187 in 10m32; nice -n 19 make check-links passes with 791 links and zero errors. Logs: /private/tmp/perlonjava-pr1957-parent-exact-repro.log, /private/tmp/perlonjava-pr1957-alias-owner-fix-final.log, /private/tmp/perlonjava-pr1957-globref-focused.log, /private/tmp/perlonjava-pr1957-full-make-51e7b3187.log, /private/tmp/perlonjava-pr1957-links-51e7b3187.log.
    Blocked by: #1649/#1938 base integration; PR #1976 is intentionally draft until the ownership foundation lands.
    Next: after #1938 integrates, rebase #1976 onto master, rerun the full gate, and promote it for review.

  5. fglock commented on Nov 4, 2026

    @fglock
    OwnerAuthor

    Follow-up on the deterministic TLS acceptance case: the local TLS fixture passes with system Perl and the PerlOnJava interpreter, but still fails on the JVM with sysread() on closed filehandle after response-header prefetch. This remains open pending the ownership work in #1649; the focused Perl closure-owner regression passing on the current #1649 candidate is not yet enough to close the end-to-end TLS case. Please rerun this fixture after #1649 integrates.

  6. fglock commented on Nov 4, 2026

    @fglock
    OwnerAuthor

    I tightened the local reproducer after finding that the initial version let the callback become dead before body reads. With the callback held through the read, the TLS fixture still fails on the JVM only when a freshly constructed IO::Socket::SSL is passed directly as a temporary argument to the reader; system Perl passes. Keeping that socket in a caller lexical makes the fixture pass on both current master and the #1649 candidate. The JVM failure reaches IO::Handle::DESTROY and closes the socket during TAP output, rather than going through RuntimeScalar.releaseIoOwner.

    This narrows #1957 to lifetime handling of temporary blessed filehandle arguments; it is related to the ownership semantics under #1649, while the candidate's existing hash-object closure regression alone does not prove the TLS case fixed. Keeping #1957 open pending a runtime fix and a passing direct-temporary TLS regression.

  7. fglock commented on Nov 4, 2026

    @fglock
    OwnerAuthor

    The narrowed temporary-filehandle fix and local TLS regression are in draft PR #1976, stacked on #1938. The parent fails the deterministic fixture on JVM and interpreter; the candidate passes on system Perl, JVM, and interpreter, and the full make gate passes. Keeping #1957 open until the #1649 base integrates and the stacked PR is retested on master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:ioFiles handles paths and networkingbugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions