关于PVS-Studio静态分析器和Haiku OS代码如何相遇的故事可以追溯到2015年。对于这两个项目的团队来说,这是一个令人兴奋的实验和有用的经验。为什么要做实验?那一刻,我们没有Linux的分析仪,我们不会再用一年半了。无论如何,我们团队的爱好者的努力得到了回报:我们结识了Haiku开发人员并提高了代码质量,扩大了我们的错误基础,开发人员制造了罕见的错误并改进了分析器。现在,您可以轻松快速地检查Haiku代码中的错误。

如何用C和C ++拍摄自己的脚。Haiku OS Cookbook_第1张图片

介绍

满足我们故事的主要特征 - 带有开源代码的Haiku和用于C,C ++,C#和Java 的PVS-Studio静态分析器。当我们在4.5年前深入研究项目分析时,我们只需处理已编译的可执行分析器文件。用于解析编译器参数,运行预处理器,并行分析等的所有基础结构都来自实用程序Compiler Monitoring UI,用C#编写。该实用程序部分移植到Mono平台,以便在Linux中运行。Haiku项目是使用各种操作系统下的交叉编译器构建的,Windows除外。再一次,我想提一下与Haiku大楼相关的便利性和文档完整性。另外,我要感谢Haiku开发人员在构建项目方面提供的帮助。

现在执行分析要简单得多。以下是用于构建和分析项目的所有命令的列表:

cd /opt
git clone https://review.haiku-os.org/buildtools
git clone https://review.haiku-os.org/haiku
cd ./haiku
mkdir generated.x86_64; cd generated.x86_64
../configure --distro-compatibility official -j12 \
  --build-cross-tools x86_64 ../../buildtools
cd ../../buildtools/jam
make all
cd /opt/haiku/generated.x86_64
pvs-studio-analyzer trace -- /opt/buildtools/jam/bin.linuxx86/jam \
  -q -j1 @nightly-anyboot
pvs-studio-analyzer analyze -l /mnt/svn/PVS-Studio.lic -r /opt/haiku \
   -C x86_64-unknown-haiku-gcc -o /opt/haiku/haiku.log -j12

顺便说一句,项目分析是在Docker容器中实现的。最近我们准备了关于这个主题的新文档:在Docker中运行PVS-Studio。这可以使一些公司很容易为他们的项目应用静态分析技术。

未初始化的变量

使用V614未初始化的变量'rval'。fetch.c 1727

int
auto_fetch(int argc, char *argv[])
{
  volatile int  argpos;
  int    rval;                  // <=
  argpos = 0;

  if (sigsetjmp(toplevel, 1)) {
    if (connected)
      disconnect(0, NULL);
    if (rval > 0)               // <=
      rval = argpos + 1;
    return (rval);
  }
  ....
}
该RVAL变量尚未初始化的声明,所以它与空

值将导致不确定的结果比较。如果情况失败,则rval变量的不确定值 可以成为auto_fetch函数的返回值。

使用V614未初始化指针'res'。commands.c 2873

    struct addrinfo {
     int ai_flags;
     int ai_family;
     int ai_socktype;
     int ai_protocol;
     socklen_t ai_addrlen;
     char *ai_canonname;
     struct sockaddr *ai_addr;
     struct addrinfo *ai_next;
    };

static int
sourceroute(struct addrinfo *ai, char *arg, char **cpp,
            int *lenp, int *protop, int *optp)
{
  static char buf[1024 + ALIGNBYTES];
  char *cp, *cp2, *lsrp, *ep;
  struct sockaddr_in *_sin;
#ifdef INET6
  struct sockaddr_in6 *sin6;
  struct ip6_rthdr *rth;
#endif
  struct addrinfo hints, *res;     // <=
  int error;
  char c;

  if (cpp == NULL || lenp == NULL)
    return -1;
  if (*cpp != NULL) {
    switch (res->ai_family) {      // <=
    case AF_INET:
      if (*lenp < 7)
        return -1;
      break;
      ....
    }
  }
  ....
}

下面是使用未初始化变量的类似情况,除了res是在此处发生的未初始化指针。

V506指向局部变量“normalized”的指针存储在此变量的范围之外。这样的指针将变为无效。TextView.cpp 5596

void
BTextView::_ApplyStyleRange(...., const BFont font, ....)
{
if (font != NULL) {
BFont normalized =
font;
_NormalizeFont(&normalized);
font = &normalized;
}
....
fStyles->SetStyleRange(fromOffset, toOffset, fText->Length(), mode,
font, color);
}

程序员可能需要使用中间变量来规范化对象。但现在字体指针包含指向规范化对象的指针,该指针将在退出创建临时对象的作用域后删除。

V603对象已创建但未使用。如果你想调用构造函数,应该使用'this->

BUnicodeChar :: BUnicodeChar(....)'。UnicodeChar.cpp 27

int8
BUnicodeChar::Type(uint32 c)
{
  BUnicodeChar();
  return u_charType(c);
}

C ++程序员中一个非常常见的错误是使用构造函数的调用来初始化/取消类字段。在这种情况下,不会对类字段进行修改,但会创建此类的新未命名对象,然后立即销毁。不幸的是,项目中有很多这样的地方:

  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 37
  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 49
  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 58
  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 67
  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 77
  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 89
  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 103
  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 115
  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 126
  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 142
  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 152
  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 163
  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 186
  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 196
  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 206
  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 214
  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 222
  • V603对象已创建但未使用。如果你想调用构造函数,应该使用'this-> BUnicodeChar ::BUnicodeChar(....)'。UnicodeChar.cpp 230

V670未初始化的类成员'fPatternHandler'用于初始化'fInternal'成员。请记住,成员是按照其在类中的声明顺序初始化的。Painter.cpp 184

Painter::Painter()
  :
  fInternal(fPatternHandler),
  ....
  fPatternHandler(),
  ....
{
  ....
};

class Painter {
  ....
private:
  mutable PainterAggInterface fInternal; // line 336

  bool fSubpixelPrecise : 1;
  bool fValidClipping : 1;
  bool fDrawingText : 1;
  bool fAttached : 1;
  bool fIdentityTransform : 1;

  Transformable fTransform;
  float fPenSize;
  const BRegion* fClippingRegion;
  drawing_mode fDrawingMode;
  source_alpha fAlphaSrcMode;
  alpha_function fAlphaFncMode;
  cap_mode fLineCapMode;
  join_mode fLineJoinMode;
  float fMiterLimit;

  PatternHandler fPatternHandler;        // line 355
  mutable AGGTextRenderer fTextRenderer;
};

另一个错误初始化的例子。类字段按其在类本身中的声明顺序初始化。在此示例中,fInternal字段将是第一个使用未初始化的fPatternHandler值进行初始化的字段。

可疑#define
V523'then '语句相当于'else'语句。subr_gtaskqueue.c 191

#define  TQ_LOCK(tq)              \
  do {                \
    if ((tq)->tq_spin)          \
      mtx_lock_spin(&(tq)->tq_mutex);      \
    else              \
      mtx_lock(&(tq)->tq_mutex);      \
  } while (0)
#define  TQ_ASSERT_LOCKED(tq)  mtx_assert(&(tq)->tq_mutex, MA_OWNED)

#define  TQ_UNLOCK(tq)              \
  do {                \
    if ((tq)->tq_spin)          \
      mtx_unlock_spin(&(tq)->tq_mutex);    \
    else              \
      mtx_unlock(&(tq)->tq_mutex);      \
  } while (0)

void
grouptask_block(struct grouptask *grouptask)
{
  ....
  TQ_LOCK(queue);
  gtask->ta_flags |= TASK_NOENQUEUE;
  gtaskqueue_drain_locked(queue, gtask);
  TQ_UNLOCK(queue);
}

在查看预处理器结果之前,此代码段看起来并不可疑:

void
grouptask_block(struct grouptask *grouptask)
{
  ....
  do { if ((queue)->tq_spin) mtx_lock(&(queue)->tq_mutex);
       else mtx_lock(&(queue)->tq_mutex); } while (0);
  gtask->ta_flags |= 0x4;
  gtaskqueue_drain_locked(queue, gtask);
   do { if ((queue)->tq_spin) mtx_unlock(&(queue)->tq_mutex);
        else mtx_unlock(&(queue)->tq_mutex); } while (0);
}

分析仪是真的正确 - 如果和其他分支是相同的。但是mtx_lock_spin和mtx_unlock_spin在哪里起作用?宏TQ_LOCK,TQ_UNLOCK和grouptask_block函数在一个文件中声明几乎彼此相邻,但是在这里某处发生了替换。

搜索文件仅导致mutex.h具有以下内容:

/* on FreeBSD these are different functions */
#define mtx_lock_spin(x)   mtx_lock(x)
#define mtx_unlock_spin(x) mtx_unlock(x)

项目开发人员应检查这种替换是否正确。我在Linux中检查了这个项目,这样的替换对我来说似乎很可疑。

自由功能的错误
V575空指针传递给'free'函数。检查第一个参数。setmime.cpp 727

void
MimeType::_PurgeProperties()
{
  fShort.Truncate(0);
  fLong.Truncate(0);
  fPrefApp.Truncate(0);
  fPrefAppSig.Truncate(0);
  fSniffRule.Truncate(0);

  delete fSmallIcon;
  fSmallIcon = NULL;

  delete fBigIcon;
  fBigIcon = NULL;

  fVectorIcon = NULL;            // <=
  free(fVectorIcon);             // <=

  fExtensions.clear();
  fAttributes.clear();
}

您可以在free函数中传递空指针,但这种用法肯定是可疑的。因此,分析仪找到了混合的代码行。首先,代码作者必须通过fVectorIcon指针释放内存,然后才分配NULL。

V575空指针传递给'free'函数。检查第一个参数。driver_settings.cpp 461

static settings_handle *
load_driver_settings_from_file(int file, const char *driverName)
{
  ....
  handle = new_settings(text, driverName);
  if (handle != NULL) {
    // everything went fine!
    return handle;
  }

  free(handle);           // <=
  ....
}

这是显式将空指针传递给自由函数的另一个示例。该行可以删除,因为该函数在成功获得指针后退出。

V575空指针传递给'free'函数。检查第一个参数。PackageFileHeapWriter.cpp 166

void* _GetBuffer()
{
  ....
  void* buffer = malloc(fBufferSize);
  if (buffer == NULL && !fBuffers.AddItem(buffer)) {
    free(buffer);
    throw std::bad_alloc();
  }
  return buffer;
}

有人在这里犯了错误。必须使用||运算符而不是&&。只有在这种情况下,如果内存分配(使用malloc函数)失败,则抛出std :: bad_alloc()异常。

删除操作符出错
V611使用'new T []'运算符分配内存,但使用'delete'运算符释放。考虑检查此代码。使用'delete [] fMsg;'可能更好。Err.cpp 65

class Err {
public:
 ....
private:
 char *fMsg;
 ssize_t fPos;
};

void
Err::Unset() {
 delete fMsg;                                   // <=
 fMsg = __null;
 fPos = -1;
}

void
Err::SetMsg(const char *msg) {
 if (fMsg) {
  delete fMsg;                                  // <=
  fMsg = __null;
 }
 if (msg) {
  fMsg = new(std::nothrow) char[strlen(msg)+1]; // <=
  if (fMsg)
   strcpy(fMsg, msg);
 }
}

所述fMsg指针用于为字符数组分配内存。该删除操作被用来释放所述存储器,而不是删除[] 。

V611内存使用'new'运算符分配,但是使用'free'函数释放。考虑检查'wrapperPool'变量后面的操作逻辑。vm_page.cpp 3080

status_t
vm_page_write_modified_page_range(....)
{
  ....
  PageWriteWrapper* wrapperPool
    = new(malloc_flags(allocationFlags)) PageWriteWrapper[maxPages + 1];
  PageWriteWrapper** wrappers
    = new(malloc_flags(allocationFlags)) PageWriteWrapper*[maxPages];
  if (wrapperPool == NULL || wrappers == NULL) {
    free(wrapperPool);                              // <=
    free(wrappers);                                 // <=
    wrapperPool = stackWrappersPool;
    wrappers = stackWrappers;
    maxPages = 1;
  }
  ....
}

这里malloc_flags是一个调用malloc的函数。然后placement-new在这里构造对象。由于PageWriteWrapper类以下列方式实现:

class PageWriteWrapper {
public:
 PageWriteWrapper();
 ~PageWriteWrapper();
 void SetTo(vm_page* page);
 bool Done(status_t result);

private:
 vm_page* fPage;
 struct VMCache* fCache;
 bool fIsActive;
};

PageWriteWrapper::PageWriteWrapper()
 :
 fIsActive(false)
{
}

PageWriteWrapper::~PageWriteWrapper()
{
 if (fIsActive)
  panic("page write wrapper going out of scope but isn't completed");
}

由于使用free函数释放内存,因此不会调用此类的对象析构函数。

V611使用'new T []'运算符分配内存,但使用'delete'运算符释放。考虑检查此代码。使用'delete [] fOutBuffer;'可能更好。检查线:26,45。PCL6Rasterizer.h 26

class PCL6Rasterizer : public Rasterizer
{
public:
  ....
  ~PCL6Rasterizer()
  {
    delete fOutBuffer;
    fOutBuffer = NULL;
  }
  ....
  virtual void InitializeBuffer()
  {
    fOutBuffer = new uchar[fOutBufferSize];
  }
private:
  uchar* fOutBuffer;
  int    fOutBufferSize;
};

使用delete运算符而不是delete []是一个常见错误。编写类时最容易出错,因为析构函数的代码通常远离内存位置。这里,程序员错误地释放了析构函数中fOutBuffer指针存储的内存。

V772为void指针调用'delete'操作符将导致未定义的行为。Hashtable.cpp 207

void
Hashtable::MakeEmpty(int8 keyMode,int8 valueMode)
{
  ....
  for (entry = fTable[index]; entry; entry = next) {
    switch (keyMode) {
      case HASH_EMPTY_DELETE:
        // TODO: destructors are not called!
        delete (void*)entry->key;
        break;
      case HASH_EMPTY_FREE:
        free((void*)entry->key);
        break;
    }
    switch (valueMode) {
      case HASH_EMPTY_DELETE:
        // TODO: destructors are not called!
        delete entry->value;
        break;
      case HASH_EMPTY_FREE:
        free(entry->value);
        break;
    }
    next = entry->next;
    delete entry;
  }
  ....
}

除了删除 / 删除[]和free之间的错误选择之外,还可以在尝试通过指向void类型(void *)的指针清除内存时遇到未定义的行为。

没有返回值的函数
V591非空函数应返回一个值。Referenceable.h 228

BReference& operator=(const BReference& other)
{
  fReference = other.fReference;
}

重载赋值运算符缺少返回值。在这种情况下,操作员将返回一个随机值,这可能导致奇怪的错误。

以下是此类其他代码片段中的类似问题:

  • V591非空函数应返回一个值。Referenceable.h 233
  • V591非空函数应返回一个值。Referenceable.h 239

V591非空函数应返回一个值。main.c 1010

void errx(int, const char *, ...) ;

char *
getoptionvalue(const char *name)
{
  struct option *c;

  if (name == NULL)
    errx(1, "getoptionvalue() invoked with NULL name");
  c = getoption(name);
  if (c != NULL)
    return (c->value);
  errx(1, "getoptionvalue() invoked with unknown option '%s'", name);
  /* NOTREACHED */
}

用户的评论NOTREACHED在这里没有任何意义。您需要将函数注释为noreturn才能正确编写此类场景的代码。为此,有noreturn属性:标准和特定于编译器。首先,编译器会考虑这些属性,以便使用警告正确生成代码或通知某些类型的错误。各种静态分析工具还考虑了属性以提高分析质量。

处理异常
V596对象已创建但未使用。'throw'关键字可能会丢失:抛出

ParseException(FOO); Response.cpp 659

size_t
Response::ExtractNumber(BDataIO& stream)
{
  BString string = ExtractString(stream);

  const char* end;
  size_t number = strtoul(string.String(), (char**)&end, 10);
  if (end == NULL || end[0] != '\0')
    ParseException("Invalid number!");

  return number;
}

关键字throw在这里被意外遗忘了。因此,在退出作用域时,将简单地销毁此类的对象时,不会生成ParseException异常。之后,该功能将继续工作,就像没有发生任何事情一样,就像输入了正确的数字一样。

V1022指针抛出异常。考虑改为按价值抛出它。gensyscallinfos.cpp 316

int
main(int argc, char** argv)
{
  try {
    return Main().Run(argc, argv);
  } catch (Exception& exception) {                                         // <=
    fprintf(stderr, "%s\n", exception.what());
    return 1;
  }
}

int Run(int argc, char** argv)
{
  ....
  _ParseSyscalls(argv[1]);
  ....
}

void _ParseSyscalls(const char* filename)
{
  ifstream file(filename, ifstream::in);
  if (!file.is_open())
    throw new IOException(string("Failed to open '") + filename + "'.");   // <=
  ....
}

分析器检测到指针抛出的IOException异常。抛出指针会导致异常不会被捕获。因此异常最终被引用捕获。此外,指针的使用迫使捕获方调用delete操作符来销毁尚未完成的创建对象。

其他几个代码片段有问题:

  • V1022指针抛出异常。考虑改为按价值抛出它。gensyscallinfos.cpp 347
  • V1022指针抛出异常。考虑改为按价值抛出它。gensyscallinfos.cpp 413

正式的安全

V597编译器可以删除'memset'函数调用,该调用用于刷新'f_key'对象。memset_s()函数应该用于擦除私有数据。dst_api.c 1018

#ifndef SAFE_FREE
#define SAFE_FREE(a) \
do{if(a != NULL){memset(a,0, sizeof(*a)); free(a); a=NULL;}} while (0)
....
#endif

DST_KEY *
dst_free_key(DST_KEY *f_key)
{
  if (f_key == NULL)
    return (f_key);
  if (f_key->dk_func && f_key->dk_func->destroy)
    f_key->dk_KEY_struct =
      f_key->dk_func->destroy(f_key->dk_KEY_struct);
  else {
    EREPORT(("dst_free_key(): Unknown key alg %d\n",
       f_key->dk_alg));
  }
  if (f_key->dk_KEY_struct) {
    free(f_key->dk_KEY_struct);
    f_key->dk_KEY_struct = NULL;
  }
  if (f_key->dk_key_name)
    SAFE_FREE(f_key->dk_key_name);
  SAFE_FREE(f_key);
  return (NULL);
}

分析仪检测到可疑代码,用于安全的私人数据清除。不幸的是,扩展到memset,自由调用和NULL赋值的SAFE_FREE宏不会使代码更安全,因为在使用O2进行优化时,编译器都会删除它。

顺便说一句,除了CWE-14:编译器删除代码以清除缓冲区之外别无其他。

以下是未实际清除缓冲区的位置列表:

  • V597编译器可以删除'memset'函数调用,该调用用于刷新'encoded_block'缓冲区。memset_s()函数应该用于擦除私有数据。dst_api.c446
  • V597编译器可以删除'memset'函数调用,该调用用于刷新'key_st'对象。memset_s()函数应该用于擦除私有数据。dst_api.c685
  • V597编译器可以删除'memset'函数调用,该调用用于刷新'in_buff'缓冲区。memset_s()函数应该用于擦除私有数据。dst_api.c916
  • V597编译器可以删除'memset'函数调用,该调用用于刷新'ce'对象。memset_s()函数应该用于擦除私有数据。fs_cache.c1078

与无符号变量的比较

V547表达式'剩余<0'始终为false。无符号类型值永远不会<0。DwarfFile.cpp 1947

status_t
DwarfFile::_UnwindCallFrame(....)
{
  ....
  uint64 remaining = lengthOffset + length - dataReader.Offset();
  if (remaining < 0)
    return B_BAD_DATA;
  ....
}

分析器找到了无符号变量与负值的明确比较。也许,应该只将剩余变量与null 进行比较,或者实现溢出检查。

V547表达式'sleep((unsigned)secs)<0'始终为false。无符号类型值永远不会<0。misc.cpp 56

status_t
snooze(bigtime_t amount)
{
  if (amount <= 0)
    return B_OK;

  int64 secs = amount / 1000000LL;
  int64 usecs = amount % 1000000LL;
  if (secs > 0) {
    if (sleep((unsigned)secs) < 0)     // <=
      return errno;
  }

  if (usecs > 0) {
    if (usleep((useconds_t)usecs) < 0)
      return errno;
  }

  return B_OK;
}

为了得到错误的要点,让我们解决的签名睡眠和usleep功能:

  • extern unsigned int sleep (unsigned int __seconds);
  • extern int usleep(__useconds_t __useconds);

我们可以看到,sleep函数返回无符号值,并且它在代码中的用法不正确。

危险的指针
V774释放内存后使用'device'指针。xhci.cpp 1572

void
XHCI::FreeDevice(Device *device)
{
  uint8 slot = fPortSlots[device->HubPort()];
  TRACE("FreeDevice() port %d slot %d\n", device->HubPort(), slot);

  // Delete the device first, so it cleans up its pipes and tells us
  // what we need to destroy before we tear down our internal state.
  delete device;

  DisableSlot(slot);
  fDcba->baseAddress[slot] = 0;
  fPortSlots[device->HubPort()] = 0;            // <=
  delete_area(fDevices[slot].trb_area);
  delete_area(fDevices[slot].input_ctx_area);
  delete_area(fDevices[slot].device_ctx_area);

  memset(&fDevices[slot], 0, sizeof(xhci_device));
  fDevices[slot].state = XHCI_STATE_DISABLED;
}

甲设备对象 由释放删除操作符。FreeDevice函数非常符合逻辑。但是,出于某种原因,为了释放其他资源,已经删除的对象得到了解决。

这样的代码非常危险,可以在其他几个地方遇到:

  • V774释放内存后使用'self'指针。TranslatorRoster.cpp 884
  • V774释放内存后使用'string'指针。RemoteView.cpp 1269
  • V774释放内存后使用'bs'指针。mkntfs.c4291
  • V774释放内存后使用'bs'指针。mkntfs.c 4308
  • V774重新分配内存后使用'al'指针。inode.c 1155

    V522可能会发生空指针“数据”的解除引用。空指针被传递到'malo_hal_send_helper'函数中。检查第三个参数。检查行:350,394。if_malohal.c 350

    static int
    malo_hal_fwload_helper(struct malo_hal mh, char helper)
    {
    ....
    / tell the card we're done and... /
    error = malo_hal_send_helper(mh, 0, NULL, 0, MALO_NOWAIT); // <= NULL
    ....
    }

    static int
    malo_hal_send_helper(struct malo_hal mh, int bsize,
    const void
    data, size_t dsize, int waitfor)
    {
    mh->mh_cmdbuf[0] = htole16(MALO_HOSTCMD_CODE_DNLD);
    mh->mh_cmdbuf[1] = htole16(bsize);
    memcpy(&mh->mh_cmdbuf[4], data , dsize); // <= data
    ....
    }

过程间分析揭示了NULL传递给函数的情况,并且具有这样值的数据指针最终在memcpy函数中被解引用。

V773退出该函数时未释放'inputFileFile'指针。内存泄漏是可能的。command_recompress.cpp 119

int
command_recompress(int argc, const char* const* argv)
{
  ....
  BFile* inputFileFile = new BFile;
  error = inputFileFile->SetTo(inputPackageFileName, O_RDONLY);
  if (error != B_OK) {
    fprintf(stderr, "Error: Failed to open input file \"%s\": %s\n",
      inputPackageFileName, strerror(error));
    return 1;
  }
  inputFile = inputFileFile;
  ....
}

PVS-Studio可以检测内存泄漏。在此示例中,如果发生错误,则不会释放内存。有人可能会认为如果出现错误,你不应该为内存释放烦恼,因为程序仍然会结束。但并非总是如此。许多程序要求正确处理错误并继续工作。

V595'fReply '指针在针对nullptr进行验证之前使用。检查行:49,52。ReplyBuilder.cpp 49

RPC::CallbackReply*
ReplyBuilder::Reply()
{
  fReply->Stream().InsertUInt(fStatusPosition, _HaikuErrorToNFS4(fStatus));
  fReply->Stream().InsertUInt(fOpCountPosition, fOpCount);

  if (fReply == NULL || fReply->Stream().Error() == B_OK)
    return fReply;
  else
    return NULL;
}

在检查它们之前取消引用指针是一个非常常见的错误。在V595诊断几乎总是优先于一个项目的警告数量。此代码片段包含fReply指针的危险用法。

V595'mq '指针在针对nullptr进行验证之前使用。检查线:782,786。oce_queue.c 782

static void
oce_mq_free(struct oce_mq *mq)
{
  POCE_SOFTC sc = (POCE_SOFTC) mq->parent;
  struct oce_mbx mbx;
  struct mbx_destroy_common_mq *fwcmd;

  if (!mq)
    return;
  ....
}

一个类似的例子。该毫克指针被解除引用几行早于它的检查无效。项目中有很多类似的地方。在某些片段中,指针的使用和检查彼此相距很远,因此在本文中您将只找到几个这样的示例。欢迎开发人员查看完整分析器报告中的其他示例。

V645'strncat '函数调用可能导致'output'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。NamespaceDump.cpp 101

static void
dump_acpi_namespace(acpi_ns_device_info *device, char *root, int indenting)
{
  char output[320];
  char tabs[255] = "";
  ....
  strlcat(tabs, "|--- ", sizeof(tabs));
  ....
  while (....) {
    uint32 type = device->acpi->get_object_type(result);
    snprintf(output, sizeof(output), "%s%s", tabs, result + depth);
    switch(type) {
      case ACPI_TYPE_INTEGER:
        strncat(output, "     INTEGER", sizeof(output));
        break;
      case ACPI_TYPE_STRING:
        strncat(output, "     STRING", sizeof(output));
        break;
      ....
    }
    ....
  }
  ....
}

strlcat和strncat函数之间的区别对于不熟悉这些函数描述的人来说并不是很明显。的strlcat提供函数希望整个缓冲区作为而第三个参数的大小strncat函数功能-的自由空间的在缓冲器的大小,这需要在调用之前评估所需值。但开发人员经常忘记或不知道它。将整个缓冲区大小传递给strncat函数可能会导致缓冲区溢出,因为该函数会将此值视为要复制的可接受字符数。该strlcat提供功能没有这样的问题。但是你必须传递字符串,以terminal null结尾,以便它正常工作。

以下是包含字符串的危险地点的完整列表:

  • V645'strncat'函数调用可能导致'output'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。NamespaceDump.cpp104
  • V645'strncat'函数调用可能导致'output'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。NamespaceDump.cpp107
  • V645'strncat'函数调用可能导致'output'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。NamespaceDump.cpp110
  • V645'strncat'函数调用可能导致'output'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。NamespaceDump.cpp113
  • V645'strncat'函数调用可能导致'output'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。NamespaceDump.cpp118
  • V645'strncat'函数调用可能导致'output'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。NamespaceDump.cpp119
  • V645'strncat'函数调用可能导致'output'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。NamespaceDump.cpp120
  • V645'strncat'函数调用可能导致'output'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。NamespaceDump.cpp123
  • V645'strncat'函数调用可能导致'output'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。NamespaceDump.cpp126
  • V645'strncat'函数调用可能导致'output'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。NamespaceDump.cpp129
  • V645'strncat'函数调用可能导致'output'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。NamespaceDump.cpp132
  • V645'strncat'函数调用可能导致'output'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。NamespaceDump.cpp135
  • V645'strncat'函数调用可能导致'output'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。NamespaceDump.cpp138
  • V645'strncat'函数调用可能导致'output'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。NamespaceDump.cpp141
  • V645'strncat'函数调用可能导致'output'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。NamespaceDump.cpp144
  • V645'strncat'函数调用可能导致'features_string'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。VirtioDevice.cpp283
  • V645'strncat'函数调用可能导致'features_string'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。VirtioDevice.cpp284
  • V645'strncat'函数调用可能导致'features_string'缓冲区溢出。边界不应包含缓冲区的大小,而应包含它可以容纳的多个字符。VirtioDevice.cpp
    285

V792'SetDecoratorSettings '函数位于运算符'|'的右侧无论左操作数的值如何,都将被调用。也许,最好使用'||'。DesktopListener.cpp 324

class DesktopListener : public DoublyLinkedListLinkImpl {
public:
 ....
 virtual bool SetDecoratorSettings(Window* window,
         const BMessage& settings) = 0;
 ....
};

bool
DesktopObservable::SetDecoratorSettings(Window* window,
  const BMessage& settings)
{
  if (fWeAreInvoking)
    return false;
  InvokeGuard invokeGuard(fWeAreInvoking);

  bool changed = false;
  for (DesktopListener* listener = fDesktopListenerList.First();
    listener != NULL; listener = fDesktopListenerList.GetNext(listener))
    changed = changed | listener->SetDecoratorSettings(window, settings);

  return changed;
}

最有可能的是,'|' 和'||' 经营者感到困惑。此错误导致不必要的SetDecoratorSettings函数调用。

V627考虑检查表达式。sizeof()的参数是扩展为数字的宏。device.c 72

#define PCI_line_size 0x0c /* (1 byte) cache line size in 32 bit words */

static status_t
wb840_open(const char* name, uint32 flags, void** cookie)
{
  ....
  data->wb_cachesize = gPci->read_pci_config(data->pciInfo->bus,
    data->pciInfo->device, data->pciInfo->function, PCI_line_size,
    sizeof(PCI_line_size)) & 0xff;
  ....
}

将0x0c值传递给sizeof运算符看起来很可疑。也许,作者应该评估对象的大小,例如数据。

V562将bool类型值与值18进行比较是奇怪的:0x12 == IsProfessionalSpdif()。CEchoGals_mixer.cpp 533

typedef bool BOOL;

virtual BOOL IsProfessionalSpdif() { ... }

#define ECHOSTATUS_DSP_DEAD 0x12

ECHOSTATUS CEchoGals::ProcessMixerFunction(....)
{
  ....
  if ( ECHOSTATUS_DSP_DEAD == IsProfessionalSpdif() ) // <=
  {
    Status = ECHOSTATUS_DSP_DEAD;
  }
  else
  {
    pMixerFunction->Data.bProfSpdif = IsProfessionalSpdif();
  }
  ....
}

该IsProfessionalSpdif函数返回布尔类型值。在这样做时,函数的结果与条件中的数字0x12进行比较。

结论

去年秋天我们错过了第一个Haiku测试版的发布,因为我们正在忙着发布PVS-Studio for Java。编程错误的本质仍然是如果你不搜索它们并且不注意代码质量它们就不会消失。项目开发人员使用Coverity Scan,但最后一次运行是在两年前。这必须让Haiku用户感到不安。尽管2014年使用Coverity对配置进行了分析,但它并没有阻止我们在2015年撰写两篇关于错误审核的长篇文章(第1 部分,第2部分)

对于那些直到最后阅读这篇文章的人来说,Haiku的另一个错误评论很快就会出现。在发布此错误评论之前,将向开发人员发送完整的分析器报告,因此在您阅读此错误时可能会修复一些错误。为了在文章之间传递时间,我建议为您的项目下载并尝试PVS-Studio。

作者:Svyatoslav Razmyslov
原文链接:https://www.viva64.com/en/b/0644/