postgresql-jdbc.git / summary / log / commit / refs

commit    ed03772b3ab980cee24470b39cf6f4d428f034ed
Author:   Kris Jurka <books@ejurka.com>
Date:     Mon Oct 18 03:45:26 2004 +0000

    When streaming data from setBinaryStream to the backend, do not
    allow Exceptions or bad length parameters to cause the connection
    state to get confused destroying the connection.


org/postgresql/core/PGBindException.java | 16 ++++++ org/postgresql/core/PGStream.java | 20 +++++-- org/postgresql/core/v3/QueryExecutorImpl.java | 66 +++++++++++++++++++--- org/postgresql/jdbc2/AbstractJdbc2Statement.java | 9 +++ .../test/jdbc2/PreparedStatementTest.java | 56 ++++++++++++++++++ org/postgresql/test/util/BrokenInputStream.java | 25 ++++++++ 6 files changed, 179 insertions(+), 13 deletions(-) diff --git a/org/postgresql/core/PGBindException.java b/org/postgresql/core/PGBindException.java new file mode 100644 index 00000000..8f7e8120 --- /dev/null +++ b/org/postgresql/core/PGBindException.java @@ -0,0 +1,16 @@ +package org.postgresql.core; + +import java.io.IOException; + +public class PGBindException extends IOException { + + private IOException _ioe; + + public PGBindException(IOException ioe) { + _ioe = ioe; + } + + public IOException getIOException() { + return _ioe; + } +} diff --git a/org/postgresql/core/PGStream.java b/org/postgresql/core/PGStream.java index 068aaaac..3c942707 100644 --- a/org/postgresql/core/PGStream.java +++ b/org/postgresql/core/PGStream.java @@ -24,6 +24,8 @@ import java.io.Writer; import java.net.Socket; import java.sql.*; +import org.postgresql.util.GT; + /** * Wrapper around the raw connection to the server that implements some basic * primitives (reading/writing formatted data, doing string encoding, etc). @@ -437,6 +439,7 @@ public class PGStream * @param remaining the number of bytes to copy */ public void SendStream(InputStream inStream, int remaining) throws IOException { + int expectedLength = remaining; if (streamBuffer == null) streamBuffer = new byte[8192]; @@ -444,10 +447,19 @@ public class PGStream int count = (remaining > streamBuffer.length ? streamBuffer.length : remaining); int readCount; - readCount = inStream.read(streamBuffer, 0, count); - if (readCount < 0) - throw new EOFException("Premature end of input stream"); - + try { + readCount = inStream.read(streamBuffer, 0, count); + if (readCount < 0) + throw new EOFException(GT.tr("Premature end of input stream, expected {0} bytes, but only read {1}.", new Object[]{new Integer(expectedLength), new Integer(expectedLength - remaining)})); + } catch(IOException ioe) { + while (remaining > 0) { + Send(streamBuffer, count); + remaining -= count; + count = (remaining > streamBuffer.length ? streamBuffer.length : remaining); + } + throw new PGBindException(ioe); + } + Send(streamBuffer, readCount); remaining -= readCount; } diff --git a/org/postgresql/core/v3/QueryExecutorImpl.java b/org/postgresql/core/v3/QueryExecutorImpl.java index 8cd4e41e..a1ba095f 100644 --- a/org/postgresql/core/v3/QueryExecutorImpl.java +++ b/org/postgresql/core/v3/QueryExecutorImpl.java @@ -131,10 +131,30 @@ public class QueryExecutorImpl implements QueryExecutor { ((V3ParameterList)parameters).checkAllParametersSet(); try { - handler = sendQueryPreamble(handler, flags); - sendQuery((V3Query)query, (V3ParameterList)parameters, maxRows, fetchSize, flags); - sendSync(); - processResults(handler, flags); + try { + handler = sendQueryPreamble(handler, flags); + sendQuery((V3Query)query, (V3ParameterList)parameters, maxRows, fetchSize, flags); + sendSync(); + processResults(handler, flags); + } catch (PGBindException se) { + // There are three causes of this error, an + // invalid total Bind message length, a + // BinaryStream that cannot provide the amount + // of data claimed by the length arugment, and + // a BinaryStream that throws an Exception + // when reading. + // + // We simply do not send the Execute message + // so we can just continue on as if nothing + // has happened. Perhaps we need to + // introduce an error here to force the + // caller to rollback if there is a + // transaction in progress? + // + sendSync(); + processResults(handler, flags); + handler.handleError(new PSQLException(GT.tr("Unable to bind parameter values for statement."), PSQLState.INVALID_PARAMETER_VALUE, se.getIOException())); + } } catch (IOException e) { protoConnection.close(); handler.handleError(new PSQLException(GT.tr("An I/O error occured while sending to the backend."), PSQLState.CONNECTION_FAILURE, e)); @@ -576,14 +596,14 @@ public class QueryExecutorImpl implements QueryExecutor { // + 2 (param format code count) + N * 2 (format codes) // + 2 (param value count) + N (encoded param value size) // + 2 (result format code count, 0) - int encodedSize = 0; + long encodedSize = 0; for (int i = 1; i <= params.getParameterCount(); ++i) { if (params.isNull(i)) encodedSize += 4; else - encodedSize += 4 + params.getV3Length(i); + encodedSize += (long)4 + params.getV3Length(i); } - + encodedSize = 4 + (encodedPortalName == null ? 0 : encodedPortalName.length) + 1 + (encodedStatementName == null ? 0 : encodedStatementName.length) + 1 @@ -591,8 +611,18 @@ public class QueryExecutorImpl implements QueryExecutor { + 2 + encodedSize + 2; + // backend's MaxAllocSize is the largest message that can + // be received from a client. If we have a bigger value + // from either very large parameters or incorrent length + // descriptions of setXXXStream we do not send the bind + // messsage. + // + if (encodedSize > 0x3fffffff) { + throw new PGBindException(new IOException(GT.tr("Bind message length {0} too long. This can be caused by very large or incorrect length specifications on InputStream parameters.", new Long(encodedSize)))); + } + pgStream.SendChar('B'); // Bind - pgStream.SendInteger4(encodedSize); // Message size + pgStream.SendInteger4((int)encodedSize); // Message size if (encodedPortalName != null) pgStream.Send(encodedPortalName); // Destination portal name. pgStream.SendChar(0); // End of portal name. @@ -605,12 +635,26 @@ public class QueryExecutorImpl implements QueryExecutor { pgStream.SendInteger2(params.isBinary(i) ? 1 : 0); // Parameter format code pgStream.SendInteger2(params.getParameterCount()); // # of parameter values + + // If an error occurs when reading a stream we have to + // continue pumping out data to match the length we + // said we would. Once we've done that we throw + // this exception. Multiple exceptions can occur and + // it really doesn't matter which one is reported back + // to the caller. + // + PGBindException bindException = null; + for (int i = 1; i <= params.getParameterCount(); ++i) { if (params.isNull(i)) pgStream.SendInteger4(-1); // Magic size of -1 means NULL else { pgStream.SendInteger4(params.getV3Length(i)); // Parameter size - params.writeV3Value(i, pgStream); // Parameter value + try { + params.writeV3Value(i, pgStream); // Parameter value + } catch(PGBindException be) { + bindException = be; + } } } @@ -618,6 +662,10 @@ public class QueryExecutorImpl implements QueryExecutor { pgStream.SendChar(0); // (...) pendingBindQueue.add(portal); + + if (bindException != null) { + throw bindException; + } } private void sendDescribe(Portal portal) throws IOException { diff --git a/org/postgresql/jdbc2/AbstractJdbc2Statement.java b/org/postgresql/jdbc2/AbstractJdbc2Statement.java index 33dade8a..c1e359c0 100644 --- a/org/postgresql/jdbc2/AbstractJdbc2Statement.java +++ b/org/postgresql/jdbc2/AbstractJdbc2Statement.java @@ -1184,6 +1184,9 @@ public abstract class AbstractJdbc2Statement implements BaseStatement setNull(parameterIndex, Types.VARCHAR); return; } + if (length < 0) + throw new PSQLException(GT.tr("Invalid stream length {0}.", new Integer(length))); + //Version 7.2 supports AsciiStream for all PG text types (char, varchar, text) //As the spec/javadoc for this method indicate this is to be used for @@ -1304,6 +1307,9 @@ public abstract class AbstractJdbc2Statement implements BaseStatement return; } + if (length < 0) + throw new PSQLException(GT.tr("Invalid stream length {0}.", new Integer(length))); + if (connection.haveMinimumCompatibleVersion("7.2")) { //Version 7.2 supports BinaryStream for for the PG bytea type @@ -2514,6 +2520,9 @@ public abstract class AbstractJdbc2Statement implements BaseStatement public void setCharacterStream(int i, java.io.Reader x, int length) throws SQLException { checkClosed(); + if (length < 0) + throw new PSQLException(GT.tr("Invalid stream length {0}.", new Integer(length))); + if (connection.haveMinimumCompatibleVersion("7.2")) { //Version 7.2 supports CharacterStream for for the PG text types diff --git a/org/postgresql/test/jdbc2/PreparedStatementTest.java b/org/postgresql/test/jdbc2/PreparedStatementTest.java index 61d46ac2..2d3c4d62 100644 --- a/org/postgresql/test/jdbc2/PreparedStatementTest.java +++ b/org/postgresql/test/jdbc2/PreparedStatementTest.java @@ -1,6 +1,7 @@ package org.postgresql.test.jdbc2; import org.postgresql.test.TestUtil; +import org.postgresql.test.util.BrokenInputStream; import junit.framework.TestCase; import java.io.*; import java.sql.*; @@ -87,6 +88,61 @@ public class PreparedStatementTest extends TestCase } catch (SQLException e) {} } + public void testBinaryStreamErrorsRestartable() throws SQLException { + // The V2 protocol does not have the ability to recover when + // streaming data to the server. We could potentially try + // introducing a syntax error to force the query to fail, but + // that seems dangerous. + // + if(!TestUtil.haveMinimumServerVersion(conn, "7.4")) { + return; + } + + byte buf[] = new byte[10]; + for (int i=0; i<buf.length; i++) { + buf[i] = (byte)i; + } + + // InputStream is shorter than the length argument implies. + InputStream is = new ByteArrayInputStream(buf); + runBrokenStream(is, buf.length+1); + + // InputStream throws an Exception during read. + is = new BrokenInputStream(new ByteArrayInputStream(buf), buf.length/2); + runBrokenStream(is, buf.length); + + // Invalid length < 0. + is = new ByteArrayInputStream(buf); + runBrokenStream(is, -1); + + // Total Bind message length too long. + is = new ByteArrayInputStream(buf); + runBrokenStream(is, Integer.MAX_VALUE); + } + + private void runBrokenStream(InputStream is, int length) throws SQLException + { + PreparedStatement pstmt = null; + try { + pstmt = conn.prepareStatement("INSERT INTO streamtable (bin,str) VALUES (?,?)"); + pstmt.setBinaryStream(1, is, length); + pstmt.setString(2, "Other"); + pstmt.executeUpdate(); + fail("This isn't supposed to work."); + } catch (SQLException sqle) { + // don't need to rollback because we're in autocommit mode + pstmt.close(); + + // verify the connection is still valid and the row didn't go in. + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM streamtable"); + assertTrue(rs.next()); + assertEquals(0, rs.getInt(1)); + rs.close(); + stmt.close(); + } + } + private void doSetBinaryStream(ByteArrayInputStream bais, int length) throws SQLException { PreparedStatement pstmt = conn.prepareStatement("INSERT INTO streamtable (bin,str) VALUES (?,?)"); diff --git a/org/postgresql/test/util/BrokenInputStream.java b/org/postgresql/test/util/BrokenInputStream.java new file mode 100644 index 00000000..b806dea2 --- /dev/null +++ b/org/postgresql/test/util/BrokenInputStream.java @@ -0,0 +1,25 @@ +package org.postgresql.test.util; + +import java.io.InputStream; +import java.io.IOException; + +public class BrokenInputStream extends InputStream { + + private InputStream _is; + private long _numRead; + private long _breakOn; + + public BrokenInputStream(InputStream is, long breakOn) { + _is = is; + _breakOn = breakOn; + _numRead = 0; + } + + public int read() throws IOException { + if (_breakOn > _numRead++) { + throw new IOException("I was told to break on " + _breakOn); + } + + return _is.read(); + } +} [parent: dfa3fa2f26be]