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

Merged
merged 22 commits into from
Jan 8, 2025

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 820d18b

Copy link

github-actions bot commented Dec 13, 2024

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1219.27 ms 1249.75 ms 30.48 ms
Size 22.31 KiB 765.79 KiB 743.48 KiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
9cc7e7c 1231.84 ms 1245.24 ms 13.41 ms
8e76be4 1272.67 ms 1286.38 ms 13.71 ms
2401cbd 1219.49 ms 1250.14 ms 30.65 ms
e8b14db 1193.58 ms 1225.56 ms 31.98 ms
5d6ce0e 1227.57 ms 1241.08 ms 13.51 ms
d8cc6ae 1228.13 ms 1253.86 ms 25.73 ms
ec879f7 1304.84 ms 1337.04 ms 32.20 ms
c677654 1228.02 ms 1248.65 ms 20.63 ms
7bb0873 1193.70 ms 1222.74 ms 29.04 ms
038edae 1223.35 ms 1246.78 ms 23.43 ms

App size

Revision Plain With Sentry Diff
9cc7e7c 22.84 KiB 403.14 KiB 380.29 KiB
8e76be4 20.76 KiB 427.66 KiB 406.89 KiB
2401cbd 22.85 KiB 408.85 KiB 386.00 KiB
e8b14db 20.76 KiB 401.60 KiB 380.84 KiB
5d6ce0e 22.85 KiB 405.38 KiB 382.53 KiB
d8cc6ae 21.58 KiB 699.25 KiB 677.67 KiB
ec879f7 21.58 KiB 669.68 KiB 648.10 KiB
c677654 22.30 KiB 748.66 KiB 726.36 KiB
7bb0873 22.85 KiB 407.09 KiB 384.24 KiB
038edae 21.58 KiB 678.19 KiB 656.61 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
d747559 1237.35 ms 1253.76 ms 16.41 ms
c1ccd22 1223.02 ms 1244.00 ms 20.98 ms
51c07c7 1244.06 ms 1261.04 ms 16.98 ms
83a0223 1243.98 ms 1257.29 ms 13.31 ms
428b027 1236.82 ms 1256.59 ms 19.78 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
d747559 22.31 KiB 760.76 KiB 738.44 KiB
c1ccd22 22.31 KiB 758.83 KiB 736.52 KiB
51c07c7 22.31 KiB 765.80 KiB 743.49 KiB
83a0223 22.31 KiB 760.68 KiB 738.36 KiB
428b027 22.31 KiB 760.74 KiB 738.43 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';

Copy link

codecov bot commented Jan 2, 2025

Codecov Report

Attention: Patch coverage is 98.51852% with 6 lines in your changes missing coverage. Please review.

Project coverage is 91.232%. Comparing base (79239ff) to head (820d18b).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...ntryCrash/SentryCrashMonitor_CppException_Tests.mm 91.803% 5 Missing ⚠️
...Tests/SentryCrash/SentryThreadInspectorTests.swift 75.000% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@              Coverage Diff              @@
##              main     #4636       +/-   ##
=============================================
+ Coverage   90.588%   91.232%   +0.643%     
=============================================
  Files          623       622        -1     
  Lines        71285     71660      +375     
  Branches     25359     26101      +742     
=============================================
+ Hits         64576     65377      +801     
+ Misses        6617      6184      -433     
- Partials        92        99        +7     
Files with missing lines Coverage Δ
Sources/Sentry/SentryAsyncSafeLog.c 100.000% <100.000%> (ø)
...rding/Monitors/SentryCrashMonitor_CPPException.cpp 75.000% <100.000%> (+49.242%) ⬆️
Sources/SentryCrash/Recording/SentryCrashC.c 79.577% <100.000%> (+11.971%) ⬆️
Sources/SentryCrash/Recording/SentryCrashReport.c 53.846% <100.000%> (+19.963%) ⬆️
...ces/SentryCrash/Recording/SentryCrashReportFixer.c 87.283% <100.000%> (ø)
...SentryCrash/Recording/Tools/SentryCrashFileUtils.c 87.764% <100.000%> (+1.882%) ⬆️
...SentryCrash/Recording/Tools/SentryCrashJSONCodec.c 87.093% <ø> (+0.697%) ⬆️
...ests/SentryTests/Recording/SentryCrashCTests.swift 100.000% <100.000%> (ø)
...ntryTests/SentryCrash/SentryCrashFileUtils_Tests.m 100.000% <100.000%> (ø)
...Tests/SentryCrash/SentryThreadInspectorTests.swift 85.947% <75.000%> (ø)
... and 1 more

... and 30 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 79239ff...820d18b. Read the comment docs.

@philprime philprime marked this pull request as ready for review January 2, 2025 15:08
CHANGELOG.md Outdated Show resolved Hide resolved
Copy link
Member

@armcknight armcknight left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work to make the changes and write the tests that cover a lot of implementation code.

We use a common pattern of sut (system under test) which you might be able to use to refactor some of the tests. I know there's a tension between DRY and self-contained test logic. Figured I'd just offer it up as food for thought.

Overall I think the changes are fine so this has my approval. I merely offer some suggestions and comments along the way.

Copy link
Member

@philipphofmann philipphofmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have nothing to add as @armcknight knowledge on C is way more extensive than mine.

Copy link
Member

@philipphofmann philipphofmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a few smoke tests, if the crash reports end up successfully in Sentry via the sample app via Testflight, would reduce the chance of bugs. We should test this with the following buttons in the iOS-Swift sample app: Throw FatalError, Crash the app, force unwrap optional, and an unhandled CPP exception, which isn't in the sample app I believe. Finally, we should do a beta release with our dogfooding app before releasing this.

@philprime
Copy link
Contributor Author

Passing smoke tests performed manually with errors reported in Sentry:

Sample App - macOS-Swift:

  • raiseNSException ✅
  • reportNSException ✅
  • NSRangeException ✅
  • captureTransaction ✅
  • Sentry.crash() ✅
  • CPPException ✅
  • Rethrow No Active CPP Exception ✅
  • async crash ✅
  • disk write exception ✅

Sample App - iOS-Swift:

  • Capture Error ✅
  • Capture NSException ✅
  • Throw FatalError ✅
  • OOM crash ✅
  • Force unwrap optional ✅
  • DiskWriteException ✅
  • Crash the app ✅

@philprime
Copy link
Contributor Author

These also show up in the Testflight crashes as expected:

  • Throw FatalError ✅
  • Force unwrap optional ✅
  • Crash the app ✅

LGTM?

Copy link
Member

@armcknight armcknight left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, nice work @philprime !

.github/workflows/testflight.yml Show resolved Hide resolved
@philprime philprime merged commit d8bb3eb into main Jan 8, 2025
71 checks passed
@philprime philprime deleted the philprime/strncpy-replacement branch January 8, 2025 08:16
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
3 participants