java-topology/test/jdk/com/sun/jndi/ldap/SkipAbandonRequestOnClosedConn.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
2026-03-26 17:11:57 -04:00

270 lines
11 KiB
Java

/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.Socket;
import java.util.Arrays;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import com.sun.jndi.ldap.Connection;
import com.sun.jndi.ldap.LdapClient;
import com.sun.jndi.ldap.LdapCtx;
import jdk.test.lib.net.URIBuilder;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
/*
* @test
* @bug 8362268
* @summary Verify that unexpected exceptions aren't propagated to LdapCtx callers
* when the LdapCtx's (internal) connection is closed due to an exception
* when reading/writing over the connection's stream
* @modules java.naming/com.sun.jndi.ldap:+open
* @library /test/lib lib/
* @build jdk.test.lib.net.URIBuilder
* BaseLdapServer LdapMessage
* @run junit/othervm ${test.main.class}
*/
class SkipAbandonRequestOnClosedConn {
private static final String LOOKUP_NAME = "ou=People,o=FooBar";
private static final class Server extends BaseLdapServer {
private Server() throws IOException {
super();
}
// handles and responds to the incoming LDAP request
@Override
protected void handleRequest(final Socket socket,
final LdapMessage request,
final OutputStream out) throws IOException {
switch (request.getOperation()) {
case SEARCH_REQUEST: {
System.err.println("responding to SEARCH_REQUEST with id: "
+ request.getMessageID() + " on socket " + socket);
// write out some bytes as a response. it doesn't matter what those
// bytes are - in this test they aren't expected to reach
// the application code.
final byte[] irrelevantResponse = new byte[]{0x42, 0x42, 0x42};
System.err.println("Response: " + Arrays.toString(irrelevantResponse));
out.write(irrelevantResponse);
out.flush();
break;
}
default: {
throw new IOException("unexpected operation type: " + request.getOperation()
+ ", request: " + request);
}
}
}
}
private static Server server;
@BeforeAll
static void beforeAll() throws Exception {
server = new Server();
server.start();
System.err.println("server started at " + server.getInetAddress()
+ ":" + server.getPort());
}
@AfterAll
static void afterAll() throws Exception {
if (server != null) {
System.err.println("stopping server " + server.getInetAddress()
+ ":" + server.getPort());
server.close();
}
}
/*
* Creates a com.sun.jndi.ldap.LdapCtx and configures its internal com.sun.jndi.ldap.Connection
* instance with an InputStream that throws an IOException in its read() methods. The test
* then initiates a Context.lookup() so that a LDAP request is internally issued and a LDAP
* response is waited for. Due to the configured InputStream throwing an exception from its
* read(), the com.sun.jndi.ldap.Connection will get closed (internally) when reading the
* response and the main thread which was waiting for the LDAP response is woken up and notices
* the connection closure. This test verifies that a NamingException gets thrown from the
* lookup() in this scenario.
*/
@Test
void testNamingException() throws Exception {
final Hashtable<String, String> envProps = new Hashtable<>();
envProps.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
final String providerUrl = URIBuilder.newBuilder()
.scheme("ldap")
.host(server.getInetAddress().getHostAddress())
.port(server.getPort())
.build().toString();
envProps.put(Context.PROVIDER_URL, providerUrl);
// explicitly set LDAP version to 3 to prevent LDAP BIND requests
// during LdapCtx instantiation
envProps.put("java.naming.ldap.version", "3");
// create a (custom) InitialContext which allows us to access the
// LdapCtx's internal connection instance
try (final CustomContext ctx = new CustomContext(envProps)) {
// replace the InputStream and OutputStream of the LdapCtx's
// internal connection, to allow us to raise exception from
// the InputStream and OutputStream as necessary
ctx.replaceStreams();
System.err.println("issuing ldap request against " + providerUrl
+ " using context " + ctx);
// trigger the LDAP SEARCH request through the lookup call. we are not
// interested in the returned value and are merely interested in the
// Context.lookup(...) failing with a NamingException
assertLookupThrowsNamingException(ctx);
}
}
// assert that Context.lookup(...) raises a NamingException
private static void assertLookupThrowsNamingException(final Context ctx) {
try {
final Object result = ctx.lookup(LOOKUP_NAME);
fail("Context.lookup() was expected to throw NamingException" +
" but returned result " + result);
} catch (NamingException ne) {
// verify the NamingException is for the right reason
if (!ne.toString().contains("LDAP connection has been closed")) {
// unexpected exception, propagate it
fail("NamingException is missing \"LDAP connection has been closed\" message", ne);
}
// expected
System.err.println("got expected exception: " + ne);
}
}
private static final class CustomContext extends InitialContext implements AutoCloseable {
private CustomContext(final Hashtable<?, ?> environment) throws NamingException {
super(environment);
}
private LdapCtx getLdapCtx() throws NamingException {
final Context ctx = getDefaultInitCtx();
if (ctx instanceof LdapCtx ldapCtx) {
return ldapCtx;
}
throw new IllegalStateException("Not a LdapCtx: " + ctx.getClass().getName());
}
// using reflection, return the com.sun.jndi.ldap.Connection instance
// from within the com.sun.jndi.ldap.LdapCtx
private Connection getConnection() throws Exception {
final LdapCtx ldapCtx = getLdapCtx();
final Field clientField = ldapCtx.getClass().getDeclaredField("clnt");
clientField.setAccessible(true);
final LdapClient ldapClient = (LdapClient) clientField.get(ldapCtx);
final Field connField = ldapClient.getClass().getDeclaredField("conn");
connField.setAccessible(true);
return (Connection) connField.get(ldapClient);
}
private void replaceStreams() throws Exception {
final Connection conn = getConnection();
assertNotNull(conn, "com.sun.jndi.ldap.Connection instance is null");
final InputStream originalInputStream = conn.inStream;
final OutputStream originalOutputStream = conn.outStream;
// replace the connection's streams with our test specific streams
conn.replaceStreams(new In(originalInputStream), new Out(originalOutputStream));
System.err.println("replaced streams on connection: " + conn);
}
}
// an OutputStream which intentionally throws a NullPointerException
// from its write(...) methods if the OutputStream has been closed
private static final class Out extends FilterOutputStream {
private volatile boolean closed;
private Out(final OutputStream underlying) {
super(underlying);
}
@Override
public void write(final int b) throws IOException {
if (closed) {
throw new NullPointerException("OutputStream is closed - intentional" +
" NullPointerException instead of IOException");
}
super.write(b);
}
@Override
public void write(final byte[] b, final int off, final int len) throws IOException {
if (closed) {
throw new NullPointerException("OutputStream is closed - intentional" +
" NullPointerException instead of IOException");
}
super.write(b, off, len);
}
@Override
public void close() throws IOException {
System.err.println("closing output stream " + this);
closed = true;
super.close();
}
}
// an InputStream which intentionally throws an exception
// from its read(...) methods.
private static final class In extends FilterInputStream {
private In(InputStream underlying) {
super(underlying);
}
@Override
public int read() throws IOException {
final int v = super.read();
System.err.println("read " + v + " from " + in
+ ", will now intentionally throw an exception");
throw new IOException("intentional IOException from " + In.class.getName() + ".read()");
}
@Override
public int read(final byte[] b, final int off, final int len) throws IOException {
final int v = super.read(b, off, len);
System.err.println("read " + v + " byte(s) from " + in
+ ", will now intentionally throw an exception");
throw new IOException("intentional IOException from " + In.class.getName()
+ ".read(byte[], int, int)");
}
}
}