Skip to content

Commit 8279746

Browse files
fix: address all code review issues in BOLT protocol
- CRITICAL: Remove database.close() call in cleanup() - database is managed by server - CRITICAL: Add logging for all ResultSet closure failures - HIGH: Add position validation in ridToId() to prevent overflow - HIGH: Fix authentication error handling to send failure message to client - HIGH: Document precision loss for BigInteger/BigDecimal conversions - MEDIUM: Add database health check in handleReset() to force re-auth if needed - MEDIUM: Add rollback attempt in transaction exception handler - MEDIUM: Sanitize authentication error messages to prevent information disclosure - LOW: Add query analysis failure logging with improved verbosity control - LOW: Add test cases for database persistence, connection limits, and path structures Co-authored-by: Luca Garulli <lvca@users.noreply.github.com>
1 parent da36fe5 commit 8279746

File tree

3 files changed

+139
-19
lines changed

3 files changed

+139
-19
lines changed

bolt/src/main/java/com/arcadedb/bolt/BoltNetworkExecutor.java

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ private void handleReset() throws IOException {
355355
try {
356356
currentResultSet.close();
357357
} catch (final Exception e) {
358-
// Ignore
358+
LogManager.instance().log(this, Level.WARNING, "Failed to close ResultSet during RESET", e);
359359
}
360360
}
361361

@@ -374,7 +374,13 @@ private void handleReset() throws IOException {
374374
firstResult = null;
375375

376376
sendSuccess(Map.of());
377-
state = State.READY;
377+
378+
// Check database health and force re-authentication if database is unavailable
379+
if (database == null || !database.isOpen()) {
380+
state = State.AUTHENTICATION;
381+
} else {
382+
state = State.READY;
383+
}
378384
}
379385

380386
/**
@@ -523,7 +529,7 @@ private void handlePull(final PullMessage message) throws IOException {
523529
try {
524530
currentResultSet.close();
525531
} catch (final Exception e) {
526-
// Ignore
532+
LogManager.instance().log(this, Level.WARNING, "Failed to close ResultSet during PULL completion", e);
527533
}
528534
currentResultSet = null;
529535
currentFields = null;
@@ -565,7 +571,7 @@ private void handleDiscard(final DiscardMessage discardMessage) throws IOExcepti
565571
try {
566572
currentResultSet.close();
567573
} catch (final Exception e) {
568-
// Ignore
574+
LogManager.instance().log(this, Level.WARNING, "Failed to close ResultSet during DISCARD", e);
569575
}
570576
}
571577

@@ -613,6 +619,14 @@ private void handleBegin(final BeginMessage beginMessage) throws IOException {
613619
state = State.TX_READY;
614620

615621
} catch (final Exception e) {
622+
// Attempt to rollback in case transaction was partially started
623+
try {
624+
if (database != null) {
625+
database.rollback();
626+
}
627+
} catch (final Exception rollbackError) {
628+
LogManager.instance().log(this, Level.WARNING, "Failed to rollback after BEGIN error", rollbackError);
629+
}
616630
final String errorMsg = e.getMessage() != null ? e.getMessage() : "Transaction error";
617631
sendFailure(BoltException.TRANSACTION_ERROR, errorMsg);
618632
state = State.FAILED;
@@ -673,7 +687,7 @@ private void handleRollback() throws IOException {
673687
try {
674688
currentResultSet.close();
675689
} catch (final Exception e) {
676-
// Ignore
690+
LogManager.instance().log(this, Level.WARNING, "Failed to close ResultSet during ROLLBACK", e);
677691
}
678692
}
679693
if (database != null) {
@@ -810,7 +824,10 @@ private boolean isWriteQuery(final String query) {
810824
return !database.getQueryEngine("opencypher").analyze(query).isIdempotent();
811825
} catch (final Exception e) {
812826
// If analysis fails, assume it's a write operation to be safe
813-
LogManager.instance().log(this, Level.WARNING, "Failed to analyze query, assuming write operation: " + query, e);
827+
// Log at FINE level to avoid spam for complex but valid queries
828+
LogManager.instance().log(this, Level.FINE,
829+
"Query analysis failed for: " + (query.length() > 100 ? query.substring(0, 100) + "..." : query) +
830+
" - assuming write operation", e);
814831
return true;
815832
}
816833
}
@@ -832,19 +849,22 @@ private String generateBookmark() {
832849
*/
833850
private boolean authenticateUser(final String principal, final String credentials) throws IOException {
834851
if (principal == null || credentials == null) {
852+
sendFailure(BoltException.AUTHENTICATION_ERROR, "Missing credentials");
853+
state = State.FAILED;
835854
return false;
836855
}
837856

838857
try {
839858
user = server.getSecurity().authenticate(principal, credentials, databaseName);
840859
if (user == null) {
841-
sendFailure(BoltException.AUTHENTICATION_ERROR, "Invalid credentials");
860+
sendFailure(BoltException.AUTHENTICATION_ERROR, "Authentication failed");
842861
state = State.FAILED;
843862
return false;
844863
}
845864
return true;
846865
} catch (final ServerSecurityException e) {
847-
sendFailure(BoltException.AUTHENTICATION_ERROR, e.getMessage());
866+
// Sanitize error message to avoid information disclosure
867+
sendFailure(BoltException.AUTHENTICATION_ERROR, "Authentication failed");
848868
state = State.FAILED;
849869
return false;
850870
}
@@ -904,7 +924,7 @@ private void cleanup() {
904924
currentResultSet = null;
905925
}
906926
} catch (final Exception e) {
907-
// Ignore
927+
LogManager.instance().log(this, Level.WARNING, "Failed to close ResultSet during cleanup", e);
908928
}
909929

910930
try {
@@ -915,14 +935,9 @@ private void cleanup() {
915935
// Ignore
916936
}
917937

918-
try {
919-
if (database != null) {
920-
database.close();
921-
database = null;
922-
}
923-
} catch (final Exception e) {
924-
// Ignore
925-
}
938+
// Database is managed by the server - just release our reference
939+
// DO NOT close the shared database instance
940+
database = null;
926941

927942
try {
928943
socket.close();

bolt/src/main/java/com/arcadedb/bolt/structure/BoltStructureMapper.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,19 @@ private static Map<String, Object> toMap(final Map<?, ?> map) {
280280

281281
/**
282282
* Convert a Number to PackStream-compatible number.
283+
* <p>
284+
* <strong>Precision Loss Warning:</strong> BigInteger values that exceed Long.MAX_VALUE
285+
* and BigDecimal values are converted to double, which may lose precision for very large
286+
* or very precise numbers. This is a limitation of the BOLT protocol's numeric type system.
287+
* <p>
288+
* For example:
289+
* <ul>
290+
* <li>BigInteger larger than 2^63-1 will lose precision when converted to double</li>
291+
* <li>BigDecimal with high precision will be rounded to double precision (~15-17 digits)</li>
292+
* </ul>
293+
*
294+
* @param number the number to convert
295+
* @return a Long or Double compatible with BOLT PackStream
283296
*/
284297
private static Object toNumber(final Number number) {
285298
if (number instanceof Byte || number instanceof Short || number instanceof Integer || number instanceof Long) {
@@ -289,14 +302,16 @@ private static Object toNumber(final Number number) {
289302
return number.doubleValue();
290303
}
291304
if (number instanceof BigInteger bigInt) {
292-
// Try to fit in long, otherwise convert to double
305+
// Try to fit in long, otherwise convert to double (with potential precision loss)
293306
try {
294307
return bigInt.longValueExact();
295308
} catch (final ArithmeticException e) {
309+
// Precision loss: BigInteger too large for long, converting to double
296310
return bigInt.doubleValue();
297311
}
298312
}
299313
if (number instanceof BigDecimal bigDec) {
314+
// Precision loss: BigDecimal always converted to double
300315
return bigDec.doubleValue();
301316
}
302317
// Default to double
@@ -316,7 +331,14 @@ public static long ridToId(final RID rid) {
316331
if (bucketId < 0 || bucketId > 0xFFFF) {
317332
throw new IllegalArgumentException("Bucket ID out of range for BOLT ID conversion: " + bucketId);
318333
}
334+
335+
// Validate position to prevent overflow (max 48 bits)
336+
final long position = rid.getPosition();
337+
if (position < 0 || position > 0xFFFFFFFFFFFFL) {
338+
throw new IllegalArgumentException("Position out of range for BOLT ID conversion: " + position);
339+
}
340+
319341
// Combine bucket ID (high bits) and position (low bits)
320-
return ((long) bucketId << 48) | (rid.getPosition() & 0xFFFFFFFFFFFFL);
342+
return ((long) bucketId << 48) | position;
321343
}
322344
}

bolt/src/test/java/com/arcadedb/bolt/BoltProtocolIT.java

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,4 +981,87 @@ void testMultipleErrorsInSequence() {
981981
}
982982
}
983983
}
984+
985+
@Test
986+
void testDatabasePersistsAfterConnectionClose() {
987+
// CRITICAL TEST: Verify that closing one connection doesn't close the database for others
988+
// This test catches the bug where database.close() was incorrectly called in cleanup()
989+
try (Driver driver = getDriver()) {
990+
// Session 1: Create some data
991+
try (Session session1 = driver.session(SessionConfig.forDatabase(getDatabaseName()))) {
992+
session1.run("CREATE (n:PersistenceTest {id: 1, name: 'test'})").consume();
993+
} // session1 closes here
994+
995+
// Session 2: Verify the data still exists and database is still open
996+
try (Session session2 = driver.session(SessionConfig.forDatabase(getDatabaseName()))) {
997+
Result result = session2.run("MATCH (n:PersistenceTest {id: 1}) RETURN n.name AS name");
998+
assertThat(result.hasNext()).isTrue();
999+
assertThat(result.next().get("name").asString()).isEqualTo("test");
1000+
}
1001+
1002+
// Session 3: Verify database is still accessible after multiple connection cycles
1003+
try (Session session3 = driver.session(SessionConfig.forDatabase(getDatabaseName()))) {
1004+
Result result = session3.run("MATCH (n:PersistenceTest) RETURN count(n) AS cnt");
1005+
assertThat(result.next().get("cnt").asLong()).isGreaterThanOrEqualTo(1L);
1006+
}
1007+
}
1008+
}
1009+
1010+
@Test
1011+
void testPathStructure() {
1012+
// Test returning path structures from Cypher queries
1013+
try (Driver driver = getDriver()) {
1014+
try (Session session = driver.session(SessionConfig.forDatabase(getDatabaseName()))) {
1015+
// Create a simple path: (a)-[:REL]->(b)
1016+
session.run("CREATE (a:PathNode {name: 'start'})-[:CONNECTS_TO {weight: 1.0}]->(b:PathNode {name: 'end'})").consume();
1017+
1018+
// Query for the path
1019+
Result result = session.run("MATCH p = (a:PathNode {name: 'start'})-[r:CONNECTS_TO]->(b:PathNode {name: 'end'}) RETURN p");
1020+
assertThat(result.hasNext()).isTrue();
1021+
1022+
org.neo4j.driver.Record record = result.next();
1023+
// Note: Path support depends on OpenCypher implementation
1024+
// At minimum, verify the query doesn't throw an error
1025+
assertThat(record).isNotNull();
1026+
}
1027+
}
1028+
}
1029+
1030+
@Test
1031+
void testConnectionLimit() {
1032+
// Test that connection limiting works if configured
1033+
final int maxConnections = GlobalConfiguration.BOLT_MAX_CONNECTIONS.getValueAsInteger();
1034+
1035+
// Only run this test if connection limiting is enabled
1036+
if (maxConnections > 0 && maxConnections < 100) {
1037+
List<Driver> drivers = new ArrayList<>();
1038+
try {
1039+
// Create connections up to the limit
1040+
for (int i = 0; i < maxConnections; i++) {
1041+
Driver driver = getDriver();
1042+
driver.verifyConnectivity();
1043+
drivers.add(driver);
1044+
}
1045+
1046+
// Attempt to create one more connection beyond the limit
1047+
// This should either be rejected or one of the existing connections should be replaced
1048+
try (Driver extraDriver = getDriver()) {
1049+
// If we get here, the server either increased the limit or is using connection pooling
1050+
extraDriver.verifyConnectivity();
1051+
} catch (Exception e) {
1052+
// Connection was rejected due to limit - this is expected behavior
1053+
assertThat(e.getMessage()).contains("connection");
1054+
}
1055+
} finally {
1056+
// Clean up all drivers
1057+
for (Driver driver : drivers) {
1058+
try {
1059+
driver.close();
1060+
} catch (Exception e) {
1061+
// Ignore cleanup errors
1062+
}
1063+
}
1064+
}
1065+
}
1066+
}
9841067
}

0 commit comments

Comments
 (0)