Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: replace occurences of strncpy to strlcpy #4636

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

philprime
Copy link
Contributor

@philprime philprime commented Dec 13, 2024

📜 Description

Renames all occurences of strncpy with strlcpy as explained in #2783.

💡 Motivation and Context

I looked all the occurences and tried to create additional unit tests where applicable. Some of the cases using strlcpy have larger buffer sizes set, i.e. SentryCrashFU_MAX_PATH_LENGTH is set to 500, but I was not able to file with a longer name because the OS throws NSPOSIXErrorDomain, code 63, indicating that the file system does not support such long files.

Closes #2783

💚 How did you test it?

📝 Checklist

You have to check all boxes before merging:

  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

  • Validate if additional tests are necessary
  • Add unit tests for CPPExceptionTerminate to test if strncpy_safe and strlcpy do the same
  • Add unit tests for sentrycrashreport_writeRecrashReport
  • Add unit tests for deletePathContents for too long file paths

@philprime philprime changed the title feat: rename strncpy to strlcpy; added tests feat: replace occurences of strncpy to strlcpy Dec 13, 2024
Copy link

github-actions bot commented Dec 13, 2024

Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against ce9668b

Copy link

github-actions bot commented Dec 13, 2024

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1223.02 ms 1244.00 ms 20.98 ms
Size 22.31 KiB 758.83 KiB 736.52 KiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
ca507ec 1224.43 ms 1246.04 ms 21.61 ms
a176fc4 1226.24 ms 1247.50 ms 21.26 ms
2a769ba 1217.92 ms 1239.78 ms 21.86 ms
fdfe96b 1227.90 ms 1242.56 ms 14.66 ms
154f795 1250.38 ms 1274.54 ms 24.16 ms
9d56232 1192.09 ms 1228.86 ms 36.77 ms
f8fc36d 1226.31 ms 1247.80 ms 21.49 ms
16450b8 1196.33 ms 1217.26 ms 20.93 ms
2eb78be 1242.15 ms 1259.00 ms 16.85 ms
7c5d161 1224.38 ms 1249.66 ms 25.28 ms

App size

Revision Plain With Sentry Diff
ca507ec 21.58 KiB 616.76 KiB 595.17 KiB
a176fc4 22.84 KiB 403.24 KiB 380.39 KiB
2a769ba 21.58 KiB 683.64 KiB 662.05 KiB
fdfe96b 20.76 KiB 419.70 KiB 398.95 KiB
154f795 20.76 KiB 435.25 KiB 414.49 KiB
9d56232 20.76 KiB 425.80 KiB 405.04 KiB
f8fc36d 20.76 KiB 419.70 KiB 398.94 KiB
16450b8 22.85 KiB 410.98 KiB 388.13 KiB
2eb78be 21.58 KiB 706.97 KiB 685.39 KiB
7c5d161 20.76 KiB 414.44 KiB 393.68 KiB

Previous results on branch: philprime/strncpy-replacement

Startup times

Revision Plain With Sentry Diff
12db161 1227.21 ms 1242.79 ms 15.58 ms
30fe95a 1230.39 ms 1243.60 ms 13.22 ms
3ec7df9 1225.36 ms 1247.29 ms 21.93 ms

App size

Revision Plain With Sentry Diff
12db161 22.31 KiB 757.18 KiB 734.88 KiB
30fe95a 22.31 KiB 757.18 KiB 734.87 KiB
3ec7df9 22.31 KiB 756.53 KiB 734.22 KiB

@armcknight
Copy link
Member

One thing I'd recommend here is to wrap whatever function we actually want to use in a function we control, to make it easier to test and potentially change later. It could be the case that strncpy is ok in some places, but not others where we'd then want strlcpy. (I wrote this paragraph before taking a deeper look at the code, and it looks like we used to have such a function, and we do have places in the code that emulate strlcpy and others that don't. So, we would need to validate the behavior after making this change).

My understanding after reading the docs is that strncpy is fine to use for copying from string buffers to characters arrays, but doesn't guarantee null termination (although does fill in the remainder of the array if the size of the source buffer is lesser than the length of the destination char array), which, I don't know why that's actually needed in a character array aside from copying it back to a buffer, which I don't think we do with any of these.

from https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/strncpy.3.html#//apple_ref/doc/man/3/strncpy:

The strncpy() function copies at most n characters from s2 into s1. If s2 is less than n characters long, the remainder of s1 is filled with `\0' characters. Otherwise, s1 is not terminated.

strlcpy is guaranteed to write at least one null terminator, and so that must be accounted for in the length parameter passed into it. I think straight replacement will result in one less character of actual data being written to accommodate that, so we might have to add a +1 to each place here?

from https://linux.die.net/man/3/strlcpy:

The strlcpy() function copies up to size - 1 characters from the NUL-terminated string src to dst, NUL-terminating the result.

I guess this is what this line of code is trying to replicate: https://github.com/getsentry/sentry-cocoa/pull/4636/files#diff-de0cc4487a50688dff672b8cd13b3901fdbf09abe93afa34b54bd561d1796fe8L1260 and I also just saw we actually already had another function to do this: https://github.com/getsentry/sentry-cocoa/pull/4636/files#diff-898d2165aedeeac3e2023d294e4bffee79b96b5c04d9d9d5be7e7593d4eb9aeeL21-L27

This is why a wrapper function is my preference, we can hide all these details and keep things consistent. If we have places that move data from char arrays back to buffers, we should also have a wrapper function for that as well.

@philprime philprime self-assigned this Dec 16, 2024
@philprime
Copy link
Contributor Author

Thanks @armcknight for reviewing the PR.
I haven't had a chance yet to add more written context, therefore the PR is marked as draft.

As requested in the issue and mentioned by you, we need to make sure the changes do not break the SDK. That's why I went ahead and started adding additional unit tests.

strlcpy is guaranteed to write at least one null terminator, and so that must be accounted for in the length parameter passed into it. I think straight replacement will result in one less character of actual data being written to accommodate that, so we might have to add a +1 to each place here?

I was thinking of the same, and yes we might need to extend the buffers at some places by one character, if it actually uses the full buffer length.

I also found the function strncpy_safe, but looking at the implementation it seems to work mimic the behaviour of strlcpy so I don't see a point in maintaining a duplicate implementation.

static inline char *
strncpy_safe(char *dst, const char *src, size_t n)
{
strncpy(dst, src, n - 1);
dst[n - 1] = '\0';
return dst;
}

TL;DR: I added todos to the description what is left to do.

In total I found 7 uses of strlcpy and strncpy, so I'll quickly add my thoughts for each so far:

  1. int sentry_asyncLogSetFileName(const char *filename, bool overwrite):
  • Called by SentryAsyncLogWrapper with the full path of the async.log file, which is placed in the result of NSSearchPathForDirectoriesInDomains. Therefore the length of the path is controlled by the OS.
  • Currently no maximum file path length is enforced → therefore the current implementation using strncpy will write up to 1024 characters leaving it unterminated, with strlcpy up to 1023 characters but definitely terminated.
  • Copies the given filename to the g_logFileName, which is a 1024 char buffer.
  • The g_logFileName is used by addTextLinesFromFile(..) in SentryCrashReport.c which then uses the path with open to create a file descriptor.
  • I don't think the g_logFileName should ever be unterminated, so we should definitely replace it.
  1. static void CPPExceptionTerminate(void):
  • Uses the strncpy_safe to copy the exception description into a 1000 char long descriptionBuff.
  • The description should be null terminated.
  • As mentioned above, the strncpy_safe method tries to do the same as strlcpy, so I believe we should replace this.
  • I will try to create a unit test with a description longer than 1000 chars, to verify the expected behaviour.
  1. static void onCrash(struct SentryCrash_MonitorContext *monitorContext):
  • Generates a crash report file path using sentrycrashcrs_getNextCrashReportPath, then writes it to a local variable crashReportFilePath with a size of SentryCrashFU_MAX_PATH_LENGTH = 500 characters.
  • sentrycrashcrs_getNextCrashReportPath uses getCrashReportPathByID to generate the path
  • getCrashReportPathByID uses snprintf which also has SentryCrashCRS_MAX_PATH_LENGTH defined as the max length.
  • The generated path will be null terminated, from https://linux.die.net/man/3/snprintf:

The functions snprintf() and vsnprintf() write at most size bytes (including the terminating null byte ('\0')) to str.

  • Therefore crashReportFilePath will always be null terminated and can safely be copied to g_lastCrashReportFilePath without changing the buffer size.
  • I added tests in SentryCrashCTests.swift
  1. void sentrycrashreport_writeRecrashReport(..., const *path, ...):
  • This one I still need to verify/test, because it uses strncpy to copy path to a tempPath which has a length limit of SentryCrashFU_MAX_PATH_LENGTH, but then replaces the last characters with a .old extension
  • I tried to write unit tests in SentryCrashCTests.swift but kind-off failed because it tries to create a monitor context from thread data, which I do not fully understand yet.

char writeBuffer[1024];
SentryCrashBufferedWriter bufferedWriter;
static char tempPath[SentryCrashFU_MAX_PATH_LENGTH];
strncpy(tempPath, path, sizeof(tempPath) - 10);
strncpy(tempPath + strlen(tempPath) - 5, ".old", 5);
SENTRY_ASYNC_SAFE_LOG_INFO("Writing recrash report to %s", path);

  1. static bool increaseDepth(FixupDepth *context, const char *name):
  • Copies the given name to the context->objectPath[context->currentDepth]
  • If name is NULL it sets the context->objectPath[context->currentDepth] to \0, therefore an empty but terminated string
  • Based on that, I believe it is safe to assume that the context->objectPath[context->currentDepth] should always be terminated, and it is safe to use strlcpy instead of strncpy.
  • The object path length is MAX_NAME_LENGTH = 100 so we might have to increase the buffer size by one, as there could be names having exactly that length.

static bool
increaseDepth(FixupContext *context, const char *name)
{
if (context->currentDepth >= MAX_DEPTH) {
return false;
}
if (name == NULL) {
*context->objectPath[context->currentDepth] = '\0';
} else {
strncpy(context->objectPath[context->currentDepth], name,
sizeof(context->objectPath[context->currentDepth]));
}
context->currentDepth++;
return true;
}

  1. static bool deletePathContents(const char *path, bool deleteTopLevelPathAlso) in SentryCrashFileUtils.c:
  • The given path is passed to snprintf which truncates it to SentryCrashFU_MAX_PATH_LENGTH - 1 - 1 characters, then appending / and \0 and written to pathBuffer.
  • The address of the end of pathBuffer is set to pathPtr to calculate the remaining buffer length and set it to pathRemainingLength.
  • strncpy will use the the pathRemainingLength as the limit of characters copied to pathPtr and therefore to pathBuffer.
  • The pathBuffer is then used to recursively call deletePathContents which uses snprintf again to terminate the string, therefore we can use strlcpy instead of strncpy because it will be terminated anyways.

int bufferLength = SentryCrashFU_MAX_PATH_LENGTH;
char *pathBuffer = malloc((unsigned)bufferLength);
snprintf(pathBuffer, bufferLength, "%s/", path);
char *pathPtr = pathBuffer + strlen(pathBuffer);
int pathRemainingLength = bufferLength - (int)(pathPtr - pathBuffer);
for (int i = 0; i < entryCount; i++) {
char *entry = entries[i];
if (entry != NULL && canDeletePath(entry)) {
strncpy(pathPtr, entry, pathRemainingLength);
deletePathContents(pathBuffer, true);
}
}

  • I added additional unit tests in SentryCrashFileUtils_Tests.m
  • ⚠️ The current implementation might actually not work if the path is longer than SentryCrashFU_MAX_PATH_LENGTH. I will add another unit test to verify.
  1. decodeElement() in SentryCrashJSONCodec.c:
  • The current implementation performs manual null termination while using strncpy, so I believe it to be safe to replace it with strlcpy instead:

strncpy(context->stringBuffer, start, len);
context->stringBuffer[len] = '\0';

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Replace strncpy with strlcpy
2 participants