Hacker News new | past | comments | ask | show | jobs | submit login
System memory allocator free operation zeroes out deallocated blocks in iOS 16 (mjtsai.com)
163 points by ingve on Sept 22, 2022 | hide | past | favorite | 173 comments

When I want to make sure allocated memory gets initialized, I have the allocator set it to 0xDEADBEEF.

The trouble with 0 initialization is it can accidentally be correct, or can look correct when it isn't. When your pointer is pointing to 0xDEADBEEF, odds are 99.9999% that the data wasn't initialized.

D does a similar thing. The default initializer for floating point variables is NaN, not zero. There was a debate about this in the D forums.


My position was pretty clear - default initializations should not be accidentally correct.

There’s a good history supporting that logic but I wonder if there were other trade offs: a couple of the comments around this mentioned better memory compression and some optimization work which made me wonder whether they might have been able to make zeroing faster than a pattern, especially if they’re going to have some generation of Apple Silicon add hardware support.

Memory compression should handle any repeating initializer.

I think they stuck with 0x0 because it keeps the performance of any calloc()-using object allocator the same (just moves the "when" of zeroing the chunk).

Answering myself: there's also an ARM opcode for zero'ing memory. That's another reason to use 0x0.

> Memory compression should handle any repeating initializer.

That’s what I’d think, too, but I wouldn’t be shocked if someone said they had some implementation detail which made it easier to work with zeroes. I know ARM has pretty good performance zeroing memory but that’s exactly the kind of thing where their hardware team might optimize what is now an extremely common operation.

You get an efficiency problem there in that it means that the first thing allocations have to do is re-zero the allocation.

I think you got it. From some grumblings on twitter, it seems like calloc no longer zeros allocations since it assumes the memory is already zero'd and so assuming most people on the platform do the right thing (call calloc instead of malloc+memset/bzero), this change has zero performance impact. That might have some fun effects for writing exploits (if you can predict what page will be allocated next, you can do a sideways spray by hitting the region before its allocated and then let the downstream calloc callers get hit with unexpectedly non-zero fields.

Why would malloc zeroing memory have any impact on that? Yes calloc obviously no longer zeros memory, but calloc _can_ rely on that as it is part of the same libc. If you hit a region before it's been allocated you're either going to trigger a page fault, or overwrite memory that's going to be zero'd on allocation.

[this is an edit to answer my own question but I can't update the comment]

It sounds like the calloc implementation is _assuming_ that the underlying alloc always zeros, so essentially (ignore overflow check) calloc is doing something like forward directly to malloc(size*count). So I think it _changes_ the exploit path to something new.

Yeah, more or less! This closes some paths and opens brand new ones, like more of the former and less of the latter. For example, let's consider this code.

   void f(void) {
      obj *a;
      a = calloc(1, sizeof(a));

   void g(obj *o) {
      if (o->callback) {
it's contrived for simplicity, but suppose obj had a function pointer in it and just after allocation the object is passed to another routine which does some work on the object and checks if the callback exists and calls it if it does. In a single threaded application, you would not be able to target the function pointer here because nothing interesting happens between calloc and the invocation since the if check will always fail due to it being zero'd from calloc.

Now, as you mention, if calloc didn't zero, an attacker could scribble over what they think will be the next allocated slot in the heap (either by targeting a partially used page or an entirely unused page that's just being kept in malloc's retainer) and write a value into the callback pointer field. Now when a new object is allocated there, we end up using the corrupted data and invoking the evil pointer, and the rest is history.

It's a bit of a weirder exploit path but honestly with how ridiculous UAF flows have gotten with modern mitigations on iOS, it's hardly the worst thing I've ever seen and I suspect someone will get decent mileage out of it

I think that's draw of the luck with 0xdeadbeef vs 0x0. I had a case once were it would have made no difference, and I discovered it when there was no initialization in another platform, and all of a sudden we got random results for some old benchmark.

"Not accidentally correct" seems difficult in the generic case (think a 1 bit flag; of course for an int 0 is likely "more expected" than any other value); NaN for floats however is a nice variant that should work pretty well. Is it signaling or quiet?

> difficult in the generic case

We do what we can. D also uses 0xFF for char default initialization (an invalid code unit).

Yeah, there is only so much that's possible on the language side. Though static analysis in compilers can catch some instances of some problems. But looking up your name, I guess you probably already knew that :)

I'd love to see hardware having a "offloaded memory guard" flag: Mark in-use memory with it and get a OMG trap on any memory access with the flag not set (of course totally transparent with hardware acceleration). Too bad this is only simple on paper, where there is no such thing as (among others) memory latency.

macOS and iOS do something similar when you're debugging your app. By default Xcode sets various environmental variables to set newly allocated memory to 0xAAAAAAAA and deallocated memory to 0x55555555. This makes it pretty clear when looking at something in the debugger that you've hit either uninitialized or stale data.

I wonder how long until programs start to accidentally depend on it, the same way some programs currently accidentally depend on freeing memory not immediately overwriting it.

For instance, I can imagine a program which accidentally follows a dangling pointer to an already freed structure, and reads another pointer from within that structure, not crashing because it ends up checking this later pointer against NULL... until the stars align and the memory used by the freed structure has been returned to the operating system, or overwritten by a later allocation.

I urge anybody using a C family language to become familiar with AddressSanitizer [1], a tool that (with support from the compiler) detects an impressive variety of memory bugs at runtime. The scenario you describe would result in the program immediately crashing with a read-after-free error, along with stack traces for the most recent calls to malloc and free that involved that address. It's remarkably useful and straightforward, and Apple's tooling has good support for it, along with Xcode integration [2].

[1] https://clang.llvm.org/docs/AddressSanitizer.html

[2] https://developer.apple.com/documentation/xcode/diagnosing-m...

This. I personally think ASAN and it's friends are some of, if not the, best things to happen to C/C++ in the last decade. We use it at work, I use it in all my personal projects. We've found "countless" bugs with ASAN together with LSAN.

It should be part any project from the start, run as much as possible with ASAN when the performance hit can be afforded.

Just confirming, but you're taking about Leak Sanitizer? https://clang.llvm.org/docs/LeakSanitizer.html. Not really familiar with these tools, definitely not the acronyms.

Sorry, yes, I meant Leak Sanitizer.

No need to be sorry! I appreciate it!

It was actually in the link the parent posted. I just missed it at first.

Agreed! Likewise, if you're doing anything multithreaded, ThreadSanitizer (TSAN) is a must!

I’m using sanitizers on macOS for few years now. Doing cross-platform, ~~it really feels inferior~~ I really miss them on MSVC side. (wrt to exclusive asan since VS2019)

Still, in some cases you have some tiny uninitialized member for example that is much harder to find. So with this malloc it might even cover those more.

On Mac, malloc has several options to help find such bugs. If you set MallocScribble=1 in the environment, it fills blocks with 0xaa on alloc and 0x55 on free.

On Linux, running `env MALLOC_PERTURB_=43605 myprogram` will do the same.

For the confused, 43605 in decimal is 0xaa55 in hexadecimal.

That should be the default behaviour, perhaps with random values too.

It’s easier when it is a specified value. Easier to see in a debugger and to diagnose. Sure, it can occasionally be interpreted as meaning something, but so can random gibberish. As long as everything is overwritten, there is no privacy issue.

> Sure, it can occasionally be interpreted as meaning something, but so can random gibberish.

Some gibberish is better than others. For instance, on x86 (64-bit, 32-bit, or even 16-bit), filling with the 0xcc byte means that, if executed as code, it will trap (0xcc is the single-byte INT3 "single-step breakpoint" instruction). And for most Unix-style operating systems, which put the kernel in the high half of the address space, the filler bytes should have the high bit set, so that treating them as pointers will trap (the pointer will have its high bit set, pointing to kernel memory, which user space is forbidden from accessing).

Having a different pre-defined value for allocation vs deallocation scribble is nice because you can then tell if the error happened pre-initialization or post-free. $AA and $55 also weren't chosen by accident one assumes, $AA is 1010_1010 and $55 is 0101_0101. Consistent values help with reproduction.

The Linux setup where you can choose your own scribbles seems to meet your needs no?

> For instance, on x86 (64-bit, 32-bit, or even 16-bit), filling with the 0xcc byte means that, if executed as code, it will trap (0xcc is the single-byte INT3 "single-step breakpoint" instruction).

Trying to execute off the heap is going to trap anyways.

Or just use valgrind.

Or just use a memory safer programming langage.

If your use case fits into capabilities of such language, and you have the necessary libraries, and bindings, etc., then why not?

It’s slower.

As long as Apple's allocator continues to zero freed memory, programs accidentally depending on that behavior will only be a problem for other operating systems.

> As long as Apple's allocator continues to zero freed memory, programs accidentally depending on that behavior will only be a problem for other operating systems.

As I mentioned, only until that freed block is reused for a new allocation, which means it will no longer be all-zeros; or until the memory allocator decides it's time to unmap that region, which means it will trap (SIGSEGV or similar).

Memory allocators generally don't unmap anything, because the performance hit on any other threads running is severe.

When memory is unmapped, caches of memory mappings of other threads are discarded, via inter-core interrupts. Then they get misses until their cache is restored.

In what might be a multi-thread program, you don't fool with the memory map without very good reasons. Mapping new pages, or marking a page r/o or r/w is OK; anything else, probably not.

> When memory is unmapped, caches of memory mappings of other threads are discarded, via inter-core interrupts.

ARM architecture does not need to interrupt for TLB shootdown.

> which means it will trap

Is it really a problem? Failing harder and earlier is a good thing if it means the bug has more chances of being detected and fixed.

Possibly but there's the potential reverse problems where Apple programs start depending on newly allocated memory being zero .... and those programs start failing elsewhere ...

Well the change is to zero out memory on deallocation. So on allocation it may or may not be zero depending on whether it was previously freed, right?

not necessarily, even if was freed there might be defragged free blocks that still contain heap metadata by the time it gets reallocated

Might fail there, too, but only sometimes, if the memory gets allocated again.

Or until the particular allocation moves to a custom/separate heap or other non-system-malloc allocator. That sort of tuning is very common in big apps.

This is better than the status quo of use-after-free being abused for security exploits as part of an exploit chain for sandbox escape.

  "the same way some programs currently accidentally depend on freeing memory not immediately overwriting it."
Maybe a dumb question, but what do you mean by this? Could you give a C/C++ pseudo-example?

Not the poster, but it’s fairly easy to accidentally do the equivalent of

in code that does a few malloc, if/else’s, etc. between those calls. Stuff like that gets hard to spot in more complex code, especially in the presence of C++ copy constructors, move constructors, etc, where the compiler helpfully inserts that call to free for you.

Bugs in copy constructors also can lead to double frees when the destructors of an object and its copy both call delete on a pointer that they think they exclusively own. Between those two destructor calls, you can easily get a use after free. Example: https://stackoverflow.com/a/64014035

If the allocator doesn’t change the content of freed blocks, such bugs can go unnoticed, making the program “accidentally depend on freeing memory not immediately overwriting it”.

> especially in the presence of C++ copy constructors, move constructors, etc,

You only mean the former, not both. The only reason for the existence of the giant rats' nest of complexity that is C++ move semantics is eliminating this particular mistake.

Ooh, I have a good one for this. If you have a C++ `std::string` object passing into a C API that takes a null-terminated `char *`, there's a convenience method `std::string::c_str()` to produce the C string from the C++ one.

    char const* inputFilename = std::string("path/to/file").c_str();
Boom. You can typically access `inputFilename` until the next allocation on the stack, but it will almost always go bad quickly. This isn't allocation on the heap, which usually requires another level of indirection, but you get the idea.

> This isn't allocation on the heap, which usually requires another level of indirection, but you get the idea.

Isn't it allocating in the stack due to the "small string optimization"? Making string contents longer (or using a C++ standard library which doesn't have a "small string optimization" in the first place) would be enough to have it be allocated in the heap instead.

No, with longer string contents the std::string will internally allocate and store on the heap, but as the containing std::string is a temporary it will be pop'd of the stack (stack pointer moved, which is why it works "sometimes") and its destructor called at the end of the statement (at the semicolon), freeing the heap allocated memory (which, as parents are discussing, may still work because nothing has yet overwritten the recently freed memory).

Changing it to:

Makes it valid because the std::string object will live until the semicolon.

Used to be linux zerod allocated pages only upon first use. But the time to take the page fault was larger than the time to zero the page so they changed to zero-immediately.

Modern processors zero things very, very fast. This seems like a defensible approach.

On Windows, there's that lowest priority task (Zero Page Allocator) whose sole purpose is to zero out the freed pages and add them to the available pages list. This way, allocation tasks don't have to wait for zeroing.

IIRC, Zero Page Allocator's priority was so low that it was even lower than the lowest priority available to Win32 API's.

Yes, but this is a heap, not a VM. You're not streaming through the cache into DRAM, you're polluting the cache with dirty lines that the app would presumably like to be using for near-future loads. The performance impact is obviously going to depend on usage patterns, but it certainly could be much higher.

It is the default I wish in all de-allocations no matter what OS you are talking about.

That way there is no 'dead' data to be probed for.

Well, it's also waste of time and if your app is constantly allocating and deallocating stuff it would take a lot more.

If you are concerned about performance at that level you likely shouldn't be allocating and de-allocating memory all the time, as that is slow even without zeroing. You should use a memory pool to allow re-using your allocated memory.

> You should use a memory pool to allow re-using your allocated memory

Soon: programs implement their own allocators and memory pools and stop using the libc malloc, and thus stop benefiting from all the safety valves added to the libc malloc (including TFA), all because of the rumor that the libc malloc is slow (when it likely isn't).

Actually, that happened a couple decades ago.

Sometimes a well placed "placement new" is enough. Memory pools are also nice, but at least for my work we ditched them again once the Windows allocator became faster.

(I can count the times I did either of these on one hand, but of course this depends a lot on the code your working on).

seems like something that could be handled at the memory/controller level instead of wasting CPU cycles writing 0's.

It can be faster or slower, it's a wash at worst if you're calloc()ing later anyway, and it helps swap (or memory compression on swap-less iOS).

Wait, what calloc (3) no-longer zeros though?

>Memory Allocation

>Known Issues

>The system memory allocator free operation zeroes out all deallocated blocks in macOS 13 beta or later. Invalid accesses to free memory might result in new crashes or corruption, including NULL-pointer dereferences and non-zero memory being returned from calloc. (97449075)


edit: I guess this is taking advantage of use-after-free being undefined behavior.

From what I understood, the trick is that, for a block which was returned by free() and later returned by calloc(), the former behavior was to zero within the calloc(), while the new behavior would be to zero within the free(). If there's a bug which writes to the memory after the free() but before the calloc(), the former behavior would mean it's still zeroed, but the new behavior would mean it ends up overwritten with something which isn't all-zeros.

I'm not sure I see it as "taking advantage of"; if code is engaging in the UB of writing to invalid pointers, there could still have a race condition where calloc() appears to be returning non-zeroed memory, simply because the invalid pointer write happened after the clear. This just moves the clear back in time.

calloc(3) isn't the only way to allocate memory, nor the most common one in ObjC apps.

Actually, ObjC objects are allocated with calloc() so it might be the most common one.

It also has a better interface than malloc() because it can check for overflow evaluating `size*count`, though it'd be even better if C had first-class types and you could pass that in.


> Hopefully you aren’t relying on any abandoned software.

Every major Android update, I get scared. I use an alarm clock (Gentle Alarm) that has far more features than all others, but it’s been long abandoned (as in: removed from the play store, all websites dead. This was a paid app). Android 10 or 11 already broke it, but someone on reddit (where all 4 or so users of the app met :D) found a manifest change and allowing "drawing over other apps" to fix it.

Now it works on 12, but after I already spent hours asking on HN and Reddit for a maintained replacement and testing those, I hope this continues, because with how lacking all others are I’d rather learn Android development and program my own replacement than using something else.

edit: Hah! The thread [0] with the fixed APK is still getting comments from people who found it every few months :D

[0]: https://old.reddit.com/r/androidapps/comments/i01xh3/gentle_...

This is a problem with how we treat backwards compatibility on mobile operating systems. Imagine if this was an app for Windows. Would you still be scared of updating?

This is why no one ever pays more than a couple bucks for a mobile app. That would be a terrible investment, as the app could break at any time!

Similarly, this is why mobile will never be a good platform for traditional single-player video games.

Very true. Apple has removed several of my games (all working perfectly fine) because, well, they felt like it:

> Since this app hasn’t been updated within the last three years [...] it has been removed from the App Store

I hate this. I don't think it's the right approach to improving search and discoverability in the app store. (My personal suggestion for instantly improving it 100% would be removing ads that eat up the first half-screen of search results on an iPhone.)

I also greatly dislike how iOS takes the backwards approach of offloading backward compatibility as a yearly support burden multiplied across every developer - rather than maintaining it in the OS.

Platforms are supposed to absorb developer pain for multiplicative benefit - not the other way around.

As a developer I’m not sure that I agree. Some degree of backwards compatibility is good, but it quickly becomes an unwieldy burden that acts as a ball and chain on the development of the OS and platform toolkit.

Among modern operating systems, iOS is probably the worst in terms of backwards compatibility but it’s also one of the most pleasant to develop for — UIKit/Cocoa is unrivaled in terms of both breadth and depth. One can easily build a top class iOS app with few or no third party dependencies, which is not the case on most other platforms. I don’t think that’s a coincidence… if the iOS dev team were encumbered with maintaining backwards compatibility for upwards of a decade it would be much more difficult to build such a complete and polished toolkit.

If iOS had versioned libraries and separate teams maintaining the older branches, would that really impede the modern toolkit? I realize that expanding the scope of a project by adding more developers isn't as simple as it seems, but I feel like there ought to be an organizational/development structure that could make this work.

I wouldn't even be opposed to older apps running in some sort of seamless-to-the-user virtual machine.

Yeah this is insane. I bought Sonic CD and Sim Tower for iOS years ago and they're both just completely gone. Not listed, impossible to install. I'm never buying a mobile game again.

It blows my goddamn mind.

Android's backwards compatibility, especially for apps that don't update, is quite solid. The cases where previously working behavior is broken without requiring the app to "opt-in" to the breakage is generally scoped solely to things that are considered abuse vectors. Which "draws over other apps" unfortunately is.

It's not as good as Windows' back compat, no. But then again nothing is, as that's the only platform concerned with preserving not just API compatibility but bug compatibility.

There's one case I know of where previously working behavior is broken, for something which was not an abuse vector at all: if your app depends on the hardware "menu" button (which is the case for nearly all apps made back when having a hardware "menu" button was standard for Android), for several versions Android showed an emulated software "menu" button next to the back/home/etc buttons, but more recent Android versions no longer show that button, breaking all these apps (since there's no way to get to their menu). See https://www.xda-developers.com/rip-menu-button-android-10-dr... for details.

Android's backwards compatibility is atrocious with a capital A.

Every version has random will-be-breaking changes that get held off if your app was compiled before said version came out, but there's only a release or two before the change starts to ignore your compiled version (for example, when a massive permissions change comes down the pipe, any app that's compiled against the newest SDK breaks immediately, and apps that were compiled against older ones just get to break next release)

And just like any mobile platform, apps heavily rely on the OS to provide UI widgets and layout. So even design language changes can (and often do) break the UI.


What you're probably seeing is what developers are forced to work around: the fact that everyone is stuck targeting ancient versions of Android because Android users have just accepted their devices never getting OS upgrades.

Android Studio will still default to targeting a minimum SDK version of API 25. Android 5.0. From November 12 2014.

The situation is so bad that Google removed it from their dashboard (https://developer.android.com/about/dashboards) and buried it in the IDE: https://9to5google.com/2022/05/20/android-2022-distribution-...

Nearly a third of Android devices are on a version of Android >5 years old... just imagine if all your iOS apps had to target iOS 11 as a minimum feature set, and XCode defaulted to supporting iOS 8 for all new projects

When the apps can't rely on any newer features, it creates the illusion of backwards compatibility.

Uhh... Like all of that is wrong. Android Studio defaults to targeting the latest API level, and Play Store even refuses to let you upload an APK targeting something more than a couple APIs old. You're regularly confusing target API with min API. Those are very different things. And compiled SDK version is yet something else.

And the rest of your rant is about developers updating their app needing to track changes. That's an unrelated discussion. We're talking about backwards compatibility for apps that don't update. Meaning compile SDK & target API aren't changing. Android keeps those working quite well.

> So even design language changes can (and often do) break the UI.

Nonsense. The platform widgets are governed by the theme which almost never changes. Old apps that don't get updated will still be depending on Theme.Holo which is still there, still unchanging.

And most apps are primarily not using platform widgets anyway, they're using Jetpack instead. Things like RecyclerView, MotionLayout, ConstraintLayout, CardView, etc.. are not in the platform, they're in jetpack. Which is a static library bundled into the APK. And therefore obviously not changing with an OS update

I'm something of a career Android guy so I can do better than point out how you're wrong, I can explain how you're wrong too :)

> Android Studio defaults to targeting the latest API level

Whoops, you confused compileSdkVersion, targetSdkVersion, and minimumSdkVersion (I even mention targetSdkVersion separately as "compiled against")

You (wrongly) thought I was talking about targetSdkVersion (which often doesn't match compileSdkVersion on new releases because... targetSdkVersion lets you duck those breaking changes! That's why Google usually gives about a year before any given compileSdkVersion requirement [latest release] is graduated to a targetSdkVersion requirement).

I mean, read what I wrote again: "targeting a minimum SDK version of API 25".

Min SDK is still defaulted to selecting Lollipop in AS Dolphin which hit full release just this month, and when you ask AS for help it specifically highlights API 25. Try it yourself :)


> And the rest of your rant is about developers updating their app needing to track changes. That's an unrelated discussion. We're talking about backwards compatibility for apps that don't update. Meaning compile SDK & target API aren't changing. Android keeps those working quite well.

You didn't understand the conversation so you failed to follow the connection. I'm saying that Android doesn't do backwards compatibility well, but it doesn't need to since everyone is actively going out of their way to support old versions.

That's why any moderately long lived Android codebase will be full of:

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.XXX) useOldPermission/Flag/Type else useNewPermission/Flag/Type

> Nonsense. The platform widgets are governed by the theme which almost never changes. Old apps that don't get updated will still be depending on Theme.Holo which is still there, still unchanging.

Forget OS version, the theme changes per device manufacturer! There've been cases of manufacturers redefining constants like android.R.color.black to be something completely different than black! That's why you can't rely on android.R for anything

> And most apps are primarily not using platform widgets anyway, they're using Jetpack instead. Things like RecyclerView, MotionLayout, ConstraintLayout, CardView, etc.. are not in the platform, they're in jetpack.

You failed to realize that the part of Jetpack they're using the most is a library literally called AppCompat. You know why it's called that?

It try (and sometimes fail) to wrap dozens of versions worth of incompatibilities, bugs, breaking changes, and missing implementations in the user space (app)!

It's a literal ode to my entire point, that Android *the OS* has never done backwards compatibility well, so developers *in userspace* end up putting a *ton* off work into glossing it over!

Imagine if on iOS you couldn't just make a UITextView, you had to use a wrapper around a UITextView because iOS 8 was still a valid target for many apps and any new feature for the UITextView in the OS had to be simultaneously back ported to iOS 8...

Just the other day I observed a case where the Jetpack Cardview worked on API 32 but broke on API 25 because on API 32 the library falls back to the built-in from the OS... Well it turns out the AppCompat implementation just randomly breaks if the radius of a corner is greater than View.height / 2. The OS implementation handles that gracefully.

That's why any half-serious development team ends up needing to run their apps on all those versions of Android (or just unwittingly lives the life of one of those randomly poorly behaving apps in the wild)


Like if you just slow your roll and read what I said, it shouldn't be controversial to any serious Android developer.

You said it yourself... Android has reached the point where basic UI development is literally built on a ball of strategies to work around the atrocious backwards compatibility situation via AppCompat and other libraries in Jetpack... do you realize that's not normal or indicative of an OS with good backwards compatibility? It's an utterly bizarre corner that Google backed themselves into and are aching for outs on

> I'm saying that Android doesn't do backwards compatibility well, but it doesn't need to since everyone is actively going out of their way to support old versions.

Which is not relevant to the discussion. The discussion is about apps that aren't updated. Android doesn't break those. Developers targeting previous releases is a wholly unrelated issue.

> AppCompat. You know why it's called that?

Because it provides compatibility for new APIs on older platforms. You seem to be completely lost on what backwards compatibile means.

It means old binaries/APIs work on new releases. Which it does on Android quite well. It does not mean that new APIs magically work on old releases. Which is the problem jetpack/appcompat is targeting, and is the entirety of your rant.

> android.R.color.black to be something completely different than black! That's why you can't rely on android.R for anything

And yet everyone does rely on android.R, because those color redefine issues are vanishingly rare.

There's also entire CTS suites that verify oems don't modify theme.holo and theme.material. which is why everyone inherits from those, and don't have problems with it

> Which is not relevant to the discussion. The discussion is about apps that aren't updated. Android doesn't break those. Developers targeting previous releases is a wholly unrelated issue.

No, it is the discussion. If you rely on the OS and lean into new features, many updates will break your app, and in fact many devices on the same OS version will break it!

But there's a massive reluctance to rely on anything but the lowest common denominator of functionality in your apps, so there is outwardly an impression of backwards compatibility.

Seriously, you're talking about backwards compatibility when Android can barely do sideways compatibility, the mind boggles.

> And yet everyone does rely on android.R, because those color redefine issues are vanishingly rare.

Well I guess I'm referring to a certain level of experience... so just call it a code smell.

You get Lint warnings for not fully qualifying any use of it since it's usually not what you want.

Those of us who've needed to support enough apps realize it's really not that rare for random android.R overrides to exist in things like animations, and especially if you have a global footprint. So we just take 5 seconds to define ourpackage.R.color.black or ourpackage.R.anim.fade_in

> There's also entire CTS suites that verify oems don't modify theme.holo and theme.material. which is why everyone inherits from those, and don't have problems with it

No... they inherit from AppCompat wrappers on those because there are many subtle breaking changes between versions.


You're clearly not making the logical connection conveyed here, and I don't think there's anything wrong with accepting that.

No one will completely understand or agree with everything they read, but clearly others are able to make the link, and I think I've gone out of my way enough explaining the link, but you're completely stuck at the first step convinced that someone who's dealt with backwards compatibility for a very long time doesn't know what it is, instead of opening your mind to comprehending the bigger picture.

So I'll leave it at that :)

Preach brother. More people need to know about this insanity outside /r/mAndroidDev .

While you're right that Microsoft has a much better track record for backwards compatibility, this isn't just limited to mobile operating systems. See: Apple breaking x86, deprecating OpenGL, switching platforms, disabling drivers, etc.

Most things that actually break on Android break for good reason, in my opinion. I consider giving apps the ability to draw overlays without any kind of permission in a supposedly sandboxed environment a design flaw, for example. I'm more annoyed by their restrictions on dynamic loading but even that still seems to work fine if you avoid the Play Store and set your API targets to a version that triggers the backwards compatibility system.

The recent change to the way uploading to Google Play works (where you have Google sign your apps instead doing it yourself) is the worst change in years, but that's technically a Google change, not an Android change.

Almost all API changes necessary to run old apps are minute. The permission overhaul for Android 6 (interactive permissions for many permissions that were previously granted install-time) requires a few more lines of code to show an error if the user denies a critical permission but it doesn't require a full rewrite.

The consequence is that app devs with some minimal time on their hands can update their apps to work on modern devices quite easily. Only apps that are thrown over the wall and abandoned or created by defunct companies are now broken.

Luckily, decompiling most Android apps is quite trivial. If the app is indeed completely defunct with the company disappearing, one might relatively safely maintain a decompiled fork of the abandonware to make it work on modern platforms.

It's a pain, but not a dissimilar amount of pain to searching working on NoCD cracks for games that require DRM broken on Windows 10. Enthusiasts do it, quite regularly as it turns out!

This is also why I'm in favour of laws about source control for abandonware; if a company doesn't exist anymore and people still use their software, the source code should be opened so customers aren't screwed over. Source escrow isn't unheard of in B2B applications and I don't see why it can't be done for consumer apps.

Re: source code release, it seems like that could get rather messy for anyone using third party source code that isn't open.

Also, right away any sort of law like that would become incredibly nuanced. I have a pacemaker and there's an optional BLE app for it. Security through obscurity is lame, but I'd rather that app's source code not be dumped on the internet if the pacemaker company stops supporting it. The source code wouldn't keep the app going because it is dependent on the cloud, and would only be of value to folk trying to reverse engineer the diagnostic protocol. Personally that sounds like a lot of fun, but it's also hardwired into my heart...

On the other hand, something wired into your heart probably shouldn't rely on a proprietary app from a for-profit company that could drop support at any time...

It is what it is. As far as I know, there aren't any viable open source pacemakers. The existence of pacemakers is a wonderful modern marvel, and for-profit companies made it happen. It's likely $100 in materials, but my insurance probably paid at least $30K for the device and two leads. Considering how much engineering went into building it, I think it's a fair price. I once heard that there's roughly a 2:1 ratio between requirements and lines of code.

BLE strapped to one's heart sounds incredibly dystopic to me for some reason.

Source code for such products shouldn't be a problem because devices like these should definitely come with the necessary signature checks out of the box. Such key material shouldn't be released for anything important to one's health (medical devices, emergency devices, etc.). If you're at risk of having a heart attack when someone beats some kind of app code obfuscation then you're in for a bad time.

The cloud won't save you either. When the company disappears, their domain name expires eventually. Someone malicious could record and analyse API traffic and buy the domain to set up their own evil API server.

There are so many attack vectors for such a setup that I'm worried about how tech-ingrained such crucial devices are.

> I consider giving apps the ability to draw overlays without any kind of permission in a supposedly sandboxed environment a design flaw, for example.

FWIW, Gentle Alarm doesn’t do that :D

It’s not quite clear why giving the permission helps, but without it, the alarm doesn’t go off when the screen is off (it still does if the screen is on and the app in the background).

I think it does, but not in the way you expect it. For the longest time this API was the only reliable way to get anything onto the lock screen of some Android phones. If it didn't use this API, you would need to unlock your phone before you could get to the snooze/dismiss buttons.

I can easily see the dev calling into the API to initialise the alarm dialogue before actually playing the sound (after all, who wants an alarm they can't turn off?) and the API call that used to go through without issue now throwing an exception, killing the wakeup process dead in its tracks. I haven't reversed the APK, or course, but it's a very likely scenario.

I don't think this is how the API is intended to be used, but you can find plenty of stackoverflow answers like https://stackoverflow.com/questions/35327328/android-overlay... that (ab)use the window API. This one: https://stackoverflow.com/questions/46860993/window-over-oth... shows the API change, throwing exceptions on API level 26 (Android 8) that didn't get thrown before.

On modern devices, these old workarounds are usually no longer necessary. There's a separate API which is easier to use for interacting with the lock screen now and the permissions to take over the lock screen have been made harsher. For a maintained app this would mean fewer compatibility hacks, less weird lingering visual artifacts in edge cases and a better alarm. Sadly, with the dev ending their operation, the alarm is probably stuck calling old APIs with limited support now that Android security is tightening. Such a change broke clipboard sync in KDE Connect, for example, except it didn't leave a compatibility workaround for old apps.

I'm in favour of better privacy and security but restricting previously unrestricted APIs is a painful process. I've gone so far as to root my phone to provide the necessary permissions but I don't think I'll be able to do that forever...

Oh, that explanation makes perfect sense, thank you for the background information :)

But then in the case of Windows we complain that Windows releases in 2022 still have stuff that looks like Windows releases from 2002. Maintaining that backwards compatibility can become a massive pain too.

I've seen several remnants of Android 2.1 or below in my Android phone in settings and dialogues that don't need much maintenance. They're reskinned easier, but they are there!

Those tend to be the most usable parts of recent Windows releases. It comes as a relief to encounter them.

Probably you should decompile it, instead of starting from scratch.

This is made easier because apps are distributed in bytecode form, and compiled to machine code on installation. The bytecode is retained in the .apk file, IIUC.

I use starfield as screensaver since pretty much first android phones, just keep old APK from few years and it still works

What's so special about that app? So that we know

> When setting the alarm, by default I want to have the option to set it in hours/minutes from now. So instead of setting the alarm for 04:30, I want to tell it to set it in 6h30m from now.

> I need it to allow me to set a time of X minutes during which it gets louder until it reaches the pre-set volume.

> pre-alarm: It’s an alarm that goes off X minutes before your main alarm, at a very low volume, with its own settings of volume, time to reach full volume, media to play, etc.

From my Reddit thread [0] back then. It was the upgrade to 10 that did it.

[0]: https://www.reddit.com/r/androidapps/comments/jvbsuz/looking...

This looks awesome. It's the perfect example of way too many features for a built in app, which is supposed to be used by the general public, but a wonderful fit for a very specific niche that should be addressed by third party.

I feel you and that's why we need more open source software.

Several of those features have subfeatures, and then there are more that I don’t use. It’s exactly my kind of app, one that does everything, and everything well ;)

My favorite Windows apps are the same (MediaMonkey, EmEditor, and DirectoryOpus), paid apps with tons of features, more than I need, but also all that I need.

For your first note, if I ask Siri to "Set an alarm in 6 hours" it does that (yes, I know you're talking about Android, but I assume Google Assistant does the same)

But the feature in Gentle Alarm works with all its other features, specifically alarm profiles that have different settings depending on what I need at the moment (sleeping alone, sleeping next to my wife, napping).

In addition, I’m already trying to get rid of Alexa, I really don’t want to add another voice assistant.

Google Assistant will set an alarm for ${currentTime} + 6 hours in your standard alarm app (can by Google's clock, can be your app of choice). Depending on your app of choice, this can have the effect of polluting your list of alarm presets with one-off alarms that you'd need to delete later.

Isn't setting an alarm a distance from now vs a set time called a "timer"?

I use alarms instead of timers for most things because of snooze. For stuff like laundry, this is critical to me because I often will turn off a timer's alert thinking "I'll get to this in a minute" but without snooze I forget until the next morning.

Technically, I guess? But it behaves exactly like an alarm (it is actually just the input, if it’s 13:00, and I set the alarm-timer for 6h, the app will set an alarm for 19:00, not a 6h countdown).

I have seen release notes about automated fixes Apple themselves for apps in the App Store that haven’t been updated recently.

The fact that this is a problem is probably a sign that every OS should do this from the start so that apps won't be written that...rely on this behavior.

It’s not really an OS problem: it’s the userland allocator that needs to implement this. So on Linux that would mostly be a glibc change to how malloc/free is implemented. The OS itself doesn’t know whether or not a processes memory page is logically „in use“. It only knows that once the process returns it (using for example munmap). But once you do that, any access crashes the process already.

I hope you won't call me stallman but in my brain libc should be considered part of the OS, but yes, it would be clearer if I said "libc's should do it."

Apps aren't being written _on purpose_ to rely on the allocator doing nothing with freed memory because that's not in its interface, and this change doesn't actually do anything to stop apps from reading freed memory.

There is no behavior that a program could legally rely on when reading freed memory. The assumption behind malloc/free is that freed allocations aren't used by the program anymore, so correct programs don't rely on reading freed memory at all and the allocator implementation is then free to do anything at all with the freed memory, it just turns out that the simplest behavior on the allocator side is to do nothing at all and just leave the freed memory as-is.

The only thing this does when speaking of programs reading freed memory is that it breaks existing incorrect programs that just happened to work because of an implementation detail of the allocator, and then later someone will write another incorrect program that just happens to work because the allocator now zeroes out freed memory. To actually stop the problem of programs reading freed memory, the OS side would have to enforce the use of memory-safe languages which by design cannot access unused memory.

To be very explicit, the "...rely" is me being facetious, I'm not really implying anyone actually relies on use after free! More like they lucked out on not catching it and shipped it anyway, hence the "rely" bit.

Writing random values to freed memory would mostly work, at some cost to performance.

The memory could get allocated again before it is re-used, and whatever is put in it might be less trappy than a random value. And, of course, a random value might not trap.

Better is not to do things that result in referring to dead memory: easy in modern C++, apparently difficult in C.

The article does not actually claim this is a problem, it states the author thinks it will probably be a problem.

I doubt this actually will be a problem for 99% of all apps, the ‘but what about this essential software from 50 years ago’ problem does not exist on Apple platforms because they routinely deprecate and then drop obsolete software without mercy.

The only real problem I can imagine occurring is that this change uncovers dangerous security vulnerabilities that are a crash (so a denial of service) on those platforms but an exploit on others. But that’s a problem for those other platforms to deal with.

It seems totally fine to me that software needing the performance of non-zeroed deallocations can implement its own pool itself for the critical path in the code that needs it.

Yeah, everyone complaining about zeroing taking time are ignoring that this should be a sane default, and for fucks sake if you need performance you shouldn't be constantly malloc'ing and free'ing on the heap in your critical path anyway. I guess that bias was built into me because I learned C from tutorials written in the 90s where they kept stressing heap is slow somehow because it was on a P3 I guess.

Allocators really were slow. I.e. it was easy to write a general-purpose allocator 10, 100, or (on SGI Irix!) 1000 times faster than what their libc did.

Nowadays it takes work to get more than 10x, but hardly any to get there.

I wonder how many projects will be oblivious because they use their own allocator (eg: we use a modified version of Doug Lea's allocator).

If the system allocator gets slower people might be more inclined to switch to something custom or use locally recycled allocations which may make use-after-free bugs even harder to find.

I'm sympathetic but I wonder if it would be more sensible as an option that's on by default instead.

There is apparently no performance degradation due to other improvements in malloc.

Why should the system allocator get slower?

Zeroing memory takes longer than doing nothing.

Though some chips optimize zeroing memory. For example Intel does, perhaps Apple does as well?


Apple uses dc zva on ARM.

Is there a reason they aren't making this change only for apps compiled with the latest SDK?

Yes. This protects your data against a malicious app. Your proposal gives a malicious app a workaround to avoid this.

Is there a reason you don’t want this on by default?

No it doesn't. Malicious apps aren't impacted by this at all. It's a hardening technique for non-malicious apps to help protect them against malicious inputs. Which any maintained, updated app would naturally just get when they update, and in the meantime there's no regression in security from previous releases.

It's only on-everywhere because Apple doesn't do backwards compatible. Update or get kicked out is the expectation.

Aren't both benefits?

E.g. a malicious app mallocs a large buffer and just reads the memory looking for anything interesting? AND as you pointed out a non-malicious app that doesn't handle input.

> It's only on-everywhere because Apple doesn't do backwards compatible.

If this causes issue in an app then doesn't it mean that app has a possible vulnerability? So, it can be argued that breaking an app is increasing the security of the ecosystem. And yes, I understand the other side too of having an obscure app that you depend upon breaking because it is no longer supported. That's also the type of app that doesn't have a high probability of being exploited.

> E.g. a malicious app mallocs a large buffer and just reads the memory looking for anything interesting?

No, the OS zeros pages before giving them to a new process, otherwise you'd have all kinds of information leaks across security boundaries.

You only see dirty memory within the same process when you malloc and then free and malloc again and get a page from the free list within the process. Increasingly allocators are zeroing those too though to reduce the attack surface from malicious inputs (which is what changed in iOS).

> If this causes issue in an app then doesn't it mean that app has a possible vulnerability?

Only if the app had any untrusted inputs in the first place. If the app doesn't ever touch the network or only connects to its own servers then no, it's not really improving security.

Does iOS use different libc dynamic libraries for applications compiled with different SDKs? I'm not an iOS developer, so I don't know, but I would assume there is only one libsystem_c.dylib[1] on iOS.

[1]: Or even 0, as in Big Sur, where it's provided inside the runtime linker(/loader), instead of being a separate file.

They don’t (usually) have different versions but they do have “linked on or after checks”. So one libsystem, but it can look at the sdk the software was built against and adjust its behavior accordingly. But I have no idea if they’re doing that here.

Usually no.

Security, probably. UAF attacks are going to be slightly harder to perform if the data you put there is zeroes now.

>UAF attacks are going to be slightly harder to perform

SOME attacks are going to be harder, others are going to be easier(at least that's what a Project Zero researcher thinks)


If they “worked until now” doesn’t that imply that the app ecosystem makes too little use of sanitizers like ASAN? Sanitizers will stomp on deallocated regions, and much much more.

That appears to be the effect of changes to calloc, though. The change to free is a security improvement.

On Windows, deallocated blocks get filled with 0xFEFEFEFE. For 32-bit programs that aren't large-address-aware, this would basically have the same effect. Trying to dereference a pointer of that value will be an access violation.

While we are talking about dynamic memory allocation and de-allocation, a useful trick at times is to wrap your mallocator with a bit of code so that you can program it to fail after, say, n allocations, or on a random basis. In a testing context, this can help you systematically explore the set of possible allocation failures to see if the code handles them as expected.

The vast majority of code cannot so this isn’t particularly useful.

It really depends on context. There are times when it's useful, and times when it's not. It's helped me at times in improving code coverage when developing software for medical devices, so I thought I'd mention it.

The technique is can also be generalized beyond just triggering malloc() failures under specified conditions.

Is zero better than 0xDEADBEEF or just faster?

Malloc doesn't guarantee zero filling memory, but ObjC and Swift currently need to zero memory immediately after malloc returns it, and they both share C's malloc pool.

This makes a lot more performance sense for those languages.

One advantage is that when you allocate the memory back out (either by malloc, or calloc which requires zeroing), you don't have to do anything.

It is mentioned that if you write to freed memory, that could lead to later callocs not returning zeroed memory, which shows they are relying on this.

Yeah, if performance is the goal, zero is certainly faster. I have used 0xDEADBEEF to find both free-after-use and use-before-initialize bugs.

The advantage of 0xDEADBEEF would be that it reduces the chance of software starting to rely on that value, compared to zero, isn't it?

Better for memory compression, at least.

Less likely to cause crashes, probably.

I have used custom malloc()/free() implementations that do both just so I could find more crashes.

I know the apple documents note this for the release. But we spent a bit today at work trying to do it, and we couldn't build a test case. Has anyone actually tested this? The kernel already has their own tagging system which poisons things, so I assume this is a usermode thing.

Make an allocation that’s, say, 100 bytes. You’ll see it go through platform_bzero on the free path.

Awesome! So that did work. But when trying to do 1024 bytes it doesn't zero it out. Do you happen to know what the cutoff is? I think my coworker is gonna open it up in ida, so will update back if we figure it out.

EDIT: We seem to only get the zero behavior ?? < 100 < ZEROMEM <= 1008. So malloc 1009 isn't zeroing for us. And some value below 100 isn't freeing and just poisons

Yeah, some of the sizes don't do it. I think maybe 32 on the low end, and 1016 on the high one?

Sounds similar to what jemalloc (default libc allocator in FreeBSD) calls “junk filling”.

A lock-free datastructure for free blocks and a lazy zero-initializer (or pattern-based-initializer) process may address the perceived performance impacts to a large extent.

I thought accessing deallocated memory would crash an app anyway?

You can think of two levels of deallocated memory: deallocated just inside the app, and fully deallocated towards the OS. Only the fully deallocated towards the OS will crash.

When you deallocate small amounts of memory, the underlying C library allocator will keep the deallocated memory around and not return it to the OS in case you need a similar sized piece of memory soon.

Not correct.

It is very rare for memory ever to be freed to the OS before process exit. Malloc never does.

And, zeroing the memory, if code later uses it like a pointer, that will be null and the use will trap.

You’re the one who’s not correct. malloc is more than willing to hand memory back to the OS when feasible, most implementations will unmap the memory or at least mark it as unneeded.

I am corrected. Eek.

That's true for memory which has been unmapped from the process (as long as something else isn't mapped to the same virtual address range). But different implementations of C malloc()+calloc()+free() don't necessarily unmap memory when you call free(). Instead they mark it as something that may be returned from subsequent calls to malloc()/calloc(), i.e. the memory isn't returned to the OS.

None of them unmap freed memory.

That depends on the specific implementation. glibc does unmap memory[1]. Not in every free() call, but still. There are also implementations which unmap in every free(), which are used to quickly detect bugs. Honestly, I wouldn't want to use an implementation which never unmaps memory. I've already been irritated by Go's reluctance to return memory to the OS in the past. I'm certainly glad it's less of a problem in C. (Although I've read some reports of glibc being a bit suboptimal here in multithreaded programs, compared to allocators like tcmalloc or jemalloc.)

[1]: <https://github.com/bminor/glibc/blob/a364a3a7090b82ddd30e920...>

It won't do that by default. You need to set some non-portable flags to get it to munmap things.

Unmapping trashes page-map caches of other threads in the process. That involves inter-core interrupts. Not pretty.

Example program:

  #include <stdlib.h>

  int main() {
   for (size_t i = 0; i < 4096*4096; i++) {
    void* p = malloc(i * 4096);
Running strace on it gives me a whole spam of logs like:

  mmap(NULL, 139264, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0e7d46f000
  munmap(0x7f0e7d46f000, 139264)          = 0
  mmap(NULL, 143360, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0e7d46e000
  munmap(0x7f0e7d46e000, 143360)          = 0
  mmap(NULL, 147456, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0e7d46d000
  munmap(0x7f0e7d46d000, 147456)          = 0
  mmap(NULL, 151552, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0e7d46c000
  munmap(0x7f0e7d46c000, 151552)          = 0
  mmap(NULL, 155648, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0e7d46b000
  munmap(0x7f0e7d46b000, 155648)          = 0
  mmap(NULL, 159744, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0e7d46a000
  munmap(0x7f0e7d46a000, 159744)          = 0
That was run under a standard Debian 11 installation.

Eek. I am corrected.

Now I need to find the nonstandard flags to set to turn it off.

I see, I rarely use lower level stuff like this, so don't encounter it much, but, when I have to, I would get deallocation errors when dealing with things like metal buffers and just assumed the system always took care to protect the memory.

It can be a bit mysterious until you look at how it works.

Most CPUs provide memory permissions with granularity of pages, which are usually 4 KB in size. The OS manages these by modifying a "page table" which the CPU uses. Changing the page table is not free, so the userland allocator (malloc/free) will request large blocks of memory from the OS (with mmap) (or sbrk, if you're a dinosaur) and then divide them up into smaller blocks for your program. They typically remain accessible until the entire original block is freed.

If you allocate a large enough block of memory, it will typically just be backed by one region of pages made with mmap. Then, when you free it, the allocator will munmap it. You can test this out by allocating a large block of memory, freeing it, and then dereferencing it.

Your system might do this for 1 MiB allocations, for example... change the allocation size and see whether it still crashes.

  #include <stdio.h>
  #include <stdlib.h>
  int main(int argc, char **argv) {
      volatile int *ptr = malloc(1024 * 1024);
      puts("*ptr = 1");
      *ptr = 1;
      puts("*ptr = 2");
      *ptr = 2;

Pages are 16KB on iOS (and AS Macs). They're 4KB everywhere else on the world, which is too small these days, although there's things like "superpages" to help with that.

> They're 4KB everywhere else on the world

They're 64KB on RHEL/CentOS 8 on 64-bit ARM, see for instance https://git.centos.org/rpms/kernel/blob/c8/f/SOURCES/kernel-... which is AFAIK the kernel configuration for CentOS 8 (search for CONFIG_ARM64_64K_PAGES=y).

Malloc will not unmap memory. Period. That would have huge performance cost in multi-thread programs.

This is factually false, and I'm wondering why you'd say that.

Some of the more common malloc implementations out there are dlmalloc, jemalloc, and tcmalloc. Here is where they call munmap.



Note that not all allocators work this way, you can see that tcmalloc will instead MADV_DONTNEED. (You can find munmap in tcmalloc, but it's not being used to free memory.)


I am corrected. Now I need to start turning on nonstandard flags to disable it.

That depends on the applications CRT and compiler and how they are setting up their memory regions.

Some CRT's just go get their own block of memory then malloc out of that. Some use the OS extensively to minimize that sort of thing. It just depends.

I like the German grammar of capitalizing all nouns

Would that be useful here even in a computer context?

Is there any way of measuring how many apps were affected by this change?

Apple crash reports will measure this (if you compare old reports to new ones), but the data isn't public. I assume some third party SDKs will get updates to fix their bugs (and changelog might reveal it) but otherwise it is hard for normal user to see what issue caused an app to crash.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact