Identifying "required reason" API call locations from app binary

Dear Experts,

I've just received the exciting new email from App Store Connect telling me that I'm using a "required reason" API call and need to declare it in my privacy manifest. Of course this is easy to fix, I'll just add the code to my privacy manifest - but I thought I'd at least go through the motions of trying to work out what function I am calling and from where.

First issue is that the email just tells me that the app "references one or more APIs that require reasons ... including NSPrivacyAcceeedAPICategoryFileTimestamp". Dear Apple, why on earth can't you actually tell me the specific function that I am calling? (FB13689896).

So let's see if I can work out what has been detected. I look at the app binary:

% objdump --syms App.app

I think that is probably more or less what App Review must get from their scan, right? So I can see _stat in there but it doesn't know the corresponding source file.

So I go to the build directory with the object files and extract symbols from them all individually, using objdump --syms. Provided that I've not enabled link-time optimisation that works and I can find ... zero calls to stat(). Which tells me that my C++ std::filesystem calls have not been detected! Interesting. So if you want to bypass this amazing new privacy technology, I guess that's the way to go.

Anyway if there's a call to stat() in the binary but not in the object files, it must be coming from one of my .a files. That's a bit more difficult to track down as (1) my .a files are not in a convenient single directory, and (2) they may have calls to stat() in archive members that aren't needed and aren't included in this binary.

So the question: is there some convenient way to take the binary and identify which object files or static library archive members resulted in which of its UND symbols?

Hello, you could use this tool to search for the static library that contains the symbol https://github.com/Wooder/ios_17_required_reason_api_scanner If you want to know which specific object file contains it, you could unarchive the static library using: ar -x library.a

Thanks for the link. That seems to use nm to scan the object files, in much the same way that I was using objdump.

Note that for .a files, both nm and objdump group the symbols by object file. It's not necessary to unpack the .a file.

The remaining problem is how to determine which members of a .a archive were actually included in the app binary.

The remaining problem is how to determine which members of a .a archive were actually included in the app binary.

I have a post about that somewhere…

Oh, here it is: Using a Link Map to Track Down a Symbol’s Origin.

1970s-era technology ftw!

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks Quinn. I can almost answer the original question, "from where do I call stat()?", from the link map file - except it describes where symbols are defined, not where they are used. But I can use it to get the names of the archive members that are being included in the binary:

% grep '\.a\[.*\](.*\.o)' app-LinkMap-normal-arm64.txt
...
[256] /usr/local/iphone-arm64/lib/libboost_container.a[6](pool_resource.o)
...

I wasted a lot of time at this point because the nm man page mentions the archive(member) syntax

If an argument is an archive, a listing for each object file in the archive will be produced. File can be of the form libx.a(x.o), in which case only symbols from that member of the object file are listed. (The parentheses have to be quoted to get by the shell.)

but it doesn't work:

% objdump --syms '/usr/local/iphone-arm64/lib/libboost_container.a(pool_resource.o)'
...or...
% nm '/usr/local/iphone-arm64/lib/libboost_container.a(pool_resource.o)'

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/objdump: error: '/usr/local/iphone-arm64/lib/libboost_container.a(pool_resource.o)': No such file or directory

However it's not mentioned in the llvm-nm man page; it seems to be nm-classic only (FB13691747), and that's not on my PATH. But if I call it directly it works:

% /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/nm-classic -u '/usr/local/iphone-arm64/lib/libboost_container.a(pool_resource.o)'

So now I can get all my app's undefined symbols, grouped by the object file or archive member in which they are used:

cat app-LinkMap-normal-arm64.txt |
grep '^\[' |
sed 's/^\[[ 0-9]*\] \(.*\)$/\1/' |
sed 's/\[.*\]//' |
while read N
do
  echo "$N"
  /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/nm-classic -u "$N"
done

Splendid. I can now see that _stat is called from two places:

libcrypto.a(conf_def.o)
libcrypto.a(loader_file.o)

and _fstat is called from one place also in libcrypto, and that's it.

yes it seems super weird that they would not help you identify what's causing the warning. For example, the privacy report (via Organizer > right click on Archive) doesn't indicate much either. For me it just had a listing of a SPM package I'm linking. So do I have to fork that repo and fix it in the package, or does my privacy manifest act as an umbrella for any module in my binary? It's strange that they would just say "fix it" and not really tell you where and what the problem is.

I just uploaded a new build for TestFlight from which I had removed libcrypto, and I still got an email saying that I am missing an API declaration for NSPrivacyAccessedAPICategoryFileTimeStamp. So I attempted to identify the symbol:

% cd ~/Library/Developer/Xcode/DerivedData/blah/Build/Products/Debug-iphoneos/blah.app
% nm blah | grep 'NSFileCreation'
% nm blah | grep 'NSFileModification'
% nm blah | grep 'fileModification'   
% nm blah | grep 'NSURLContentModification'      
% nm blah | grep 'NSURLCreation'           
% nm blah | grep 'getattr'      
% nm blah | grep ' _stat' 
% nm blah | grep 'fstat' 
% nm blah | grep 'lstat'

So... no sign of any of the symbols in the published list.

What's going on here? Is my method for looking for symbols flawed? Is the published list of symbols incomplete? In particular, I am using C++ std::filesystem functions that access file timestamps, but they still aren't on the list; maybe they have been added to the checker but not to the published list?

Mabye a dumb question but are you using any swift code in your app? might be worth to check the swift symbols as well, i've no idea exactly how they'd show up in the nm output.

Right, I also don’t really know how swift symbols appear; I think there is some sort of mangling. Does anyone know? I would hope that the mangled names would still be found by grep.

(There is not much swift in this app, and certainly none that interacts with the filesystem.)

Right, I also don’t really know how swift symbols appear

If you’re calling a C or Objective-C API from Swift, there is no Swift symbol. Rather, the importer just sets things up for the Swift code to call the API directly. Consider this code:

import Foundation

func main() throws {
    let attr = try FileManager.default.attributesOfItem(atPath: "/")
    print(attr[.modificationDate])
}

try main()

It imports the Foundation symbols directly:

% nm -u -m Test748621 | grep NSFile
                 (undefined) external _NSFileModificationDate (from Foundation)
                 (undefined) external _OBJC_CLASS_$_NSFileManager (from Foundation)

There is some wiggle room here — sometimes Swift ends up referencing an overlay symbol rather than the original symbol — but it’s a good general rule.

I am using C++ std::filesystem functions that access file timestamps, but they still aren't on the list; maybe they have been added to the checker but not to the published list?

That’s an interesting edge case. The C++ standard library is part of the OS, so it’s possible you’re importing a worrisome symbol. OTOH, it’s common for C++ stuff to get inlined, in which case it’ll behave similarly to Swift.

What does that timestamp code look like?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

If you’re calling a C or Objective-C API from Swift

Are the specific things listed (NSFileCreation, NSFileModification, …) C / objC functions called from Swift? Should that grep work?

it’s common for C++ stuff to get inlined

Right; I looked this up previously in the libc++ source and it seems that these functions are not inlined. In my objdump I can see (mangled) std::filesystem::exists, but not stat.

Are the specific things listed

To be clear, by listed you mean the things under the File timestamp APIs section, right?

If so, then… well… yes.

But don’t take my word for it (-: You can generalise the test I described above to see exactly how each of these is presented in your app’s binary. For example, the contentModificationDate in a URL come out like this:

% nm -u -m Test748621 | grep contentModificationDate
                 (undefined) external _$s10Foundation17URLResourceValuesV23contentModificationDateAA0F0VSgvg (from libswiftFoundation)

It seems that the option value doesn’t appear in the binary but the getter for the URLResourceValues property does.

In my objdump I can see (mangled) std::filesystem::exists, but not stat.

Interesting.

Fortunately that just returns a Boolean, so it shouldn't impact on this issue.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

To be clear, by listed you mean the things under the File timestamp APIs section, right?

Yes.

OK. So what to do about that depends on the level of fidelity you want:

  • If you’re OK filtering out some false positives by hand, you could just grep for the ‘main name’ of the API. For example, if you search for contentModificationDate property you’ll find its mangled name _$s10Foundation17URLResourceValuesV23contentModificationDateAA0F0VSgvg, but you might also find other stuff that you’ll have to ignore.

  • OTOH, if you want to be precise about this, it’d be best to search for the full mangled name. Working out that mangled name can be a bit tricky. Earlier I outlined my approach for do that, which is to create a small test project that uses the symbol and then look at what gets generated.

    Hmmmm, you could probably shortcut that by searching through the .tbd stub library file in the SDK [1]. Then again, that can be a bit tricky for frameworks like Foundation, that have a core in Objective-C and then a Swift overlay.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] You’re probably familiar with stub libraries but, if not, I explain them in An Apple Library Primer.

Identifying "required reason" API call locations from app binary
 
 
Q