-
Notifications
You must be signed in to change notification settings - Fork 866
Description
Hello!
I use npgsql v9 and I get strange behavior when getting NULL value from DB bigint column and reading it with reader.GetFieldValue<long?>(). I sometimes get nullable variable with HasValue == false but Value is some random value. Normally, I can't get it by just myVar.Value because .NET Nullable struct denies it:
but if I use myVar.GetValueOrDefault() then I directly get private field value without any validations:
so if there is some value inside then I just get it as it is default value of myVar.
.NET normally doesn't allow me to create this kind of broken Nullable, so it could be done only in unsafe operations or with reflection.
I believe that it's not bug of MS .NET because I have this behavior in npgsql 9.0.0 and newer but I don't have this issue in versions 8.0.7 or older.
Reproduction:
I used MacOS running on M2 Arm CPU, but it it also can be reproduced in MS docker image with sdk:9.0. For Postgres I use postgres:14-alpine image.
- Create some DB, you don't even need to create any tables
- Write the following method to perform queries in DB:
public async Task<IReadOnlyCollection<long?>> GetData() { await using var connection = new NpgsqlConnection(_connectionString); await connection.OpenAsync(); // just take NULL value from DB await using var command = new NpgsqlCommand("SELECT NULL::bigint AS value1;", connection); await using var reader = await command.ExecuteReaderAsync(); var data = new List<long?>(); while (await reader.ReadAsync()) { data.Add(reader.GetFieldValue<long?>(0)); } return data; }
- And code to call it:
Note that I call that query in infinite loop.
public async Task Success() { var repository = new Repository(); var i = 0; while (true) { var result = await repository.GetData(); foreach (var data in result) { var d = data.GetValueOrDefault(); outputHelper.WriteLine("{0}: `{1}` / {2}, `{3}`", i, data, d, data.HasValue); if (d != 0) { outputHelper.WriteLine($"Unexpected value: {d} / {d:X} / {d:b8}"); throw new Exception("We are here"); } } i++; } }
- The result will be the following:
0: `` / 0, `False` 1: `` / 0, `False` 2: `` / 0, `False` 3: `` / 0, `False` ... 227: `` / 0, `False` 228: `` / 0, `False` 229: `` / 0, `False` 230: `` / 4403988152, `False` Unexpected value: 4403988152 / 1067F86B8 / 100000110011111111000011010111000 System.Exception "We are here"
Count of iterations before error is always different but it is always under 1000 (even under 300). Unexpected value is always different too, and there is no order except that their hex representation always ends with B8.
Interesting is that when I use GetFieldValueAsync() I don't get this error. When I run the code under debugger I also can't get the error.
And that make me believe that this error connected to unsafe casting of BD data to .NET nullable value types in some kind of race condition when internal value of nullable value type can be set to random value (maybe memory address itself or some old value in process memory).
Please could you take a look at this issue.

