Developers Club geek daily blog

2 years ago
PVS-Studio: 25 suspicious fragments of code from CoreCLR

The Microsoft corporation has uploaded publicly the source code of the CoreCLR engine which is the key .NET Core element. This news, of course, could not but draw our attention. After all the more audience at the project, the will be more disturbing to look the found suspicious places. Despite authorship of Microsoft as in any large project, is here on what to look and of what to think.

Introduction


CoreCLR is the environment of execution of .NET Core, executing such functions as garbage collection or compilations in final machine code. .Net Core — is modular implementation of .Net which can be used as base for huge number of scenarios.

The source code since recent time is available on GitHub and was checked by means of PVS-Studio 5.23. As well as I, persons interested can receive full log of check by means of Microsoft Visual Studio Community Edition which output too was recent news from Microsoft.


Typographical errors


Traditionally I begin with the places similar to typographical errors. In conditional expressions variables, constants, macroes or fields of structures/classes repeat. Availability of error — the subject of discussions, nevertheless such places have been found and look suspiciously.

V501 There are identical sub-expressions 'tree-> gtOper == GT_CLS_VAR' to the left and to the right of the' ||' operator. ClrJit lsra.cpp 3140
// register variable 
GTNODE(GT_REG_VAR      , "regVar"  ,0,GTK_LEAF|GTK_LOCAL)
// static data member
GTNODE(GT_CLS_VAR      , "clsVar"  ,0,GTK_LEAF)
// static data member address
GTNODE(GT_CLS_VAR_ADDR , "&clsVar" ,0,GTK_LEAF)           
....

void  LinearScan::buildRefPositionsForNode(GenTree *tree, ....)
{
  ....
  if ((tree->gtOper == GT_CLS_VAR ||
       tree->gtOper == GT_CLS_VAR) && i == 1)
  {
      registerType = TYP_PTR;
      currCandidates = allRegs(TYP_PTR);
  }
  ....
}

Though the structure of 'GenTree' has similar by the name of the tree-> gtType field, but it has other type with "tree-> gtOper". I think, here matter in the copied constant. That is in expression one more constant, in addition to GT_CLS_VAR has to be used.

V501 There are identical sub-expressions 'DECODE_PSP_SYM' to the left and to the right of the' |' operator. daccess 264
enum GcInfoDecoderFlags
{
    DECODE_SECURITY_OBJECT       = 0x01,
    DECODE_CODE_LENGTH           = 0x02,
    DECODE_VARARG                = 0x04,
    DECODE_INTERRUPTIBILITY      = 0x08,
    DECODE_GC_LIFETIMES          = 0x10,
    DECODE_NO_VALIDATION         = 0x20,
    DECODE_PSP_SYM               = 0x40,
    DECODE_GENERICS_INST_CONTEXT = 0x80,
    DECODE_GS_COOKIE             = 0x100,   
    DECODE_FOR_RANGES_CALLBACK   = 0x200,
    DECODE_PROLOG_LENGTH         = 0x400,
    DECODE_EDIT_AND_CONTINUE     = 0x800,
};

size_t GCDump::DumpGCTable(PTR_CBYTE table, ....)
{
  GcInfoDecoder hdrdecoder(table,
   (GcInfoDecoderFlags)(  DECODE_SECURITY_OBJECT
                        | DECODE_GS_COOKIE
                        | DECODE_CODE_LENGTH
                        | DECODE_PSP_SYM                //<==1
                        | DECODE_VARARG
                        | DECODE_PSP_SYM                //<==1
                        | DECODE_GENERICS_INST_CONTEXT  //<==2
                        | DECODE_GC_LIFETIMES
                        | DECODE_GENERICS_INST_CONTEXT  //<==2
                        | DECODE_PROLOG_LENGTH),
   0);
  ....
}

Here the even two repeating constants though in transfer of "GcInfoDecoderFlags" there are also other constants which in condition are not used.

Still similar places:
  • V501 There are identical sub-expressions 'varLoc1.vlStk2.vls2BaseReg' of to the left and to the right of the' ==' operator. cee_wks util.cpp 657
  • V501 There are identical sub-expressions 'varLoc1.vlStk2.vls2Offset' of to the left and to the right of the '==' operator. cee_wks util.cpp 658
  • V501 There are identical sub-expressions 'varLoc1.vlFPstk.vlfReg' of to the left and to the right of the '==' operator. cee_wks util.cpp 661

V700 Consider inspecting the 'T foo = foo =...' expression. It is odd that variable is initialized through itself. cee_wks zapsig.cpp 172
BOOL ZapSig::GetSignatureForTypeHandle(....)
{
  ....
  CorElementType elemType = elemType =
    TryEncodeUsingShortcut(pMT);
  ....
}

Seemingly simply excess assignment, but such mistakes are made often when copying code and forgotten something to be renamed. Anyway, in such look code senseless.

V523 The 'then' statement is equivalent to the 'else' statement. cee_wks threadsuspend.cpp 2468
enum __MIDL___MIDL_itf_mscoree_0000_0004_0001
{
  OPR_ThreadAbort = 0,
  OPR_ThreadRudeAbortInNonCriticalRegion = .... ,
  OPR_ThreadRudeAbortInCriticalRegion = ....) ,
  OPR_AppDomainUnload = .... ,
  OPR_AppDomainRudeUnload = ( OPR_AppDomainUnload + 1 ) ,
  OPR_ProcessExit = ( OPR_AppDomainRudeUnload + 1 ) ,
  OPR_FinalizerRun = ( OPR_ProcessExit + 1 ) ,
  MaxClrOperation = ( OPR_FinalizerRun + 1 ) 
}  EClrOperation;

void Thread::SetRudeAbortEndTimeFromEEPolicy()
{
  LIMITED_METHOD_CONTRACT;
  DWORD timeout;
  if (HasLockInCurrentDomain())
  {
    timeout = GetEEPolicy()->
      GetTimeout(OPR_ThreadRudeAbortInCriticalRegion);  //<==
  }
  else
  {
    timeout = GetEEPolicy()->
      GetTimeout(OPR_ThreadRudeAbortInCriticalRegion);  //<==
  }
  ....
}

This diagnostics finds identical blocks in constructions of if/else. And here too there is suspicion on typographical error in constant. In the first case, on sense "OPR_ThreadRudeAbortInNonCriticalRegion" just approaches.

Similar places:
  • V523 The 'then' statement is equivalent to the 'else' statement. ClrJit instr.cpp 3427
  • V523 The 'then' statement is equivalent to the 'else' statement. ClrJit flowgraph.cpp 18815
  • V523 The 'then' statement is equivalent to the 'else' statement. daccess dacdbiimpl.cpp 6374

List of initialization of the designer


V670 The uninitialized class member 'gcInfo' is used to initialize the 'regSet' member. Remember that members are initialized in the order of their declarations inside a class. ClrJit codegencommon.cpp 92
CodeGenInterface *getCodeGenerator(Compiler *comp);

class CodeGenInterface
{
    friend class emitter;

public:
    ....
    RegSet  regSet; //<=== line 91
    ....
public:
    GCInfo  gcInfo; //<=== line 322
....
};

// CodeGen constructor
CodeGenInterface::CodeGenInterface(Compiler* theCompiler) :
    compiler(theCompiler),
    gcInfo(theCompiler),
    regSet(theCompiler, gcInfo)
{
}

According to the standard, order initialization of members of class in the designer happens as their declaration in class. For bug fix it is necessary to transfer declaration of the member of the class 'gcInfo' above declaration of 'regSet'.

False, but useful warning


V705 It is possible that 'else' block was forgotten or commented out, thus altering the program's operation logics. daccess daccess.cpp 2979
HRESULT Initialize()
{
  if (hdr.dwSig == sig)
  {
      m_rw = eRO;
      m_MiniMetaDataBuffSizeMax = hdr.dwTotalSize;
      hr = S_OK;
  }
  else
  // when the DAC initializes this for the case where the target is 
  // (a) a live process, or (b) a full dump, buff will point to a
  // zero initialized memory region (allocated w/ VirtualAlloc)
  if (hdr.dwSig == 0 && hdr.dwTotalSize == 0 && hdr.dwCntStreams == 0)
  {
      hr = S_OK;
  }
  // otherwise we may have some memory corruption. treat this as
  // a liveprocess/full dump
  else
  {
      hr = S_FALSE;
  }
  ....
}

The analyzer has found suspicious place in code. Here it is visible that the code Is commented and everything is normal. But errors of this kind are very widespread when the code after 'else' Is commented out and the operator following it becomes part of condition. In this example there is no error, but it can quite be allowed when editing this place in the future.

64-bit error


V673 The '0xefefefef << 28 expression evaluates to 1080581331517177856. 60 bits are required to store the value but the expression evaluates to the unsigned type which can only hold 32 bits. cee_dac _dac object.inl 95 br />
inline void Object::EnumMemoryRegions(void)
{
  ....
  SIZE_T size = sizeof(ObjHeader) + sizeof(Object);
  ....
  size |= 0xefefefef << 28;
  ....
}

About the term "64-bit error" it is possible to read here. In this example, after shift the operation "size | = 0xf0000000" in the 32-bit program and "by size | = 0x00000000f0000000" in 64-bit will be executed. Most likely, in the 64-bit program were going to calculate: "size | = 0x0efefefef0000000". But where the senior part of number is lost?

The number "0xefefefef" has the unsigned type as is not located in the int type. Shift of 32-bit number is executed and as a result we will receive 0xf0000000 unsigned type. Further this unsigned number will extend to SIZE_T and we will receive 0x00000000f0000000.

For correct work it is necessary to execute explicit reduction of type at first. Example of correct code:
inline void Object::EnumMemoryRegions(void)
{
  ....
  SIZE_T size = sizeof(ObjHeader) + sizeof(Object);
  ....
  size |= SIZE_T(0xefefefef) << 28;
  ....
}

Still such place:
  • V673 The '0xefefefef <<28' of expression evaluates to 1080581331517177856. 60 bits are required to store the value, but the expression evaluates to the 'unsigned' type which can only hold '32' bits. cee_dac dynamicmethod.cpp 807

Code "in resignation"


Are written with time of condition so that literally contradict each other.

V637 Two opposite conditions were encountered. The second condition is always false. Check lines: 31825, 31827. cee_wks gc.cpp 31825
void gc_heap::verify_heap (BOOL begin_gc_p)
{
  ....
  if (brick_table [curr_brick] < 0)
  {
    if (brick_table [curr_brick] == 0)
    {
      dprintf(3, ("curr_brick %Ix for object %Ix set to 0",
              curr_brick, (size_t)curr_object));
      FATAL_GC_ERROR();
    }
    ....
  }
  ....
}

Code which never receives management, but it looks not such significant, as in the following example:

V517 The use of 'if (A) {...} else if (A) {...}' pattern was detected. There is a probability of logical error presence. Check lines: 2353, 2391. utilcode util.cpp 2353
void  PutIA64Imm22(UINT64 * pBundle, UINT32 slot, INT32 imm22)
{
  if (slot == 0)
  {
    const UINT64 mask0 = UI64(0xFFFFFC000603FFFF);
    /* Clear all bits used as part of the imm22 */
    pBundle[0] &= mask0;

    UINT64 temp0;
    
    temp0  = (UINT64) (imm22 & 0x200000) << 20;     //  1 s
    temp0 |= (UINT64) (imm22 & 0x1F0000) << 11;     //  5 imm5c
    temp0 |= (UINT64) (imm22 & 0x00FF80) << 25;     //  9 imm9d
    temp0 |= (UINT64) (imm22 & 0x00007F) << 18;     //  7 imm7b
    
    /* Or in the new bits used in the imm22 */
    pBundle[0] |= temp0;
  }
  else if (slot == 1)
  {
    ....
  }
  else if (slot == 0)        //<==
  {
    const UINT64 mask1 = UI64(0xF000180FFFFFFFFF);
    /* Clear all bits used as part of the imm22 */
    pBundle[1] &= mask1;

    UINT64 temp1;
    
    temp1  = (UINT64) (imm22 & 0x200000) << 37;     //  1 s
    temp1 |= (UINT64) (imm22 & 0x1F0000) << 32;     //  5 imm5c
    temp1 |= (UINT64) (imm22 & 0x00FF80) << 43;     //  9 imm9d
    temp1 |= (UINT64) (imm22 & 0x00007F) << 36;     //  7 imm7b
    
    /* Or in the new bits used in the imm22 */
    pBundle[1] |= temp1;
  }
  FlushInstructionCache(GetCurrentProcess(),pBundle,16);
}

Perhaps, very important code never receives management because of error in the cascade of conditional statements.

Still suspicious places:
  • V637 Two opposite conditions were encountered. The second condition is always false. Check lines: 2898, 2900. daccess nidump.cpp 2898
  • V637 Two opposite conditions were encountered. The second condition is always false. Check lines: 337, 339. utilcode prettyprintsig.cpp 337
  • V637 Two opposite conditions were encountered. The second condition is always false. Check lines: 774, 776. utilcode prettyprintsig.cpp 774

Indefinite behavior


V610 Undefined behavior. Check the shift operator'<<'. The left operand '-1' is negative. bcltype metamodel.h 532
inline static mdToken decodeToken(....)
{
    //<TODO>@FUTURE: make compile-time calculation</TODO>
    ULONG32 ix = (ULONG32)(val & ~(-1 << m_cb[cTokens]));

    if (ix >= cTokens)
        return rTokens[0];
    return TokenFromRid(val >> m_cb[cTokens], rTokens[ix]);
}

The analyzer has found shift operation of negative number which leads to indefinite behavior.

V610 Undefined behavior. Check the shift operator'<<'. The left operand '(~0)' is negative. cee_dac decodemd.cpp 456
#define bits_generation 2
#define generation_mask (~(~0 << bits_generation))

#define MASK(len) (~((~0)<<len))
#define MASK64(len) ((~((~((unsigned __int64)0))<<len)))

void Encoder::Add(unsigned value, unsigned length)
{
  ....
  value = (value & MASK(length));
  ....
}

Thanks to the message of the V610 analyzer I have found some incorrect macroes.' ~ 0' it is led to sign negative number like int then shift is executed. As in one of macroes, it is necessary to execute explicit conversion to unsigned:
#define bits_generation 2
#define generation_mask (~(~((unsigned int)0) << bits_generation))

#define MASK(len) (~((~((unsigned int)0))<<len))
#define MASK64(len) ((~((~((unsigned __int64)0))<<len)))

Incorrect sizeof(xx)


V579 The DacReadAll function receives the pointer and its size as arguments. It is possibly a mistake. Inspect the third argument. daccess dacimpl.h 1688
template<class T>
inline bool MisalignedRead(CORDB_ADDRESS addr, T *t)
{
  return SUCCEEDED(DacReadAll(TO_TADDR(addr), t, sizeof(t), false));
}

Here such small function which always takes the pointer size. Most likely, here wanted to write "sizeof (*t)", well or "sizeof (T)".

One more visual primerchik:

V579 The Read function receives the pointer and its size as arguments. It is possibly a mistake. Inspect the third argument. util.cpp 4943
HRESULT GetMTOfObject(TADDR obj, TADDR *mt)
{
  if (!mt)
    return E_POINTER;

  HRESULT hr = rvCache->Read(obj, mt, sizeof(mt), NULL);
  if (SUCCEEDED(hr))
    *mt &= ~3;

  return hr;
}


Family of the memFAIL functions


With use of memXXX-functions it is possible to make the most different mistakes. For search of such places in the analyzer there are some diagnostic rules.

V512 A call of the 'memset' function will lead to underflow of the buffer 'pAddExpression'. sos strike.cpp 11973
DECLARE_API(Watch)
{
  ....
  if(addExpression.data != NULL || aExpression.data != NULL)
  {
    WCHAR pAddExpression[MAX_EXPRESSION];
    memset(pAddExpression, 0, MAX_EXPRESSION);
    swprintf_s(pAddExpression, MAX_EXPRESSION, L"%S", ....);
    Status = g_watchCmd.Add(pAddExpression);
  }
  ....
}

Widespread error when forget to do the correction on the type size:
WCHAR pAddExpression[MAX_EXPRESSION];
memset(pAddExpression, 0, sizeof(WCHAR)*MAX_EXPRESSION);

Some more such places:
  • V512 A call of the 'memset' function will lead to underflow of the buffer 'pSaveName'. sos strike.cpp 11997
  • V512 A call of the 'memset' function will lead to underflow of the buffer 'pOldName'. sos strike.cpp 12013
  • V512 A call of the 'memset' function will lead to underflow of the buffer 'pNewName'. sos strike.cpp 12016
  • V512 A call of the 'memset' function will lead to underflow of the buffer 'pExpression'. sos strike.cpp 12024
  • V512 A call of the 'memset' function will lead to underflow of the buffer 'pFilterName'. sos strike.cpp 12039

V598 The 'memcpy' function is used to copy the fields of 'GenTree' class. Virtual table pointer will be damaged by this. ClrJit compiler.hpp 1344
struct GenTree
{
  ....
  #if DEBUGGABLE_GENTREE
    virtual void DummyVirt() {}
  #endif // DEBUGGABLE_GENTREE
  ....
};

void GenTree::CopyFrom(const GenTree* src, Compiler* comp)
{
  ....
  memcpy(this, src, src->GetNodeSize());
  ....
}

If the variable of preprocessor of 'DEBUGGABLE_GENTREE' is declared, virtual function is defined. Then the class contains the pointer on the virtual method table and it cannot already be copied here so simply.

V598 The 'memcpy' function is used to copy the fields of 'GCStatistics' class. Virtual table pointer will be damaged by this. cee_wks gc.cpp 287
struct GCStatistics
    : public StatisticsBase
{
  ....
  virtual void Initialize();
  virtual void DisplayAndUpdate();
  ....
};

GCStatistics g_LastGCStatistics;

void GCStatistics::DisplayAndUpdate()
{
  ....
  memcpy(&g_LastGCStatistics, this, sizeof(g_LastGCStatistics));
  ....
}

In this place incorrect copying is executed not only in debug mode.

V698 Expression 'memcmp (....) ==-1' is incorrect. This function can return not only the value '-1', but any negative value. Consider using 'memcmp (....) < 0' instead. sos util.cpp 142
bool operator( )(const GUID& _Key1, const GUID& _Key2) const
  { return memcmp(&_Key1, &_Key2, sizeof(GUID)) == -1; }

To compare result of the memcmp function to value 1 or-1 it is not correct. Operability of such constructions depends on libraries, the compiler, its settings, operating system, its digit capacity and so on; in that case it is necessary to check one of three statuses:'< 0', '0' или '> 0'.

Similar place:
  • V698 Expression 'wcscmp (....) ==-1' is incorrect. This function can return not only the value '-1', but any negative value. Consider using 'wcscmp (....) <0' instead. sos strike.cpp 3855

About pointers


V522 Dereferencing of the null pointer 'hp' might take place. cee_wks gc.cpp 4488
heap_segment* gc_heap::get_segment_for_loh (size_t size
#ifdef MULTIPLE_HEAPS
                                           , gc_heap* hp
#endif //MULTIPLE_HEAPS
                                           )
{
#ifndef MULTIPLE_HEAPS
    gc_heap* hp = 0;
#endif //MULTIPLE_HEAPS
    heap_segment* res = hp->get_segment (size, TRUE);
  ....
}

If 'MULTIPLE_HEAPS' is not defined, trouble. The pointer will be equal to zero.

V595 The 'tree' pointer was utilized before it was verified against nullptr. Check lines: 6970, 6976. ClrJit gentree.cpp 6970
void Compiler::gtDispNode(GenTreePtr tree, ....)
{
  ....
  if (tree->gtOper >= GT_COUNT)
  {
    printf(" **** ILLEGAL NODE ****");
    return;
  }

  if  (tree && printFlags)
  {
    /* First print the flags associated with the node */
    switch (tree->gtOper)
    {
      ....
    }
    ....
  }
  ....
}

Places when the pointer validity is checked, but after dereferencing are widespread in the source code.

All list: CoreCLR_V595.txt.

Excess checks


Even if the excess code does not do harm, its availability can simply distract attention of development from more important places.

V503 This is a nonsensical comparison: pointer> = 0. cee_wks gc.cpp 21707
void gc_heap::make_free_list_in_brick (BYTE* tree,
                                       make_free_args* args)
{
  assert ((tree >= 0));
  ....
}

Here such verification of the pointer. Still examples:
  • V503 This is a nonsensical comparison: pointer> = 0. cee_wks gc.cpp 23204
  • V503 This is a nonsensical comparison: pointer> = 0. cee_wks gc.cpp 27683

V547 Expression 'maxCpuId> = 0' is always true. Unsigned type value is always> = 0. cee_wks codeman.cpp 1219
void EEJitManager::SetCpuInfo()
{
  ....
  unsigned char buffer[16];
  DWORD maxCpuId = getcpuid(0, buffer);
  if (maxCpuId >= 0)
  {
  ....
}

Similar example, only with the DWORD type.

V590 Consider inspecting the 'wzPath[0]! = L '\0' && wzPath[0] == L' \\'' expression. The expression is excessive or contains a misprint. cee_wks path.h 62
static inline bool
HasUncPrefix(LPCWSTR wzPath)
{
  _ASSERTE(!clr::str::IsNullOrEmpty(wzPath));
  return wzPath[0] != W('\0') && wzPath[0] == W('\\')
      && wzPath[1] != W('\0') && wzPath[1] == W('\\')
      && wzPath[2] != W('\0') && wzPath[2] != W('?');
}

This function can be asked to such option:
static inline bool
HasUncPrefix(LPCWSTR wzPath)
{
  _ASSERTE(!clr::str::IsNullOrEmpty(wzPath));
  return wzPath[0] == W('\\')
      && wzPath[1] == W('\\')
      && wzPath[2] != W('\0')
      && wzPath[2] != W('?');
}

Still such place:
  • V590 Consider inspecting this expression. The expression is excessive or contains a misprint. cee_wks path.h 72

V571 Recurring check. The 'if (moduleInfo[MSCORWKS].baseAddr == 0)' condition was already verified in line 749. sos util.cpp 751
struct ModuleInfo
{
    ULONG64 baseAddr;
    ULONG64 size;
    BOOL hasPdb;
};

HRESULT CheckEEDll()
{
  ....
  // Do we have clr.dll
    if (moduleInfo[MSCORWKS].baseAddr == 0)          //<==
    {
        if (moduleInfo[MSCORWKS].baseAddr == 0)      //<==
            g_ExtSymbols->GetModuleByModuleName (
               MAIN_CLR_MODULE_NAME_A,0,NULL,
               &moduleInfo[MSCORWKS].baseAddr);
        if (moduleInfo[MSCORWKS].baseAddr != 0 &&    //<==
            moduleInfo[MSCORWKS].hasPdb == FALSE)
        {
          ....
        }
        ....
    }
  ....
}

In the second case of 'baseAddr' it is already possible not to check.

V704 'this == nullptr' expression should be avoided — this expression is always false on newer compilers, because 'this' pointer can never be NULL. ClrJit gentree.cpp 12731
bool FieldSeqNode::IsFirstElemFieldSeq()
{
    if (this == nullptr)
        return false;
    return m_fieldHnd == FieldSeqStore::FirstElemPseudoField;
}

According to the standard C ++, the pointer this can never be zero. About possible effects of such code it is possible to read in detail in the description of diagnostics of V704. That such code can correctly work after compilation with the compiler Visual C ++, it is simple luck and it is impossible to rely on it frankly.

All list: CoreCLR_V704.txt.

V668 There is no sense in testing the 'newChunk' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error. ClrJit stresslog.h 552
FORCEINLINE BOOL GrowChunkList ()
{
  ....
  StressLogChunk * newChunk = new StressLogChunk (....);
  if (newChunk == NULL)
  {
    return FALSE;
  }
  ....
}

If the operator 'new' could not select memory, according to language standard of Xi ++, std exception is generated:: bad_alloc (). Thus it does not make sense to check to zero the pointer for equality.

It is better to check such places, here the complete list: CoreCLR_V668.txt.

Conclusion


Recently opened CoreCLR project good example of how the software closed source can look. On this subject discussions and here to you one more reason for reflections and discussions are constantly conducted.

It is important for us that the static analyzer can find some errors and the best application in any big project are regular checks. Are not lazy, download PVS-Studio and check the project.

This article in English


If you wish to share this article with English-speaking audience, I ask to use the reference to transfer: Svyatoslav Razmyslov. PVS-Studio: 25 Suspicious Code Fragments in CoreCLR.

Have read article and there is question?
Often to our articles ask the same questions. We have collected answers to them here: Answers to questions of readers of articles about PVS-Studio and CppCat, version 2014. Please, study the list.

This article is a translation of the original post at habrahabr.ru/post/253280/
If you have any questions regarding the material covered in the article above, please, contact the original author of the post.
If you have any complaints about this article or you want this article to be deleted, please, drop an email here: sysmagazine.com@gmail.com.

We believe that the knowledge, which is available at the most popular Russian IT blog habrahabr.ru, should be accessed by everyone, even though it is poorly translated.
Shared knowledge makes the world better.
Best wishes.

comments powered by Disqus