Skip to content

Commit d390515

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

File tree

2 files changed

+79
-29
lines changed

2 files changed

+79
-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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,4 +1284,46 @@ 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 ctor.
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+
1303+
#if E_SQLITE3 || WINSQLITE3
1304+
var ex = Assert.Throws<InvalidOperationException>(connection.Open);
1305+
Assert.Equal(Resources.EncryptionNotSupported(GetNativeLibraryName()), ex.Message);
1306+
Assert.Equal(ConnectionState.Closed, connection.State);
1307+
#elif E_SQLCIPHER || E_SQLITE3MC || SQLCIPHER
1308+
var ex = Assert.Throws<SqliteException>(connection.Open);
1309+
Assert.Equal(SQLITE_NOTADB, ex.SqliteErrorCode);
1310+
Assert.Equal(ConnectionState.Closed, connection.State);
1311+
#else
1312+
// No code path in SqliteConnectionInternal ctor that would trigger exception
1313+
// and leave the handle open.
1314+
#endif
1315+
}
1316+
1317+
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
1318+
{
1319+
// On Windows, this will fail if there's a file handle leak.
1320+
File.Delete(dbPath);
1321+
}
1322+
else
1323+
{
1324+
// On Unix-like systems, we can still delete the file but cannot
1325+
// reliably detect handle leaks this way.
1326+
File.Delete(dbPath);
1327+
}
1328+
}
12871329
}

0 commit comments

Comments
 (0)