You should read this article fully first: https://learn.microsoft.com/en-us/dotnet/framework/interop/default-marshaling-for-arrays#unmanaged-arrays
I also found this relevant QAs: How to create and initialize SAFEARRAY of doubles in C++ to pass to C#
Your GetCppArray function only returns a pointer - it doesn't return a self-describing "safe" array, whereas arrays in .NET include length and rank (dimension) information, so you definitely need to modify the C++ code to do this correctly.
The first option is to return the array as a COM-style safe array, this is done with the SAFEARRAY( typename ) macro - and it must be passed as a parameter, not a return value.
There are two main ways of using COM Safe-Arrays in C++: using the Win32 functions like SafeArrayCreate - which are painful to use correctly, or by using the ATL CComSafeArray.
(Disclaimer: I wrote this code by looking at the API references, I haven't tested it - I don't even know if it will compile).
// C++ code for SafeArrayCreate:
#include <comdef.h>
int test_data[5] = { 12, 60, 55, 49, 26 };
extern "C" __declspec(dllexport) HRESULT GetCppArray( [out] SAFEARRAY( int )** arr )
{
    SAFEARRAYBOUND bounds;
    bounds.lLbound   = 0;
    bounds.cElements = sizeof(test_data);
    *arr = SafeArrayCreate(
        VT_I4,  // element type
        1,      // dimensions
        &bounds 
    );
    if( !arr ) {
        // SafeArrayCreate failed.
        return E_UNEXPECTED;
    }
    int* arrPtr;
    HRESULT hr = SafeArrayAccessData( *arr, &arrPtr );
    if( !SUCCEEDED( hr ) ) {
         hr = SafeArrayDestroy( arr );
         // After SafeArrayDestory, if `hr != S_OK` then something is really wrong.
         return E_UNEXPECTED;
    }
    for( size_t i = 0; i < sizeof(test_data); i++ ) {
        *arrPtr[i] = test_data[i];
    }
    hr = SafeArrayUnaccessData( *arrPtr );
    if( !SUCCEEDED( hr ) ) {
         hr = SafeArrayDestroy( arr );
         return E_UNEXPECTED;
    }
    return S_OK;
}
The C# code then needs to be updated to declare it returns a SafeArray:
// HRESULT is best represented as a UInt32 instead of Int32.
[DllImport( "CppDll.dll", CallingConvention = CallingConvention.Cdecl )]
public static extern UInt32 GetCppArray(
    [MarshalAs( UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_I4 )] out Int32[] arr
);