Skip to content

Commit edecf8e

Browse files
committed
Fix handle leak
1 parent 8fdfbad commit edecf8e

File tree

2 files changed

+75
-29
lines changed

2 files changed

+75
-29
lines changed

src/Microsoft.Data.Sqlite.Core/SqliteConnectionInternal.cs

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -97,46 +97,54 @@ public SqliteConnectionInternal(SqliteConnectionStringBuilder connectionOptions,
9797
? connectionOptions.Vfs
9898
: null;
9999
var rc = sqlite3_open_v2(filename, out _db, flags, vfs: vfs);
100-
SqliteException.ThrowExceptionForRC(rc, _db);
101-
102-
if (connectionOptions.Password.Length != 0)
100+
try
103101
{
104-
if (SQLitePCLExtensions.EncryptionSupported(out var libraryName) == false)
102+
SqliteException.ThrowExceptionForRC(rc, _db);
103+
104+
if (connectionOptions.Password.Length != 0)
105105
{
106-
throw new InvalidOperationException(Resources.EncryptionNotSupported(libraryName));
106+
if (SQLitePCLExtensions.EncryptionSupported(out var libraryName) == false)
107+
{
108+
throw new InvalidOperationException(Resources.EncryptionNotSupported(libraryName));
109+
}
110+
111+
// NB: SQLite doesn't support parameters in PRAGMA statements, so we escape the value using the
112+
// quote function before concatenating.
113+
var quotedPassword = ExecuteScalar(
114+
"SELECT quote($password);",
115+
connectionOptions.Password,
116+
connectionOptions.DefaultTimeout);
117+
ExecuteNonQuery(
118+
"PRAGMA key = " + quotedPassword + ";",
119+
connectionOptions.DefaultTimeout);
120+
121+
if (SQLitePCLExtensions.EncryptionSupported() != false)
122+
{
123+
// NB: Forces decryption. Throws when the key is incorrect.
124+
ExecuteNonQuery(
125+
"SELECT COUNT(*) FROM sqlite_master;",
126+
connectionOptions.DefaultTimeout);
127+
}
107128
}
108129

109-
// NB: SQLite doesn't support parameters in PRAGMA statements, so we escape the value using the
110-
// quote function before concatenating.
111-
var quotedPassword = ExecuteScalar(
112-
"SELECT quote($password);",
113-
connectionOptions.Password,
114-
connectionOptions.DefaultTimeout);
115-
ExecuteNonQuery(
116-
"PRAGMA key = " + quotedPassword + ";",
117-
connectionOptions.DefaultTimeout);
118-
119-
if (SQLitePCLExtensions.EncryptionSupported() != false)
130+
if (connectionOptions.ForeignKeys.HasValue)
120131
{
121-
// NB: Forces decryption. Throws when the key is incorrect.
122132
ExecuteNonQuery(
123-
"SELECT COUNT(*) FROM sqlite_master;",
133+
"PRAGMA foreign_keys = " + (connectionOptions.ForeignKeys.Value ? "1" : "0") + ";",
124134
connectionOptions.DefaultTimeout);
125135
}
126-
}
127136

128-
if (connectionOptions.ForeignKeys.HasValue)
129-
{
130-
ExecuteNonQuery(
131-
"PRAGMA foreign_keys = " + (connectionOptions.ForeignKeys.Value ? "1" : "0") + ";",
132-
connectionOptions.DefaultTimeout);
137+
if (connectionOptions.RecursiveTriggers)
138+
{
139+
ExecuteNonQuery(
140+
"PRAGMA recursive_triggers = 1;",
141+
connectionOptions.DefaultTimeout);
142+
}
133143
}
134-
135-
if (connectionOptions.RecursiveTriggers)
144+
catch
136145
{
137-
ExecuteNonQuery(
138-
"PRAGMA recursive_triggers = 1;",
139-
connectionOptions.DefaultTimeout);
146+
_db.Dispose();
147+
throw;
140148
}
141149

142150
_pool = pool;

test/Microsoft.Data.Sqlite.Tests/SqliteConnectionTest.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,4 +1284,42 @@ public void GetSchema_ReservedWords_works()
12841284
dataTable.Rows.Cast<DataRow>(),
12851285
r => (string)r[DbMetaDataColumnNames.ReservedWord] == "SELECT");
12861286
}
1287+
1288+
[Fact]
1289+
public void Open_releases_handle_when_constructor_fails()
1290+
{
1291+
var dbPath = Path.GetTempFileName();
1292+
1293+
// Create a file with invalid database content
1294+
File.WriteAllText(dbPath, "this is not a database file but should still open with sqlite3_open_v2");
1295+
1296+
// Use password to trigger the encryption path in SqliteConnectionInternal constructor
1297+
// This should fail during password verification when trying to decrypt the invalid file
1298+
var connectionString = $"Data Source={dbPath};Password=test;Mode=ReadOnly;Pooling=False";
1299+
1300+
using (var connection = new SqliteConnection(connectionString))
1301+
{
1302+
#if E_SQLCIPHER || E_SQLITE3MC || SQLCIPHER
1303+
var ex = Assert.Throws<SqliteException>(connection.Open);
1304+
Assert.Equal(SQLITE_NOTADB, ex.SqliteErrorCode);
1305+
#else
1306+
var ex = Assert.Throws<InvalidOperationException>(connection.Open);
1307+
Assert.Contains("password", ex.Message.ToLowerInvariant());
1308+
#endif
1309+
1310+
Assert.Equal(ConnectionState.Closed, connection.State);
1311+
}
1312+
1313+
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
1314+
{
1315+
// On Windows, this will fail if there's a file handle leak
1316+
File.Delete(dbPath);
1317+
}
1318+
else
1319+
{
1320+
// On Unix-like systems, we can still delete the file but cannot
1321+
// reliably detect handle leaks this way
1322+
File.Delete(dbPath);
1323+
}
1324+
}
12871325
}

0 commit comments

Comments
 (0)