PVS-Studio stöberte in einem Kernel von FreeBSD

    Vor etwa einem Jahr konnten wir den Linux-Kernel testen. Dies war einer der am meisten diskutierten Artikel über die Überprüfung eines Open-Source-Projekts aller Zeiten. Vorschläge zur Beachtung von FreeBSD wurden dann aktiv aufgenommen, aber erst jetzt war genügend Zeit dafür.

    Über das überprüfte Projekt


    FreeBSD ist ein modernes Betriebssystem für Server, Desktops und Embedded-Computer-Plattformen. Sein Code durchlief mehr als 30 Jahre einen kontinuierlichen Prozess der Entwicklung, Verbesserung und Optimierung. Es hat sich als System zum Aufbau von Intranets und Internet-Netzwerken und -Servern etabliert. Es bietet zuverlässige Netzwerkdienste und eine effiziente Speicherverwaltung.

    Trotz der Tatsache, dass FreeBSD regelmäßig von Coverity überprüft wird, bereue ich nicht, dass ich mit diesem Projekt zusammengearbeitet habe fand viele verdächtige Orte. In dem Artikel werden ca. 40 Teile vorgestellt. Für die Entwickler (die den Überprüfungsbericht vor Beginn des Schreibens dieses Artikels erhalten haben) habe ich eine Liste mit ca. 1000 Warnungen zu schwerwiegenden Analysen erstellt.

    Meiner Meinung nach sind aus den ausgegebenen Warnungen des Analysators viele Stellen echte Fehler, aber ich kann nichts über Kritikalität sagen, weil Ich bin kein Betriebssystementwickler. Ich denke, dies ist ein guter Anlass zur Diskussion und Kommunikation mit den Autoren des Projekts.

    Der Quellcode für die Überprüfung wurde von GitHub aus dem 'Master'-Zweig übernommen. Das Repository enthält ~ 23000 Dateien und zwei Dutzend Konfigurationen für die Assembly für verschiedene Plattformen, aber ich habe nur den Kernel überprüft, den ich folgendermaßen zusammengestellt habe:

    make buildkernel KERNCONF=MYKERNEL

    So prüfen Sie


    Zur Überprüfung des Kernels wurde der statische Code-Analysator PVS-Studio Version 6.01 verwendet .

    Der Einfachheit halber habe ich PC-BSD installiert und ein kleines Dienstprogramm in C ++ geschrieben, das die Arbeitsumgebung für Compiler-Starts während der Kernel-Erstellung sparte. Die erhaltenen Informationen wurden verwendet, um vorverarbeitete Dateien zu erhalten und diese mit PVS-Studio zu analysieren. Mit dieser Methode konnte ich das Projekt schnell überprüfen, ohne ein unbekanntes Montagesystem für die Integration des Analysators zu untersuchen. Wenn Sie die vorverarbeiteten Dateien überprüfen, können Sie den Code genauer analysieren und komplexere und interessantere Fehler finden, z. B. in Makros. Der Artikel wird einige solcher Beispiele liefern.

    Der Linux-Kernel wurde auf ähnliche Weise getestet. Für Windows-Benutzer ist dieser Überprüfungsmodus im Standalone- Dienstprogramm verfügbar, das im PVS-Studio-Distributionskit enthalten ist. Für Entwickler, die den Analyzer in ihr Projekt integrieren möchten, ist dies in der Regel kein Problem. Sie verwenden eine Art von Integration, die in der Dokumentation beschrieben ist. Der Vorteil der Überwachungsprogramme besteht darin, dass Sie den Analysator schnell testen können, wenn das Projekt über ein nicht standardmäßiges Montagesystem verfügt.

    Ungewöhnliches Glück


    Ich habe den ersten möglichen Fehler gefunden, noch bevor der Analysator gestartet wurde, noch bevor ich den Kernel zusammengestellt habe, weil die Zusammenstellung durch einen Verbindungsfehler unterbrochen wurde. Als ich zu der im Fehler angegebenen Datei ging, sah ich Folgendes:



    Achten Sie auf das ausgewählte Fragment. Um den Einzug zu formatieren, wird ein Tabulatorzeichen verwendet und zwei Operatoren werden unter der Bedingung verschoben. Die letzte Anweisung ist jedoch keine Bedingung und wird immer ausgeführt. Möglicherweise haben Sie vergessen, hier geschweifte Klammern einzufügen.

    Zu einem Artikel gab es einen Kommentar, in dem wir die Warnungen des Analysegeräts einfach umschrieben. Dies ist jedoch nicht der Fall. Bevor Sie das Projekt überprüfen, müssen Sie sicherstellen, dass es korrekt kompiliert wird, und nachdem Sie den Bericht und die Warnungen des Analysators erhalten haben, müssen Sie das Projekt untersuchen / zerlegen und dem Leser erklären. Genau die gleiche Arbeit erledigt das Benutzer-Support-Team des Analysegeräts, indem es auf E-Mails antwortet. Fälle, in denen Benutzer ihrer Meinung nach Beispiele für falsch positive Ergebnisse senden, sind keine Seltenheit, aber in Wirklichkeit stellt sich dies als echter Fehler heraus.

    Capy-Poste und Flocken


    Der PVS-Studio-Analysator ist ein leistungsstarkes statisches Analysetool, das eine Vielzahl von Fehlern im Code findet. Die ersten Diagnosen waren jedoch einfach und dienten dazu, nach den häufigsten Fehlern im Zusammenhang mit Tippfehlern und Copy-Paste-Programmierung zu suchen. Beim Anzeigen des Analyseberichts sortiere ich ihn nach dem Fehlercode und beginne meine Geschichte normalerweise mit dieser Art von Diagnoseregel.



    V501 Links und rechts vom Operator '>' gibt es identische Unterausdrücke '(uintptr_t) b-> handler'. ip_fw_sockopt.c 2893
    static int
    compare_sh(const void *_a, const void *_b)
    {
      const struct ipfw_sopt_handler *a, *b;
      a = (const struct ipfw_sopt_handler *)_a;
      b = (const struct ipfw_sopt_handler *)_b;
      ....
      if ((uintptr_t)a->handler < (uintptr_t)b->handler)
        return (-1);
      else if ((uintptr_t)b->handler > (uintptr_t)b->handler) // <=
        return (1);
      return (0);
    }

    Ein kleines Beispiel dafür, wie schädlich es ist, Variablen kurz und uninformativ zu benennen. Aufgrund eines Tippfehlers im Buchstaben 'b' wird ein Teil der Bedingung niemals erfüllt. Daher gibt die Funktion nicht immer den beabsichtigten Nullstatus zurück.

    V501 Links und rechts vom Operator '! =' Gibt es identische Unterausdrücke: m-> m_pkthdr.len! = M-> m_pkthdr.len key.c 7208
    int
    key_parse(struct mbuf *m, struct socket *so)
    {
      ....
      if ((m->m_flags & M_PKTHDR) == 0 ||
          m->m_pkthdr.len != m->m_pkthdr.len) { // <=
        ....
        goto senderror;
      }
      ....
    }

    Eines der Felder der Struktur wird mit sich selbst verglichen, daher ist das Ergebnis dieser logischen Operation immer Falsch.

    V501 Links und rechts vom '|' stehen identische Unterausdrücke. Betreiber: PIM_NOBUSRESET | PIM_NOBUSRESET sbp_targ.c 1327
    typedef enum {
      PIM_EXTLUNS      = 0x100,
      PIM_SCANHILO     = 0x80,
      PIM_NOREMOVE     = 0x40,
      PIM_NOINITIATOR  = 0x20,
      PIM_NOBUSRESET   = 0x10, // <=
      PIM_NO_6_BYTE    = 0x08,
      PIM_SEQSCAN      = 0x04,
      PIM_UNMAPPED     = 0x02,
      PIM_NOSCAN       = 0x01
    } pi_miscflag;
    static void
    sbp_targ_action1(struct cam_sim *sim, union ccb *ccb)
    {
      ....
      struct ccb_pathinq *cpi = &ccb->cpi;
        cpi->version_num = 1; /* XXX??? */
        cpi->hba_inquiry = PI_TAG_ABLE;
        cpi->target_sprt = PIT_PROCESSOR
             | PIT_DISCONNECT
             | PIT_TERM_IO;
        cpi->transport = XPORT_SPI;
        cpi->hba_misc = PIM_NOBUSRESET | PIM_NOBUSRESET; // <=
      ....
    }

    In diesem Beispiel ist dieselbe Variable "PIM_NOBUSRESET" an der Bitoperation beteiligt, die das Ergebnis nicht beeinflusst. Höchstwahrscheinlich wollten sie eine Konstante mit einem anderen Wert verwenden, haben jedoch vergessen, die Variable umzubenennen.

    V523 Die Anweisung 'then' entspricht der Anweisung 'else'. saint.c 2023
    GLOBAL void siSMPRespRcvd(....)
    {
      ....
      if (agNULL == frameHandle)
      {
        /* indirect mode */
        /* call back with success */
        (*(ossaSMPCompletedCB_t)(pRequest->completionCB))(agRoot,
           pRequest->pIORequestContext, OSSA_IO_SUCCESS, payloadSize,
           frameHandle);
      }
      else
      {
        /* direct mode */
        /* call back with success */
        (*(ossaSMPCompletedCB_t)(pRequest->completionCB))(agRoot,
           pRequest->pIORequestContext, OSSA_IO_SUCCESS, payloadSize,
           frameHandle);
      }
      ....
    }

    Zwei Zweige der Bedingung sind mit unterschiedlichen Kommentaren signiert: / * indirekter Modus * / und / * direkter Modus * /, aber sie werden auf die gleiche Weise implementiert, was sehr verdächtig ist.

    V523 Die Anweisung 'then' entspricht der Anweisung 'else'. smsat.c 2848
    osGLOBAL void
    smsatInquiryPage89(....)
    {
      ....
      if (oneDeviceData->satDeviceType == SATA_ATA_DEVICE)
      {
        pInquiry[40] = 0x01; /* LBA Low          */
        pInquiry[41] = 0x00; /* LBA Mid          */
        pInquiry[42] = 0x00; /* LBA High         */
        pInquiry[43] = 0x00; /* Device           */
        pInquiry[44] = 0x00; /* LBA Low Exp      */
        pInquiry[45] = 0x00; /* LBA Mid Exp      */
        pInquiry[46] = 0x00; /* LBA High Exp     */
        pInquiry[47] = 0x00; /* Reserved         */
        pInquiry[48] = 0x01; /* Sector Count     */
        pInquiry[49] = 0x00; /* Sector Count Exp */
      }
      else
      {
        pInquiry[40] = 0x01; /* LBA Low          */
        pInquiry[41] = 0x00; /* LBA Mid          */
        pInquiry[42] = 0x00; /* LBA High         */
        pInquiry[43] = 0x00; /* Device           */
        pInquiry[44] = 0x00; /* LBA Low Exp      */
        pInquiry[45] = 0x00; /* LBA Mid Exp      */
        pInquiry[46] = 0x00; /* LBA High Exp     */
        pInquiry[47] = 0x00; /* Reserved         */
        pInquiry[48] = 0x01; /* Sector Count     */
        pInquiry[49] = 0x00; /* Sector Count Exp */
      }
      ....
    }

    Dieses Beispiel ist noch verdächtiger als das vorherige. Ein so großes Stück Code wurde kopiert, aber dann wurden keine Änderungen vorgenommen.

    V547 Ausdruck ist immer wahr. Wahrscheinlich sollte hier der Operator '&&' verwendet werden. qla_hw.c 799
    static int
    qla_tx_tso(qla_host_t *ha, struct mbuf *mp, ....)
    {
      ....
      if ((*tcp_opt != 0x01) || (*(tcp_opt + 1) != 0x01) ||
        (*(tcp_opt + 2) != 0x08) || (*(tcp_opt + 2) != 10)) { // <=
        return -1;
      }
      ....
    }

    Hier hat der Analysator festgestellt, dass die Bedingung "(* (tcp_opt + 2)! = 0x08) || (* (tcp_opt + 2)! = 10)" immer wahr ist, und dies gilt, wenn Sie eine Wahrheitstabelle erstellen. Aber höchstwahrscheinlich wird der Operator '&&' hier nicht benötigt, sondern nur ein Tippfehler im Adressoffset gemacht. Vielleicht hätte der Funktionscode so aussehen sollen:
    static int
    qla_tx_tso(qla_host_t *ha, struct mbuf *mp, ....)
    {
      ....
      if ((*tcp_opt != 0x01) || (*(tcp_opt + 1) != 0x01) ||
        (*(tcp_opt + 2) != 0x08) || (*(tcp_opt + 3) != 10)) {
        return -1;
      }
      ....
    }

    V571 Wiederkehrende Prüfung. Dieser Zustand wurde bereits in Zeile 1946 überprüft. Sahw.c 1949
    GLOBAL
    bit32 siHDAMode_V(....)
    {
      ....
      if( saRoot->memoryAllocated.agMemory[i].totalLength > biggest)
      {
        if(biggest < saRoot->memoryAllocated.agMemory[i].totalLength)
        {
          save = i;
          biggest = saRoot->memoryAllocated.agMemory[i].totalLength;
        }
      }
      ....
    }

    Ein sehr seltsamer Code, wenn wir ihn bedingt vereinfachen, werden wir Folgendes sehen:
    if( A > B )
    {
      if (B < A)
      {
        ....
      }
    }

    Dieselbe Bedingung wird zweimal hintereinander geprüft. Höchstwahrscheinlich wollten sie hier anderen Code schreiben.

    Ein weiterer ähnlicher Ort:
    • V571 Wiederkehrende Prüfung. Diese Bedingung wurde bereits in Zeile 1940 überprüft. If_rl.c 1941

    Gefährliche Makros


    V523 Die Anweisung 'then' entspricht der Anweisung 'else'. agtiapi.c 829
    if (osti_strncmp(buffer, "0x", 2) == 0)
    { 
      maxTargets = osti_strtoul (buffer, &pLastUsedChar, 0);
      AGTIAPI_PRINTK( ".... maxTargets = osti_strtoul  0 \n" );
    }
    else
    {
      maxTargets = osti_strtoul (buffer, &pLastUsedChar, 10);
      AGTIAPI_PRINTK( ".... maxTargets = osti_strtoul 10\n"   );
    }

    Ich habe diese Warnung des Analysators übersprungen und festgestellt, dass es sich um ein falsches Positiv handelt. Nach der Überprüfung des Projekts müssen jedoch vom Analysegerät Fehlalarme untersucht und verbessert werden. Was ich getan habe, nach dem ich ein solches Makro getroffen habe:
    #define osti_strtoul(nptr, endptr, base)    \
              strtoul((char *)nptr, (char **)endptr, 0)

    Der Parameter 'base' wird überhaupt nicht verwendet und der Wert '0' wird immer als letzter Parameter an die Funktion strtoul übergeben. Obwohl die Werte '0' und '10' an das Makro übergeben werden. In der vorverarbeiteten Datei wurden alle Makros erweitert, und der Code wurde derselbe. Dieses Makro wird auf diese Weise mehrere Dutzend Mal verwendet. Ich habe die gesamte Liste solcher Orte an die Entwickler geschickt.

    V733 Es ist möglich, dass die Makroerweiterung zu einer falschen Auswertungsreihenfolge geführt hat. Überprüfen Sie den Ausdruck: chan - 1 * 20. isp.c 2301
    static void
    isp_fibre_init_2400(ispsoftc_t *isp)
    {
      ....
      if (ISP_CAP_VP0(isp))
        off += ICB2400_VPINFO_PORT_OFF(chan);
      else
        off += ICB2400_VPINFO_PORT_OFF(chan - 1); // <=
      ....
    }

    Auf den ersten Blick ist an diesem Code nichts Verdächtiges. Manchmal wird der Wert 'chan' verwendet, manchmal ist es einer weniger: 'chan - 1', aber schauen wir uns die Definition des Makros an:
    #define ICB2400_VPOPT_WRITE_SIZE 20
    #define  ICB2400_VPINFO_PORT_OFF(chan) \
      (ICB2400_VPINFO_OFF +                \
       sizeof (isp_icb_2400_vpinfo_t) +    \
      (chan * ICB2400_VPOPT_WRITE_SIZE))          // <=

    Wenn ein binärer Ausdruck an ein Makro übertragen wird, ändert sich die Berechnungslogik dort dramatisch. Der angebliche Ausdruck "(chan - 1) * 20" wird zu "chan - 1 * 20", d.h. in "chan - 20", und dann verwendet das Programm eine falsch berechnete Größe.

    Über Betriebsprioritäten


    In diesem Abschnitt erkläre ich Ihnen, wie wichtig es ist, die Prioritäten von Operationen zu kennen, zusätzliche Klammern zu verwenden, wenn Sie sich nicht sicher sind, und sich manchmal selbst zu testen, indem Sie eine Wahrheitstabelle eines logischen Ausdrucks erstellen.



    V502 Vielleicht arbeitet der Operator '?:' Anders als erwartet. Der Operator '?:' Hat eine niedrigere Priorität als das '|' Betreiber. ata-serverworks.c 166
    ata_serverworks_chipinit(device_t dev)
    {
      ....
      pci_write_config(dev, 0x5a,
               (pci_read_config(dev, 0x5a, 1) & ~0x40) |
               (ctlr->chip->cfg1 == SWKS_100) ? 0x03 : 0x02, 1);
      }
      ....
    }

    Operatorpriorität '?:' Unterhalb von bitweisem ODER '|'. Infolgedessen nimmt bei Bitoperationen neben numerischen Konstanten auch das Ergebnis des Ausdrucks "(ctlr-> chip-> cfg1 == SWKS_100)" teil, was die Logik der Berechnungen unerwartet ändert. Vielleicht wird an dieser Stelle oft ein der Wahrheit ähnliches Ergebnis erzielt, weshalb ein solcher Fehler noch nicht bemerkt wurde.

    V502 Vielleicht arbeitet der Operator '?:' Anders als erwartet. Der Operator '?:' Hat eine niedrigere Priorität als das '|' Betreiber. in6.c 1318
    void
    in6_purgeaddr(struct ifaddr *ifa)
    {
      ....
      error = rtinit(&(ia->ia_ifa), RTM_DELETE, ia->ia_flags |
            (ia->ia_dstaddr.sin6_family == AF_INET6) ? RTF_HOST : 0);
      ....
    }

    In einer anderen Datei gab es auch eine Stelle mit einem ähnlichen Fehler beim ternären Operator.

    V547 Ausdruck 'cdb [0]! = 0x28 || cdb [0]! = 0x2A 'ist immer wahr. Wahrscheinlich sollte hier der Operator '&&' verwendet werden. mfi_tbolt.c 1110
    int
    mfi_tbolt_send_frame(struct mfi_softc *sc, struct mfi_command *cm)
    {
      ....
      if (cdb[0] != 0x28 || cdb[0] != 0x2A) {  // <='
        if ((req_desc = mfi_tbolt_build_mpt_cmd(sc, cm)) == NULL) {
          device_printf(sc->mfi_dev, "Mapping from MFI "
              "to MPT Failed \n");
          return 1;
        }
      }
      else
        device_printf(sc->mfi_dev, "DJA NA XXX SYSPDIO\n");
      ....
    }

    Hier ist der erste Bedingungsausdruck immer wahr, wodurch der 'else'-Zweig niemals die Kontrolle erhält. Um den Fehler in diesem und den folgenden Beispielen zu beweisen, werde ich eine Wahrheitstabelle für kontroverse logische Ausdrücke geben. Ein Beispiel für diesen Fall:



    V590 Betrachten Sie die 'Fehler == 0 || Fehler! = - 1 'Ausdruck. Der Ausdruck ist zu groß oder enthält einen Druckfehler. nd6.c 2119
    int
    nd6_output_ifp(....)
    {
      ....
      /* Use the SEND socket */
      error = send_sendso_input_hook(m, ifp, SND_OUT,
          ip6len);
      /* -1 == no app on SEND socket */
      if (error == 0 || error != -1)           // <=
          return (error);
      ....
    }

    Das Problem mit diesem Codeteil ist, dass der Bedingungsausdruck unabhängig vom Ergebnis "error == 0" ist. Höchstwahrscheinlich stimmt hier etwas nicht:



    Drei weitere Fälle:
    • V590 Betrachten Sie die 'Fehler == 0 || Fehler! = 35 'Ausdruck. Der Ausdruck ist zu groß oder enthält einen Druckfehler. if_ipw.c 1855
    • V590 Betrachten Sie die 'Fehler == 0 || Fehler! = 27 'Ausdruck. Der Ausdruck ist zu groß oder enthält einen Druckfehler. if_vmx.c 2747
    • V547 Ausdruck ist immer wahr. Wahrscheinlich sollte hier der Operator '&&' verwendet werden. igmp.c 1939

    V590 Betrachten Sie diesen Ausdruck. Der Ausdruck ist zu groß oder enthält einen Druckfehler. sig_verify.c 94
    enum uni_ieact {
      UNI_IEACT_CLEAR = 0x00, /* clear call */
      ....
    }
    void
    uni_mandate_epref(struct uni *uni, struct uni_ie_epref *epref)
    {
      ....
      maxact = -1;
      FOREACH_ERR(e, uni) {
        if (e->ie == UNI_IE_EPREF)
          continue;
        if (e->act == UNI_IEACT_CLEAR)
          maxact = UNI_IEACT_CLEAR;
        else if (e->act == UNI_IEACT_MSG_REPORT) {
          if (maxact == -1 && maxact != UNI_IEACT_CLEAR)     // <=
            maxact = UNI_IEACT_MSG_REPORT;
        } else if (e->act == UNI_IEACT_MSG_IGNORE) {
          if (maxact == -1)
            maxact = UNI_IEACT_MSG_IGNORE;
        }
      }
      ....
    }

    Hier hängt das Ergebnis des gesamten Bedingungsausdrucks nicht von der Berechnung des Wertes "maxact! = UNI_IEACT_CLEAR" ab. So sieht es in der Tabelle aus:



    In diesem Kapitel habe ich bis zu drei Möglichkeiten angegeben, um Fehler in scheinbar einfachen Formeln zu machen. Denken Sie ...

    V593 Überlegen Sie, ob Sie den Ausdruck der Art 'A = B! = C' überprüfen möchten . Der Ausdruck wird wie folgt berechnet: 'A = (B! = C)'. aacraid.c 2854
    #define EINVAL 22 /* Invalid argument */
    #define EFAULT 14 /* Bad address */
    #define EPERM 1 /* Operation not permitted */
    static int
    aac_ioctl_send_raw_srb(struct aac_softc *sc, caddr_t arg)
    {
      ....
      int error, transfer_data = 0;
      ....
      if ((error = copyin((void *)&user_srb->data_len, &fibsize, 
        sizeof (u_int32_t)) != 0)) 
        goto out;
      if (fibsize > (sc->aac_max_fib_size-sizeof(....))) {
        error = EINVAL;
        goto out;
      }
      if ((error = copyin((void *)user_srb, srbcmd, fibsize) != 0)) 
        goto out;
      ....
    out:
      ....
      return(error);
    }

    Diese Funktion verdirbt den Fehlercode, wenn die Zuweisung in der Anweisung 'if' ausgeführt wird. Das heißt Im Ausdruck "error = copyin (...)! = 0" wird zuerst "copyin (...)! = 0" berechnet und dann das Ergebnis (Wert 0 oder 1) in die Variable "error" geschrieben.

    In der Dokumentation der Funktion 'copyin' heißt es, dass im Fehlerfall der EFAULT-Code (Wert 14) zurückgegeben wird und nach einer solchen Überprüfung das Ergebnis der logischen Operation gleich '1' im Fehlercode gespeichert wird. Dies ist EPERM - ein völlig anderer Fehlerstatus.

    Leider gibt es viele solcher Orte:
    • V593 Betrachten Sie den Ausdruck der Art 'A = B! = C'. Der Ausdruck wird wie folgt berechnet: 'A = (B! = C)'. aacraid.c 2861
    • V593 Betrachten Sie den Ausdruck der Art 'A = B! = C'. Der Ausdruck wird wie folgt berechnet: 'A = (B! = C)'. if_age.c 591
    • V593 Betrachten Sie den Ausdruck der Art 'A = B! = C'. Der Ausdruck wird wie folgt berechnet: 'A = (B! = C)'. if_alc.c 1535
    • V593 Betrachten Sie den Ausdruck der Art 'A = B! = C'. Der Ausdruck wird wie folgt berechnet: 'A = (B! = C)'. if_ale.c 606
    • V593 Betrachten Sie den Ausdruck der Art 'A = B! = C'. Der Ausdruck wird wie folgt berechnet: 'A = (B! = C)'. if_jme.c 807
    • V593 Betrachten Sie den Ausdruck der Art 'A = B! = C'. Der Ausdruck wird wie folgt berechnet: 'A = (B! = C)'. if_msk.c 1626
    • V593 Betrachten Sie den Ausdruck der Art 'A = B! = C'. Der Ausdruck wird wie folgt berechnet: 'A = (B! = C)'. if_stge.c 511
    • V593 Betrachten Sie den Ausdruck der Art 'A = B! = C'. Der Ausdruck wird wie folgt berechnet: 'A = (B! = C)'. hunt_filter.c 973
    • V593 Betrachten Sie den Ausdruck der Art 'A = B! = C'. Der Ausdruck wird wie folgt berechnet: 'A = (B! = C)'. if_smsc.c 1365
    • V593 Betrachten Sie den Ausdruck der Art 'A = B! = C'. Der Ausdruck wird wie folgt berechnet: 'A = (B! = C)'. if_vte.c 431
    • V593 Betrachten Sie den Ausdruck der Art 'A = B! = C'. Der Ausdruck wird wie folgt berechnet: 'A = (B! = C)'. zfs_vfsops.c 498

    Linien




    V541 Es ist gefährlich, den String 'buffer' in sich selbst zu drucken. ata-highpoint.c 102
    static int
    ata_highpoint_probe(device_t dev)
    {
      ....
      char buffer[64];
      ....
      strcpy(buffer, "HighPoint ");
      strcat(buffer, idx->text);
      if (idx->cfg1 == HPT_374) {
      if (pci_get_function(dev) == 0)
          strcat(buffer, " (channel 0+1)");
      if (pci_get_function(dev) == 1)
          strcat(buffer, " (channel 2+3)");
      }
      sprintf(buffer, "%s %s controller",
        buffer, ata_mode2str(idx->max_dma));
      ....
    }

    Hier bilden sie eine bestimmte Zeile im Puffer. Dann möchten sie eine neue Zeile erhalten, den vorherigen Wert der Zeile beibehalten und zwei weitere Wörter hinzufügen. Alles scheint einfach zu sein.

    Um zu erklären, warum hier ein unerwartetes Ergebnis erzielt wird, zitiere ich ein einfaches und verständliches Beispiel aus der Dokumentation für diese Diagnose:
    char s[100] = "test";
    sprintf(s, "N = %d, S = %s", 123, s);

    Als Ergebnis dieses Codes möchte ich die folgende Zeile erhalten:
    N = 123, S = test

    In der Praxis wird jedoch eine Zeile im Puffer gebildet:
    N = 123, S = N = 123, S =

    In anderen Situationen kann ein ähnlicher Code nicht nur dazu führen, dass falscher Text angezeigt wird, sondern auch, dass das Programm abstürzt. Der Code kann festgelegt werden, wenn ein neuer Puffer zum Speichern des Ergebnisses verwendet wird. Die richtige Option:
    char s1[100] = "test";
    char s2[100];
    sprintf(s2, "N = %d, S = %s", 123, s1);

    V512 Ein Aufruf der Funktion 'strcpy' führt zum Überlauf des Puffers 'p-> vendor'. aacraid_cam.c 571
    #define  SID_VENDOR_SIZE   8
      char   vendor[SID_VENDOR_SIZE];
    #define  SID_PRODUCT_SIZE  16
      char   product[SID_PRODUCT_SIZE];
    #define  SID_REVISION_SIZE 4
      char   revision[SID_REVISION_SIZE];
    static void
    aac_container_special_command(struct cam_sim *sim, union ccb *ccb,
      u_int8_t *cmdp)
    {
      ....
      /* OEM Vendor defines */
      strcpy(p->vendor,"Adaptec ");          // <=
      strcpy(p->product,"Array           "); // <=
      strcpy(p->revision,"V1.0");            // <=
      ....
    }

    Alle drei Zeilen hier sind falsch ausgefüllt. Arrays haben keinen Platz für ein Null-Terminal-Zeichen , was bei der weiteren Arbeit mit solchen Zeichenfolgen ernsthafte Probleme verursachen kann. Bei "p-> Anbieter" und "p-> Produkt" können Sie ein Leerzeichen entfernen. Dann passt die Klemme Null, die die Funktion strcpy () am Ende der Zeile anfügt. Für "p-> revision" gibt es jedoch keinen Platz für einen Zeilenabschluss. Daher müssen Sie den Wert von SID_REVISION_SIZE mindestens um eins erhöhen.

    Natürlich fällt es mir schwer, diesen Code zu beurteilen. Möglicherweise wird Terminal Null nicht benötigt und alles ist für eine bestimmte Puffergröße ausgelegt. Dann ist die Funktion strcpy () falsch ausgewählt. In diesem Fall sollten Sie so etwas schreiben:
    memcpy(p->vendor,   "Adaptec ",         SID_VENDOR_SIZE);
    memcpy(p->product,  "Array           ", SID_PRODUCT_SIZE);
    memcpy(p->revision, "V1.0",             SID_REVISION_SIZE);

    V583 Der Operator '?:' Gibt unabhängig von seinem Bedingungsausdruck immer denselben Wert zurück: td-> td_name. subr_turnstile.c 1029
    static void
    print_thread(struct thread *td, const char *prefix)
    {
      db_printf("%s%p (tid %d, pid %d, ....", prefix, td, td->td_tid,
          td->td_proc->p_pid, td->td_name[0] != '\0' ? td->td_name :
          td->td_name);
    }

    Verdächtiger Ort. Trotz Überprüfung von "td-> td_name [0]! = '\ 0'" wird diese Zeile weiterhin gedruckt.

    Alle diese Orte:
    • V583 Der Operator '?:' Gibt unabhängig von seinem Bedingungsausdruck immer denselben Wert zurück: td-> td_name. subr_turnstile.c 1112
    • V583 Der Operator '?:' Gibt unabhängig von seinem Bedingungsausdruck immer denselben Wert zurück: td-> td_name. subr_turnstile.c 1196

    Speicheroperationen


    In diesem Abschnitt werde ich über den Missbrauch der folgenden Funktionen sprechen:
    void bzero(void *b, size_t len);

    Die bzero () -Funktion füllt die Nullen von 'len' Bytes am Zeiger 'b' aus.
    int copyout(const void *kaddr, void *uaddr, size_t len);

    Die Funktion copyout () kopiert das 'len'-Byte von' kaddr 'nach' uaddr '.

    V579 Die Funktion bzero empfängt den Zeiger und seine Größe als Argumente. Es ist möglicherweise ein Fehler. Überprüfen Sie das zweite Argument. osapi.c 316
    /* Autosense storage */  
    struct scsi_sense_data sense_data;
    void
    ostiInitiatorIOCompleted(....)
    {
      ....
      bzero(&csio->sense_data, sizeof(&csio->sense_data));
      ....
    }

    Um die Struktur zu annullieren, müssen Sie der bzero () -Funktion einen Zeiger auf die Struktur und die Größe des rücksetzbaren Speichers in Bytes übergeben. Hier wird jedoch die Größe des Zeigers und nicht die Größe der Struktur an die Funktion übergeben.

    Die richtige Option sollte folgendermaßen aussehen:
    bzero(&csio->sense_data, sizeof(csio->sense_data));

    V579 Die Funktion bzero empfängt den Zeiger und seine Größe als Argumente. Es ist möglicherweise ein Fehler. Überprüfen Sie das zweite Argument. acpi_package.c 83
    int
    acpi_PkgStr(...., void *dst, ....)
    {
      ....
      bzero(dst, sizeof(dst));
      ....
    }

    In diesem Beispiel wurde eine ähnliche Situation: Die Größe des Zeigers und nicht das Objekt wurde erneut an die Funktion 'bzero' übergeben.

    Die richtige Option sollte folgendermaßen aussehen:
    bzero(dst, sizeof(*dst));

    V579 Die Funktion copyout empfängt den Zeiger und seine Größe als Argumente. Es ist möglicherweise ein Fehler. Untersuche das dritte Argument. if_nxge.c 1498
    int
    xge_ioctl_stats(xge_lldev_t *lldev, struct ifreq *ifreqp)
    {
      ....
      *data = (*data == XGE_SET_BUFFER_MODE_1) ? 'Y':'N';
      if(copyout(data, ifreqp->ifr_data, sizeof(data)) == 0)    // <=
          retValue = 0;
      break;
      ....
    }

    In diesem Beispiel wird der Speicher von "Daten" nach "ifreqp-> ifr_data" kopiert, während die Größe des kopierten Speichers gleich der Größe von (Daten) ist, d.h. 4 oder 8 Bytes je nach Kapazität der Architektur.

    Zeiger




    V557 Array Overrun ist möglich. Der '2'-Index zeigt über die Array-Grenze hinaus. if_spppsubr.c 4348
    #define AUTHKEYLEN  16
    struct sauth {
      u_short  proto;      /* authentication protocol to use */
      u_short  flags;
    #define AUTHFLAG_NOCALLOUT  1  
              /* callouts */
    #define AUTHFLAG_NORECHALLENGE  2  /* do not re-challenge CHAP */
      u_char  name[AUTHNAMELEN];  /* system identification name */
      u_char  secret[AUTHKEYLEN];  /* secret password */
      u_char  challenge[AUTHKEYLEN];  /* random challenge */
    };
    static void
    sppp_chap_scr(struct sppp *sp)
    {
      u_long *ch, seed;
      u_char clen;
      /* Compute random challenge. */
      ch = (u_long *)sp->myauth.challenge;
      read_random(&seed, sizeof seed);
      ch[0] = seed ^ random();
      ch[1] = seed ^ random();
      ch[2] = seed ^ random(); // <=
      ch[3] = seed ^ random(); // <=
      clen = AUTHKEYLEN;
      ....
    }

    Die Größe des Typs 'u_char' beträgt 1 Byte in 32-Bit- und 64-Bit-Anwendungen, und die Größe des Typs 'u_long' beträgt 4 Byte in einer 32-Bit-Anwendung und 8 Byte in einer 64-Bit-Anwendung. Wenn Sie dann in einer 32-Bit-Anwendung die Operation "u_long * ch = (u_long *) sp-> myauth.challenge" ausführen, besteht das Array "ch" aus 4 Elementen mit jeweils 4 Bytes. In einer 64-Bit-Anwendung besteht das Array 'ch' aus 2 Elementen mit jeweils 8 Bytes. Wenn wir also einen 64-Bit-Kernel zusammenstellen, gehen wir beim Aufrufen von ch [2] und ch [3] über die Grenzen des Arrays hinaus.

    V503 Dies ist ein unsinniger Vergleich: Zeiger> = 0. geom_vinum_plex.c 173
    gv_plex_offset(...., int *sdno, int growing)
    {
      ....
      *sdno = stripeno % sdcount;
      ....
      KASSERT(sdno >= 0, ("gv_plex_offset: sdno < 0"));
      ....
    }

    Mit der 503. Diagnose wurde ein sehr interessanter Ort gefunden. Es ist praktisch nicht sinnvoll zu überprüfen, ob der Zeigerwert größer oder gleich 0 ist. Höchstwahrscheinlich haben sie vergessen, den sdno-Zeiger hier zu dereferenzieren, um den dort gespeicherten Wert zu vergleichen.

    Zwei weitere Zeigervergleiche mit Null:
    • V503 Dies ist ein unsinniger Vergleich: Zeiger> = 0. geom_vinum_raid5.c 602
    • V503 Dies ist ein unsinniger Vergleich: Zeiger> = 0. geom_vinum_raid5.c 610

    V522 Möglicherweise findet eine Dereferenzierung des Nullzeigers 'sc' statt. mrsas.c 4027
    void
    mrsas_aen_handler(struct mrsas_softc *sc)
    {
      ....
      if (!sc) {
        device_printf(sc->mrsas_dev, "invalid instance!\n");
        return;
      }
      if (sc->evt_detail_mem) {
      ....
    }

    Wenn der sc-Zeiger null ist, wird die Funktion beendet. Aber hier ist nicht klar, warum versucht wird, einen solchen Zeiger "sc-> mrsas_dev" zu dereferenzieren.

    Liste fremder Orte:
    • V522 Möglicherweise findet eine Dereferenzierung des Nullzeigers 'sc' statt. mrsas.c 1279
    • V522 Möglicherweise findet eine Dereferenzierung des Nullzeigers 'sc' statt. tws_cam.c 1066
    • V522 Möglicherweise findet eine Dereferenzierung des Nullzeigers 'sc' statt. blkfront.c 677
    • V522 Möglicherweise findet eine Dereferenzierung des Nullzeigers 'dev_priv' statt. radeon_cs.c 153
    • V522 Eventuell findet eine Dereferenzierung des Nullzeigers 'ha' statt. ql_isr.c 728

    V713 Der Zeiger m wurde im logischen Ausdruck verwendet, bevor er im selben logischen Ausdruck gegen nullptr verifiziert wurde. ip_fastfwd.c 245
    struct mbuf *
    ip_tryforward(struct mbuf *m)
    {
      ....
      if (pfil_run_hooks(
          &V_inet_pfil_hook, &m, m->m_pkthdr.rcvif, PFIL_IN, NULL) ||
          m == NULL)
        goto drop;
      ....
    }

    Das Häkchen "m == NULL" befindet sich an der falschen Stelle. Zuerst müssen Sie den Zeiger überprüfen und dann nur die Funktion pfil_run_hooks () aufrufen.

    Zyklen




    V621 Überlegen Sie, ob Sie den Operator 'für' überprüfen möchten . Es ist möglich, dass die Schleife falsch oder gar nicht ausgeführt wird. if_ae.c 1663
    #define  AE_IDLE_TIMEOUT    100
    static void
    ae_stop_rxmac(ae_softc_t *sc)
    {
      int i;
      ....
      /*
       * Wait for IDLE state.
       */
      for (i = 0; i < AE_IDLE_TIMEOUT; i--) {  // <=
        val = AE_READ_4(sc, AE_IDLE_REG);
        if ((val & (AE_IDLE_RXMAC | AE_IDLE_DMAWRITE)) == 0)
          break;
        DELAY(100);
      }
      ....
    }

    Es gab so eine interessante und falsche Schleife im Quellcode von FreeBSD. Es ist nicht bekannt, warum, aber hier wird die Dekrementierung des Schleifenzählers vorgenommen, anstatt zu inkrementieren. Es stellt sich heraus, dass die Schleife viel mehr als den Wert AE_IDLE_TIMEOUT ausführen kann, bis die Anweisung 'break' ausgeführt wird.

    Wird der Zyklus nicht rechtzeitig gestoppt, läuft die Vorzeichenvariable 'i' über. Das Überlaufen einer vorzeichenbehafteten Variablen ist nichts anderes als das undefinierte Verhalten eines Programms. Und dies ist keine abstrakte theoretische Gefahr, sondern eine sehr reale. Mein Kollege schrieb kürzlich einen Artikel zu diesem Thema: " Undefiniertes Verhalten ist näher als Sie denken ."

    Ein weiterer interessanter Punkt. Es wurde genau derselbe Fehler festgestellt.mich im Code des Haiku-Betriebssystems (siehe Abschnitt "Warnungen # 17, # 18"). Ich weiß nicht, von wem ich die Datei "if_ae.c" ausgeliehen habe, aber der Fehler wird durch Kopieren eindeutig verbreitet :).

    V535 Die Variable 'i' wird für diese Schleife und für die äußere Schleife verwendet. Zeilen überprüfen: 182, 183. mfi_tbolt.c 183
    mfi_tbolt_adp_reset(struct mfi_softc *sc)
    {
      ....
      for (i=0; i < 10; i++) {
        for (i = 0; i < 10000; i++);
      }
      ....
    }

    Dieser kleine Code wird höchstwahrscheinlich verwendet, um eine Verzögerung zu erzeugen. Hier werden nur insgesamt 10.000 Iterationen ausgeführt und nicht 10 * 10.000. Warum sollten Sie dann zwei Zyklen verwenden?

    Ich habe dieses Beispiel speziell angeführt, weil Es ist am offensichtlichsten, wenn die Verwendung einer Variablen in externen und verschachtelten Schleifen zu unerwarteten Ergebnissen führt.

    V535 Die Variable 'i' wird für diese Schleife und für die äußere Schleife verwendet. Überprüfen Sie die Zeilen: 197, 208. linux_vdso.c 208
    void
    __elfN(linux_vdso_reloc)(struct sysentvec *sv, long vdso_adjust)
    {
      ....
      for(i = 0; i < ehdr->e_shnum; i++) {                      // <=
        if (!(shdr[i].sh_flags & SHF_ALLOC))
          continue;
        shdr[i].sh_addr += vdso_adjust;
        if (shdr[i].sh_type != SHT_SYMTAB &&
            shdr[i].sh_type != SHT_DYNSYM)
          continue;
        sym = (Elf_Sym *)((caddr_t)ehdr + shdr[i].sh_offset);
        symcnt = shdr[i].sh_size / sizeof(*sym);
        for(i = 0; i < symcnt; i++, sym++) {                    // <=
          if (sym->st_shndx == SHN_UNDEF ||
              sym->st_shndx == SHN_ABS)
            continue;
          sym->st_value += vdso_adjust;
        }
      }
      ....
    }

    Dies ist ein zu komplexes Beispiel, um zu verstehen, ob der Code korrekt ausgeführt wird. Nach dem vorherigen Beispiel können wir jedoch den Schluss ziehen, dass hier möglicherweise auch die falsche Anzahl von Iterationen ausgeführt wird.

    V547 Ausdruck 'j> = 0' ist immer wahr. Der Wert für den vorzeichenlosen Typ ist immer> = 0. safe.c 1596
    static void
    safe_mcopy(struct mbuf *srcm, struct mbuf *dstm, u_int offset)
    {
      u_int j, dlen, slen;                   // <=
      caddr_t dptr, sptr;
      /*
       * Advance src and dst to offset.
       */
      j = offset;
      while (j >= 0) {                       // <=
        if (srcm->m_len > j)
          break;
        j -= srcm->m_len;                    // <=
        srcm = srcm->m_next;
        if (srcm == NULL)
          return;
      }
      sptr = mtod(srcm, caddr_t) + j;
      slen = srcm->m_len - j;
      j = offset;
      while (j >= 0) {                       // <=
        if (dstm->m_len > j)
          break;
        j -= dstm->m_len;                    // <=
        dstm = dstm->m_next;
        if (dstm == NULL)
          return;
      }
      dptr = mtod(dstm, caddr_t) + j;
      dlen = dstm->m_len - j;
      ....
    }

    In dieser Funktion gibt es zwei gefährliche Zyklen. Weil Da die Variable 'j' (Zykluszähler) einen vorzeichenlosen Typ hat, ist die Prüfung "j> = 0" immer wahr und die Zyklen sind "ewig". Ein weiteres Problem besteht darin, dass ständig Werte von diesem Zähler subtrahiert werden. Wenn also versucht wird, den Nullwert zu überwinden, nimmt die Variable 'j' den Maximalwert des Typs an.

    V711 Es ist gefährlich, eine lokale Variable in einer Schleife mit demselben Namen wie eine Variable zu erstellen, die diese Schleife steuert. powernow.c 733
    static int
    pn_decode_pst(device_t dev)
    {
      ....
      struct pst_header *pst;                                   // <=
      ....
      p = ((uint8_t *) psb) + sizeof(struct psb_header);
      pst = (struct pst_header*) p;
      maxpst = 200;
      do {
        struct pst_header *pst = (struct pst_header*) p;        // <=
        ....
        p += sizeof(struct pst_header) + (2 * pst->numpstates);
      } while (cpuid_is_k7(pst->cpuid) && maxpst--);            // <=
      ....
    }

    Im Hauptteil der Schleife wurde eine Variablendeklaration gefunden, die mit der Variablen übereinstimmt, mit der die Schleife gesteuert wird. Ich habe den Verdacht, dass sich durch die Erstellung eines lokalen Zeigers mit dem gleichen Namen 'pst' der Wert des externen Zeigers mit dem Namen 'pst' nicht ändert. Vielleicht wird im Zustand der do .... while () -Schleife immer derselbe Wert "pst-> cupid" überprüft. Entwickler müssen diesen Ort noch einmal überprüfen und sicherstellen, dass die Variablen unterschiedliche Namen haben.

    Verschiedenes


    V569 Abschneiden des konstanten Wertes -96. Der Wertebereich des vorzeichenlosen Zeichentyps: [0, 255]. if_rsu.c 1516
    struct ieee80211_rx_stats {
      ....
      uint8_t nf;      /* global NF */
      uint8_t rssi;    /* global RSSI */
      ....
    };
    static void
    rsu_event_survey(struct rsu_softc *sc, uint8_t *buf, int len)
    {
      ....
      rxs.rssi = le32toh(bss->rssi) / 2;
      rxs.nf = -96;
      ....
    }

    Es ist sehr verdächtig, dass der vorzeichenlosen Variablen "rxs.nf" der negative Wert "-96" zugewiesen wird. Infolgedessen hat die Variable den Wert '160'.

    Der Funktionskörper von V729 enthält die Bezeichnung 'done', die von keinen 'goto'-Anweisungen verwendet wird. zfs_acl.c 2023
    int
    zfs_setacl(znode_t *zp, vsecattr_t *vsecp, ....)
    {
      ....
    top:
      mutex_enter(&zp->z_acl_lock);
      mutex_enter(&zp->z_lock);
      ....
      if (error == ERESTART) {
        dmu_tx_wait(tx);
        dmu_tx_abort(tx);
        goto top;
      }
      ....
    done:                            // <=
      mutex_exit(&zp->z_lock);
      mutex_exit(&zp->z_acl_lock);
      return (error);
    }

    Der Code enthält Funktionen, die Beschriftungen enthalten. Der goto-Operator ruft diese Beschriftungen jedoch nicht auf. In diesem Code-Snippet wird beispielsweise die Bezeichnung "top" verwendet, "done" jedoch nirgendwo. Vielleicht haben sie vergessen, den Übergang zum Etikett hinzuzufügen oder ihn im Laufe der Zeit zu löschen, aber das Etikett wurde versehentlich verlassen.

    V646 Überlegen Sie, ob Sie die Logik der Anwendung überprüfen möchten . Möglicherweise fehlt das Schlüsselwort "else". mac_process.c 352
    static void
    mac_proc_vm_revoke_recurse(struct thread *td, struct ucred *cred,
        struct vm_map *map)
    {
      ....
      if (!mac_mmap_revocation_via_cow) {
        vme->max_protection &= ~VM_PROT_WRITE;
        vme->protection &= ~VM_PROT_WRITE;
      } if ((revokeperms & VM_PROT_READ) == 0)   // <=
        vme->eflags |= MAP_ENTRY_COW |
            MAP_ENTRY_NEEDS_COPY;
      ....
    }

    Abschließend möchte ich auf die verdächtige Formatierung eingehen, die mir bereits zu Beginn der Projektüberprüfung aufgefallen ist. Hier ist der Code so gestaltet, dass das Fehlen des Schlüsselworts 'else' verdächtig aussieht.

    V705 Möglicherweise wurde der ' else' -Block vergessen oder auskommentiert , wodurch die Betriebslogik des Programms geändert wurde. scsi_da.c 3231
    static void
    dadone(struct cam_periph *periph, union ccb *done_ccb)
    {
      ....
      /*
       * If we tried READ CAPACITY(16) and failed,
       * fallback to READ CAPACITY(10).
       */
      if ((state == DA_CCB_PROBE_RC16) &&
        ....
      } else                                                    // <=
      /*
       * Attach to anything that claims to be a
       * direct access or optical disk device,
       * as long as it doesn't return a "Logical
       * unit not supported" (0x25) error.
       */
      if ((have_sense) && (asc != 0x25)                         // <=
        ....
      } else { 
        ....
      }
      ....
    }

    Es gibt noch keinen Fehler in diesem Code, aber eines Tages wird er definitiv erscheinen. Wenn Sie einen so großen Kommentar vor "else" lassen, können Sie versehentlich vergessen, dass sich dieses Schlüsselwort irgendwo befand, und in Zukunft unwissentlich eine fehlerhafte Bearbeitung des Codes vornehmen.

    Fazit




    Das FreeBSD-Projekt wurde mit einer speziellen Version von PVS-Studio getestet, die hervorragende Ergebnisse zeigte! Das gesamte Material konnte nicht alleine in diesen Artikel eingepasst werden. Trotzdem erhielt das FreeBSD-Entwicklungsteam die gesamte Liste der Warnungen, die es wert sind, beachtet zu werden.

    Ich empfehle jedem, PVS-Studio bei seinen Projekten auszuprobieren . Der Analyzer läuft in einer Windows-Umgebung. Um den Analyzer bei der Entwicklung von Projekten für Linux / FreeBSD zu verwenden, haben wir keine öffentliche Version. Wir können jedoch mögliche Optionen für den Abschluss eines Vertrags zur Anpassung von PVS-Studio an Ihre Projekte und Aufgaben erörtern.


    Wenn Sie diesen Artikel mit einem englischsprachigen Publikum teilen möchten, verwenden Sie bitte den Link zur Übersetzung: Svyatoslav Razmyslov. PVS-Studio hat sich mit dem FreeBSD-Kernel befasst .

    Haben Sie den Artikel gelesen und eine Frage?
    Oft werden unseren Artikeln die gleichen Fragen gestellt. Wir haben die Antworten hier gesammelt: Antworten auf Fragen von Lesern von Artikeln über PVS-Studio, Version 2015 . Bitte beachten Sie die Liste.

    Jetzt auch beliebt: