-
|
News has it that PQC is being integrated with .NET 10. I was excited and came here to read the code. Here's some curious finding/question/suggestion. In many cases the check for disposed object ( Is there official guidance on the order of checking for disposed object and verifying arguments? I'm eager to learn more about it. Yet without understanding the reasoning, for high-stake libraries such as crypto, checking for disposed object after verifying other arguments, especially if the verification of those arguments depends on the ex-object (state before disposal), keeps me up at night. (A perfunctory search suggests that argument verification might depend on the algorithm specifications, but not non-public state.) If there isn't a known guidance, I would argue for checking object disposal as the first thing done in public instance methods. This ensures no state-dependent logic is executed if object has been disposed, and I think that's generally good hygiene. In addition, the receiver (instance) is the implicit first argument, so if argument verification is done roughly in the order of parameter position, then the validity of |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 2 replies
-
|
An Now, the contents of the object may be in some indeterminate state, but generally that only applies to the unmanaged resources that necessitated the
If the checks are referencing |
Beta Was this translation helpful? Give feedback.
-
|
My default mental model for a disposable object is as follows: If the object has been disposed, then the only information about this object, for users of this object to know, is that the object has been disposed; Whether this is a good model is subject to discussion. Assuming this model is the right one, let's check it with commit public void SignData(ReadOnlySpan<byte> data, Span<byte> destination, ReadOnlySpan<byte> context = default)
{
int signatureSizeInBytes = Algorithm.SignatureSizeInBytes;
if (destination.Length != signatureSizeInBytes)
{
throw new ArgumentException(
SR.Format(SR.Argument_DestinationImprecise, signatureSizeInBytes),
nameof(destination));
}
if (context.Length > MaxContextLength)
{
throw new ArgumentOutOfRangeException(
nameof(context),
context.Length,
SR.Argument_SignatureContextTooLong255);
}
ThrowIfDisposed();
SignDataCore(data, context, destination);
}Suppose an To avoid revealing such information, one can call Of course, this particular, simple example doesn't appear harmful, because the cryptographic algorithm being used is public information. However, at the vastness of .NET libraries, it's not easy to verify that fields accessed prior to various scattered The above is just my model, and since what I observed in the official .NET libraries are somewhat different, it indicates
Hence the dual nature of question and suggestion. For clarification, to respond to the previous top-level comment:
The object should be in a determinate state, the cannot-be-used state.
Just because something can be done does not mean it's a good idea to do it. Suppose an object
I agree that
Not applicable to the intended meaning of my original post, but I do thank you for pointing out a natural language usage issue --- my original post:
should be changed to:
|
Beta Was this translation helpful? Give feedback.
-
|
We decided to do argument validation first for consistency.
public byte[] SignData(byte[] data, byte[]? context = null)
{
ThrowIfDisposed();
ArgumentNullException.ThrowIfNull(data);
return SignData(new ReadOnlySpan<byte>(data), new ReadOnlySpan<byte>(context));
}
public byte[] SignData(ReadOnlySpan<byte> data, ReadOnlySpan<byte> context)
{
if (context.Length > 255) throw ...;
ThrowIfDisposed();
...
}So that path makes the array path go through two public byte[] SignData(byte[] data, byte[]? context = null) => SignData(ROSpanOrThrow(data), context);
public byte[] SignData(ReadOnlySpan<byte> data, ReadOnlySpan<byte> context = default)
{
ThrowIfDisposed();
if (context.Length > 255) throw ...;
...
}Now, let's assume that we decided not to ship public static byte[] SignData(this MLDsa mldsa, byte[] data, byte[]? context = null)
{
ArgumentNullException.ThrowIfNull(mldsa);
ArgumentNullException.ThrowIfNull(data);
return mldsa.SignData(new ReadOnlySpan<byte>(data), context);
}In these latter two cases, the null check for an array happens before the dispose, but the check for context.Length happens after. It's inconsistent, and therefore bad. The extension method approach also aligns with wrapper instances, where the true disposal state lies in the wrapped object. Wrapper types can independently track disposal, but that means they're just tracking it to promote the ODE above the AOORE. An annoying amount of work for a thing that, in general, shouldn't ever matter. So what we decided on was:
If we queried parameter set information repeatedly, it would depend on the disposed state. But since any instance of MLDsa can only ever have one parameter set, we capture it at construction time and don't let it go. That moves it to "before disposed". Sort of stateful, sort of stateless. We also decided in some cases to skip disposed checks when cascading from one method to another. But, again, from the model of "what could an extension method do?" public static byte[] SignData(this MLDsa mldsa, ReadOnlySpan<byte> data, ReadOnlySpan<byte> context = default)
{
ArgumentNullException.ThrowIfNull(mldsa);
if (context.Length > 255) throw ...;
byte[] ret = new byte[mldsa.Algorithm.SignatureSizeInBytes];
mldsa.SignData(data, ret, context);
return ret;
}That array gets allocated if we don't reject At the end of the day, the best model, therefore, is "what could an extension method do?" Unless we decide to pattern attach |
Beta Was this translation helpful? Give feedback.
We decided to do argument validation first for consistency.
public byte[] SignData(byte[] data, byte[] context)wants to defer toSignData(ReadOnlySpan<byte> data, ReadOnlySpan<byte> context), but do null-checks. If Dispose is first, then it looks likeSo that path makes the array path go through two
ThrowIfDisposedif we str…