Azure SDK für .NET: Die Geschichte eines schwierigen Fehlersuchers

Bild 2

Als wir uns entschieden, nach Fehlern im Azure SDK für .NET-Projekt zu suchen, waren wir angenehm überrascht von seiner Größe. "Dreieinhalb Millionen Codezeilen", sagten wir und studierten die Statistiken des Projekts. So viel kannst du dort finden. Aber leider, ah. Das Projekt erwies sich als geheim. Was ist die Besonderheit des Projekts und wie es getestet wurde - lesen Sie diesen Artikel.

Über das Projekt


Ich schreibe diesen Artikel in einer Fortsetzung meines vorherigen Artikels, in dem es auch um ein Projekt im Zusammenhang mit Microsoft Azure ging: Azure PowerShell: "Im Grunde genommen harmlos". Diesmal rechnete ich also mit einer soliden Anzahl von abwechslungsreichen und interessanten Fehlern. In der Tat ist für eine statische Analyse in der Regel die Größe des Projekts wichtig, insbesondere bei einmaligen Überprüfungen des gesamten Projekts. Ja, in der Praxis machen sie das normalerweise nicht. Und wenn doch, dann erst in der Phase der Implementierung des Analysators. Gleichzeitig versteht niemand sofort die große Anzahl von Vorgängen (eine beträchtliche Anzahl von Warnungen ist die Norm, wenn der Analysator in diesem Modus startet), sondern verschuldet sie einfach mit Hilfe von Nachrichtenunterdrückungsmechanismen und deren Speicherung in speziellen Datenbanken (Massenunterdrückung). Wir führen einmalige Inspektionen zu Forschungszwecken durch. Daher sind große Studienprojekte immer kleinen vorzuziehen.

Das Azure SDK für .NET-Projekt zeigte jedoch sofort, dass es als Testumgebung fehlgeschlagen ist. Auch seine beeindruckende Größe hat nicht geholfen, sondern die Arbeit sogar kompliziert. Der Grund ist in der folgenden Projektstatistik angegeben:

  • Quelldateien .cs (ohne Tests): 16.500
  • Visual Studio-Lösungen (.sln): 163
  • Nicht leere Codezeilen: 3.462.000
  • Davon automatisch generiert: rund 3,3 Millionen
  • Das Projekt-Repository ist auf GitHub verfügbar.

Etwa 95% des Codes werden automatisch generiert, ein wesentlicher Teil dieses Codes wird mehrmals wiederholt. Das Überprüfen solcher Projekte mit einem statischen Analysator ist normalerweise zeitaufwändig und nutzlos, da viel gearbeitet wird, aber unlogischer (auf den ersten Blick) und redundanter Code. Dies führt zu einer Vielzahl von Fehlalarmen.

Eine große Anzahl von Visual Studio-Lösungen (163) wurden als Kirschen auf dem Kuchen verwendet, wonach diese Codemasse "verschmiert" wurde. Um den verbleibenden Code zu überprüfen (nicht automatisch generiert), musste ich einige Anstrengungen unternehmen. Es hat geholfen, dass sich der gesamte automatisch generierte Code in den Lösungsunterordnern entlang des relativen Pfads "<Lösungsordner> \ src \ Generated" befindet. Außerdem enthält jede .cs-Datei eines solchen Codes einen speziellen Kommentar im <auto-generated> -Tag :

// <auto-generated> // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for // license information. // // Code generated by Microsoft (R) AutoRest Code Generator. // Changes may cause incorrect behavior and will be lost if the code is // regenerated. // </auto-generated> 

Für die Reinheit des Experiments überprüfte ich zufällig ungefähr zehn zufällig ausgewählte Lösungen mit automatisch erzeugtem Code. Die Ergebnisse werden niedriger sein.

Trotz der geringen Menge des verbleibenden "ehrlichen" Codes konnte ich dort dennoch eine bestimmte Anzahl von Fehlern finden. Dieses Mal werde ich keine Auslösungen in der Reihenfolge der Nummern der PVS-Studio-Diagnose geben. Stattdessen werde ich die Antworten nach den Lösungen gruppieren, in denen sie erkannt wurden.

Mal sehen, was ich im Azure SDK für .NET-Code gefunden habe.

Microsoft.Azure.Management.Advisor


Dies ist nur eine von vielen Lösungen, die automatisch generierten Code enthalten. Wie ich oben sagte, wurden ungefähr ein Dutzend solcher Lösungen selektiv getestet. Und überall waren die Nachrichten gleich und erwartungsgemäß nutzlos. Lassen Sie mich einige Beispiele für solche Antworten nennen.

V3022 Der Ausdruck 'Credentials! = Null' ist immer wahr. AdvisorManagementClient.cs 204

 // Code generated by Microsoft (R) AutoRest Code Generator. .... public ServiceClientCredentials Credentials { get; private set; } .... public AdvisorManagementClient(ServiceClientCredentials credentials, params DelegatingHandler[] handlers) : this(handlers) { if (credentials == null) { throw new System.ArgumentNullException("credentials"); } Credentials = credentials; if (Credentials != null) // <= { Credentials.InitializeServiceClient(this); } } 

Offensichtlich ist der Code redundant und die Überprüfung der Berechtigungsnachweise! = Null ist nutzlos. Aber der Code funktioniert. Und automatisch generiert. Daher - keine Beschwerden.

V3022 Der Ausdruck '_queryParameters.Count> 0' ist immer falsch. ConfigurationsOperations.cs 871

 // Code generated by Microsoft (R) AutoRest Code Generator. .... public async Task<AzureOperationResponse<IPage<ConfigData>>> ListBySubscriptionNextWithHttpMessagesAsync(....) { .... List<string> _queryParameters = new List<string>(); if (_queryParameters.Count > 0) { .... } .... } 

Und wieder ein scheinbar logikloses Design. Aus irgendeinem Grund überprüfen sie die Größe der gerade erstellten leeren Liste. In der Tat - alles ist in Ordnung. Diese Prüfung ist jetzt sinnlos, aber wenn der Generator die Erstellung einer Liste z. B. auf der Grundlage einer anderen Sammlung aufbaut, ist die Prüfung bereits sinnvoll. Auch hier gibt es natürlich keine Beschwerden gegen den Code, da er seinen Ursprung hat.

Für jede Lösung mit automatisch generiertem Code wurden Hunderte ähnlicher Warnungen empfangen. In Anbetracht ihrer Sinnlosigkeit halte ich eine weitere Erörterung derartiger positiver Aspekte für sinnlos. Im Folgenden werden nur echte Fehler im „normalen“ Code berücksichtigt.

Azure.Core


V3001 Es gibt identische Unterausdrücke 'buffer.Length' links und rechts vom Operator '<'. AzureBaseBuffersExtensions.cs 30

 public static async Task WriteAsync(...., ReadOnlyMemory<byte> buffer, ....) { byte[]? array = null; .... if (array == null || buffer.Length < buffer.Length) // <= { if (array != null) ArrayPool<byte>.Shared.Return(array); array = ArrayPool<byte>.Shared.Rent(buffer.Length); } if (!buffer.TryCopyTo(array)) throw new Exception("could not rent large enough buffer."); .... } 

Es wurde ein Fehler in der Bedingung gemacht, wahrscheinlich aufgrund der Verwendung von Copy-Paste. Gemessen an der Tatsache, dass weiter im Codepuffer in das Array kopiert wird , sollte die Prüfung wie folgt aussehen:
 if (array == null || array.Length < buffer.Length) 

Aber, wie ich immer sage, sollte der Autor des Codes solche Fehler beheben.

V3083 Unsicherer Aufruf des Ereignisses '_onChange', NullReferenceException ist möglich. Überlegen Sie, ob Sie einer lokalen Variablen ein Ereignis zuweisen möchten, bevor Sie sie aufrufen. ClientOptionsMonitor.cs 44

 private event Action<TOptions, string> _onChange; .... private void InvokeChanged(....) { .... if (_onChange != null) { _onChange.Invoke(options, name); } } 

Unkritisch, aber ein Fehler. Zwischen der Überprüfung des Ereignisses auf Null und seinem Aufruf kann das Ereignis abgemeldet werden. Dann wird die Variable _onChange auf null gesetzt und eine Ausnahme ausgelöst. Code muss sicherer umgeschrieben werden. Zum Beispiel so:

 private void InvokeChanged(....) { .... _onChange?.Invoke(options, name); } 

Azure.Messaging.EventHubs


V3080 Mögliche Null-Dereferenzierung. Sehen Sie sich 'eventPropertyValue' an. AmqpMessageConverter.cs 650

 private static bool TryCreateEventPropertyForAmqpProperty( object amqpPropertyValue, out object eventPropertyValue) { eventPropertyValue = null; .... switch (GetTypeIdentifier(amqpPropertyValue)) { case AmqpProperty.Type.Byte: .... case AmqpProperty.Type.String: eventPropertyValue = amqpPropertyValue; return true; .... } .... switch (amqpPropertyValue) { case AmqpSymbol symbol: eventPropertyValue = ....; break; case byte[] array: eventPropertyValue = ....; break; case ArraySegment<byte> segment when segment.Count == segment.Array.Length: eventPropertyValue = ....; break; case ArraySegment<byte> segment: .... eventPropertyValue = ....; break; case DescribedType described when (described.Descriptor is AmqpSymbol): eventPropertyValue = ....; break; default: var exception = new SerializationException( string.Format(...., eventPropertyValue.GetType().FullName)); // <= .... } return (eventPropertyValue != null); } 

Verfolgen wir, was mit dem Wert der Variablen eventPropertyValue im obigen Codeausschnitt geschieht. Zu Beginn der Methode ist die Variable null . Ferner wird in einer der Bedingungen des ersten Schalters die Variable initialisiert, wonach das Verfahren beendet wird. Der zweite Schalterblock enthält viele Bedingungen, in denen die Variable auch einen neuen Wert erhält. Im Standardblock wird die Variable eventPropertyValue jedoch einfach ohne Überprüfung verwendet. Dies ist ein Fehler, da die Variable derzeit null ist .

V3066 Mögliche falsche Reihenfolge der Argumente, die an den Konstruktor 'EventHubConsumer' übergeben wurden: 'partitionId' und 'consumerGroup'. TrackOneEventHubClient.cs 394

 public override EventHubConsumer CreateConsumer(....) { return new EventHubConsumer ( new TrackOneEventHubConsumer(....), TrackOneClient.EventHubName, partitionId, // <= 3 consumerGroup, // <= 4 eventPosition, consumerOptions, initialRetryPolicy ); } 

Der Analysator vermutete, dass beim Aufrufen des Konstruktors der EventHubConsumer- Klasse die Reihenfolge der dritten und vierten Argumente vertauscht wurde. Schauen wir uns die Konstruktordeklaration an:

 internal EventHubConsumer(TransportEventHubConsumer transportConsumer, string eventHubName, string consumerGroup, // <= 3 string partitionId, // <= 4 EventPosition eventPosition, EventHubConsumerOptions consumerOptions, EventHubRetryPolicy retryPolicy) { .... } 

Die Argumente sind wirklich durcheinander. Ich vermute, wie dieser Fehler gemacht wurde. Der Fehler ist wahrscheinlich die schlechte Formatierung des Codes. Schauen Sie sich die EventHubConsumer- Konstruktordeklaration noch einmal an. Aufgrund der Tatsache, dass sich der erste transportConsumer- Parameter in der gleichen Zeile wie der Klassenname befindet, scheint der partitionId- Parameter bei einer kurzen Betrachtung des Codes an dritter und nicht an vierter Stelle zu stehen (meine Kommentare mit den Seriennummern der Parameter sind im Originalcode nicht enthalten) )

Dies ist nur eine Vermutung, aber ich würde die Formatierung des Konstruktordeklarationscodes folgendermaßen ändern:

 internal EventHubConsumer ( TransportEventHubConsumer transportConsumer, string eventHubName, string consumerGroup, string partitionId, EventPosition eventPosition, EventHubConsumerOptions consumerOptions, EventHubRetryPolicy retryPolicy) { .... } 

Azure.Storage


V3112 Eine Abnormalität in ähnlichen Vergleichen. Es ist möglich, dass der Ausdruck 'ContentLanguage == other.ContentEncoding' einen Tippfehler enthält. BlobSasBuilder.cs 410

 public struct BlobSasBuilder : IEquatable<BlobSasBuilder> { .... public bool Equals(BlobSasBuilder other) => BlobName == other.BlobName && CacheControl == other.CacheControl && BlobContainerName == other.BlobContainerName && ContentDisposition == other.ContentDisposition && ContentEncoding == other.ContentEncoding && // <= ContentLanguage == other.ContentEncoding && // <= ContentType == other.ContentType && ExpiryTime == other.ExpiryTime && Identifier == other.Identifier && IPRange == other.IPRange && Permissions == other.Permissions && Protocol == other.Protocol && StartTime == other.StartTime && Version == other.Version; } 

Ein Fehler, der durch Unaufmerksamkeit begangen wurde. Einen ähnlichen Fehler bei der Codeüberprüfung zu finden, ist ziemlich schwierig. Die richtige Prüfoption:

  .... ContentEncoding == other.ContentEncoding && ContentLanguage == other.ContentLanguage && .... 

V3112 Eine Abnormalität in ähnlichen Vergleichen. Es ist möglich, dass der Ausdruck 'ContentLanguage == other.ContentEncoding' einen Tippfehler enthält. FileSasBuilder.cs 265

 public struct FileSasBuilder : IEquatable<FileSasBuilder> { .... public bool Equals(FileSasBuilder other) => CacheControl == other.CacheControl && ContentDisposition == other.ContentDisposition && ContentEncoding == other.ContentEncoding // <= && ContentLanguage == other.ContentEncoding // <= && ContentType == other.ContentType && ExpiryTime == other.ExpiryTime && FilePath == other.FilePath && Identifier == other.Identifier && IPRange == other.IPRange && Permissions == other.Permissions && Protocol == other.Protocol && ShareName == other.ShareName && StartTime == other.StartTime && Version == other.Version ; 

Genau derselbe Fehler in einem sehr ähnlichen Code. Der Code wurde wahrscheinlich kopiert und teilweise geändert. Der Fehler blieb jedoch bestehen.

Microsoft.Azure.Batch


V3053 Ein übermäßiger Ausdruck. Untersuchen Sie die Teilzeichenfolgen 'IList' und 'List'. PropertyData.cs 157

V3053 Ein übermäßiger Ausdruck. Untersuchen Sie die Teilzeichenfolgen 'List' und 'IReadOnlyList'. PropertyData.cs 158

 public class PropertyData { .... public bool IsTypeCollection => this.Type.Contains("IList") || this.Type.Contains("IEnumerable") || this.Type.Contains("List") || // <= this.Type.Contains("IReadOnlyList"); // <= } 

Der Analysator gab zwei Warnungen über sinnlose oder fehlerhafte Überprüfungen aus. Im ersten Fall erscheint die Suche nach der Teilzeichenfolge "Liste" nach der Suche nach "IList" redundant. In der Tat ist die Bedingung:
 this.Type.Contains("IList") || this.Type.Contains("List") 

kann dadurch ersetzt werden:

 this.Type.Contains("List") 

Im zweiten Fall ist die Suche nach dem Teilstring "IReadOnlyList" bedeutungslos, da zuvor die Suche nach dem kürzeren Teilstring "List" durchgeführt wurde.

Es besteht auch die Möglichkeit, dass die Teilzeichenfolgen selbst für die Suche Fehler gemacht haben und es etwas anderes geben sollte. In jedem Fall kann die korrekte Version der Bedingungskorrektur unter Berücksichtigung beider Bemerkungen nur vom Autor des Codes angeboten werden.

V3095 Das Objekt 'httpRequest.Content.Headers' wurde verwendet, bevor es gegen null verifiziert wurde. Zeilen überprüfen: 76, 79. BatchSharedKeyCredential.cs 76

 public override Task ProcessHttpRequestAsync( HttpRequestMessage httpRequest, ....) { .... signature.Append(httpRequest.Content != null && httpRequest.Content.Headers.Contains("Content-Language") ? .... : ....; long? contentLength = httpRequest.Content?.Headers?.ContentLength; .... } 

Zuerst wird die Variable httpRequest.Content.Headers ohne Prüfung verwendet, aber später im Code wird auf diese Variable mit dem Operator für bedingten Zugriff zugegriffen.

V3125 Das Objekt 'omPropertyData' wurde verwendet, nachdem es gegen null verifiziert wurde. Überprüfen Sie die Zeilen: 156, 148. CodeGenerationUtilities.cs 156

 private static string GetProtocolCollectionToObjectModelCollectionString( ...., PropertyData omPropertyData, ....) { if (IsMappedEnumPair(omPropertyData?.GenericTypeParameter, ....)) { .... } if (IsTypeComplex(omPropertyData.GenericTypeParameter)) .... } 

Die umgekehrte Situation. Ein Codeblock enthält eine sichere Zugriffsoption auf den möglicherweise Null- omPropertyData- Link. Weiter im Code mit dem gleichen Link funktionieren sie ohne Prüfung.

V3146 Mögliche Null-Dereferenzierung von 'Wert'. Der 'FirstOrDefault' kann einen Standard-Nullwert zurückgeben. BatchSharedKeyCredential.cs 127

 public override Task ProcessHttpRequestAsync(HttpRequestMessage httpRequest, ....) { .... foreach (string canonicalHeader in customHeaders) { string value = httpRequest.Headers. GetValues(canonicalHeader).FirstOrDefault(); value = value.Replace('\n', ' ').Replace('\r', ' ').TrimStart(); .... } .... } 

Als Ergebnis der FirstOrDefault- Methode wird, wenn die Suche fehlschlägt, der Standardwert für den Zeichenfolgentyp zurückgegeben, dh null . Der Wert wird der Variablen value zugewiesen, die später im Code mit der Methode Replace ohne Prüfung verwendet wird. Code sollte sicherer gemacht werden. Zum Beispiel so:

 foreach (string canonicalHeader in customHeaders) { string value = httpRequest.Headers. GetValues(canonicalHeader).FirstOrDefault(); value = value?.Replace('\n', ' ').Replace('\r', ' ').TrimStart(); .... } 

Microsoft.Azure.ServiceBus


V3121 Eine Aufzählung 'BlocksUsing' wurde mit dem Attribut 'Flags' deklariert, setzt jedoch keine Initialisierer, um Standardwerte zu überschreiben. Fx.cs 69

 static class Fx { .... public static class Tag { .... [Flags] public enum BlocksUsing { MonitorEnter, MonitorWait, ManualResetEvent, AutoResetEvent, AsyncResult, IAsyncResult, PInvoke, InputQueue, ThreadNeutralSemaphore, PrivatePrimitive, OtherInternalPrimitive, OtherFrameworkPrimitive, OtherInterop, Other, NonBlocking, } .... } .... } 

Eine Aufzählung wird mit dem Attribut Flags deklariert. In diesem Fall bleiben die Werte der Konstanten standardmäßig erhalten ( MonitorEnter = 0 , MonitorWait = 1 , ManualResetEvent = 2 usw.). Dies kann dazu führen, dass beim Versuch, eine Kombination von Flags zu verwenden, beispielsweise die zweite und dritte Konstante MonitorWait (= 1) | ManualResetEvent (= 2) , es wird kein eindeutiger Wert empfangen, sondern standardmäßig eine Konstante mit dem Wert 3 ( AutoResetEvent ). Dies kann den aufrufenden Code überraschen. Wenn die BlocksUsing- Enumeration wirklich zum Angeben von Flag-Kombinationen (Bitfeld) verwendet werden soll, sollten Sie die Konstantenwerte mit Zweierpotenzen angeben:

 [Flags] public enum BlocksUsing { MonitorEnter = 1, MonitorWait = 2, ManualResetEvent = 4, AutoResetEvent = 8, AsyncResult = 16, IAsyncResult = 32, PInvoke = 64, InputQueue = 128, ThreadNeutralSemaphore = 256, PrivatePrimitive = 512, OtherInternalPrimitive = 1024, OtherFrameworkPrimitive = 2048, OtherInterop = 4096, Other = 8192, NonBlocking = 16384, } 

V3125 Das Objekt 'session' wurde verwendet, nachdem es gegen null geprüft wurde. Überprüfen Sie die Zeilen: 69, 68. AmqpLinkCreator.cs 69

 public async Task<Tuple<AmqpObject, DateTime>> CreateAndOpenAmqpLinkAsync() { .... AmqpSession session = null; try { // Create Session .... } catch (Exception exception) { .... session?.Abort(); throw AmqpExceptionHelper.GetClientException(exception, null, session.GetInnerException(), amqpConnection.IsClosing()); } .... } 

Achten Sie auf die Arbeit mit der Sitzungsvariablen im catch-Block . Die Abort- Methode wird sicher über die Conditional Access-Anweisung aufgerufen . Dann rufen sie die GetInnerException- Methode unsicher auf. In diesem Fall wird möglicherweise eine NullReferenceException ausgelöst, anstatt eine Ausnahme des erwarteten Typs auszulösen . Der Code muss repariert werden. Die AmqpExceptionHelper.GetClientException- Methode unterstützt die Übergabe eines Nullwerts für den innerException- Parameter:

 public static Exception GetClientException( Exception exception, string referenceId = null, Exception innerException = null, bool connectionError = false) { .... } 

Daher ist es ausreichend, den Operator für den bedingten Zugriff beim Aufrufen von session.GetInnerException () zu verwenden :

 public async Task<Tuple<AmqpObject, DateTime>> CreateAndOpenAmqpLinkAsync() { .... AmqpSession session = null; try { // Create Session .... } catch (Exception exception) { .... session?.Abort(); throw AmqpExceptionHelper.GetClientException(exception, null, session?.GetInnerException(), amqpConnection.IsClosing()); } .... } 

Fazit


Wie Sie sehen, garantiert eine große Projektgröße nicht immer eine große Anzahl von Fehlern. Aber keine Notwendigkeit, sich zu entspannen - Sie können immer etwas finden. Auch in solch einem komplexen Projekt wie Azure SDK für .NET. Ja, dies erfordert zusätzliche Anstrengungen, aber je angenehmer das Ergebnis sein wird. Und damit Sie sich nicht zu sehr anstrengen müssen, empfehlen wir die Verwendung einer statischen Analyse und die Arbeit der Entwickler beim Schreiben von neuem Code. Dies ist der effektivste Ansatz. Laden Sie PVS-Studio herunter und testen Sie es in Aktion. Viel Glück im Kampf gegen Bugs!



Wenn Sie diesen Artikel mit einem englischsprachigen Publikum teilen möchten, verwenden Sie bitte den Link zur Übersetzung: Sergey Khrenov. Azure SDK für .NET: Geschichte über eine schwierige Fehlersuche .

Source: https://habr.com/ru/post/de478980/


All Articles