Poll

Do you ever code to free() NULL pointers?

Sure, why not!
14 (53.8%)
Hell no, only free allocated memory
12 (46.2%)
I only use a memory safe language
0 (0%)

Total Members Voted: 26

Voting closed: May 17, 2020, 10:52:59 pm

Author Topic: Poll: Freeing NULL Pointers  (Read 801 times)

0 Members and 1 Guest are viewing this topic.

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 5324
  • Country: fr
Re: Poll: Freeing NULL Pointers
« Reply #25 on: May 14, 2020, 04:16:07 pm »
andersm:
Though it is a bit off-topic, this is a good point. And, unfortunately, there is still no portable solution. What is employed now is a bunch of platform-specific solutions (like FreeBSD’s explicit_bzero, hoping that volatile pointers will in fact cause overwrite etc.). Even those are still not fully effective.

Though, per the standard itself, there is no absolute guarantee indeed, I've never run into any platform on which the piece of code I posted above would not work and would get pruned.
The volatile qualifier ensures the compiler can''t assume the assignment has no effect, so in practice, it won't prune it, because pruning it would violate the "side-effect" rule.

What makes the complete end-result implementation-defined, though, is the following statement:
Quote
What constitutes an access to an object that has volatile-qualified type is implementation-defined.

So, yeah. IME it's safe to assume it will do what you want, but there is no strict guarantee it will be portable.
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 1705
  • Country: fi
    • My home page and email address
Re: Poll: Freeing NULL Pointers
« Reply #26 on: May 14, 2020, 05:11:02 pm »
I have personally never run into cases of malloc() that would NOT return NULL in case of failure to allocate the requested memory.
Dealing occasionally with huge data sets, I've encountered this.

Technically, the C library has allocated the requested memory, and the OS kernel has provided the virtual memory (address space) for it.  (Note that I'll assume POSIXy system for the following.  Non-POSIXy systems can be affected by this as well, but probably don't provide the POSIX C functions I'll mention.)

The problem only occurs when at the moment of use, the kernel finds that it cannot provide a RAM backing to the virtual addresses it has already provided.  (It will try swap and reusing clean pages first; when this happens, you really do have too many applications using too much RAM already.)

So, the overcommit memory issue has nothing to do with C or memory allocation, and can happen to any process (that has not "locked" its pages to memory via mlock() or mlockall()).  It is a failure of the kernel to provide actual random access memory to back up the virtual memory/address space it has already provided to the process.

Accessing the memory immediately only makes sure the RAM backing exists at that point, with the belief that the kernel cannot rescind that allocation later on.
If allowed (by the resource limits set), a process can lock pages in memory via mlock()/mlockall(), ensuring that accessing those pages will never cause I/O (paging).
In Linux, it is possible to catch SIGBUS and SIGSEGV, and either emulate the failed access (requires an instruction decoder, unfortunately, and preferably one that does not need dynamic memory, and can operate from read-only memory), or do a longjmp()-like jump cancelling the entire access attempt.  The latter is easier, but it is rather arcane, and definitely hardware-specific.

One workaround is to skip malloc() et al., and instead memory-map an already allocated file without swap reservation.  (By allocated file, i mean that you either write each block of the file, or use posix_fallocate() to ensure each block of the file has been allocated on storage, without holes.)  If there is sufficient RAM, the mapping will stay in page cache.  The upside is that this file can also be used for checkpointing a long-running simulation; allowing it to continue even if it gets terminated early.

One can use mincore() to examine if the memory is present, and attempt to entice the kernel to provide the RAM backing via madvise().  Unfortunately, madvise() is advisory, and doesn't yet have a 'make sure backing exists, or fail now' option.

The proper management function is mlock()/mlock2()mlockall().  Calling these, one can ensure the required page(s) are in RAM.

Under 4.9 and later Linux kernels, the RLIMIT_MEMLOCK soft resource limit dictates how much memory a process can lock in RAM.  In 2.6.8 and earlier either superuser privileges or CAP_IPC_LOCK capability was needed; now, only the resource limit matters.

It is also important to notice that GNU C library doesn't return typical allocations back to the OS, but keeps it in the process heap for future allocations – unless the allocation exceeds a certain limit (130 MiB, IIRC).

If you have lots of RAM on your machine, feel free to turn overcommit off.  The setting is exposed by the kernel as the pseudofile /proc/sys/vm/overcommit_memory,  0 = heuristic guess, 1 = allow, 2 = never allow.  This is system wide.  Different Linux distros usually already have something in their init system where you can tune this knob.  The /proc/sys/vm/ pseudofiles described in man 5 proc can be informative.

It is important to note that this isn't just some zealot knob that Linuxies decided they wanted.  It turns out that a lot of userspace processes allocate lots of memory, but use only a fraction of it.  Per-thread stacks (8 MiB by default) are an excellent example; typical worker threads use maybe a couple of dozen KiB, but have megabytes allocated.  By allowing overcommit, those allocated but unused pages are nearly free (cost is page table setup etc), so the user can get full use of their RAM.

We didn't use to have gigabytes of RAM, and overcommit made a big difference in performance.  There are quite a few tunable knobs, too, if you had a workload that benefits from such tuning.  On a desktop system, turning off overcommit and setting reasonable per-process limits especially for memory-hungry applications (like browsers! And Java!) probably works better if you have say 16 GiB of RAM...  It's just that the Linux distributions span such a huge range of hardware, that the defaults won't be optimal for anyone.
« Last Edit: May 14, 2020, 05:13:26 pm by Nominal Animal »
 

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 5324
  • Country: fr
Re: Poll: Freeing NULL Pointers
« Reply #27 on: May 14, 2020, 05:16:26 pm »
We didn't use to have gigabytes of RAM, and overcommit made a big difference in performance.

Yes, yes, I understand the rationale and the benefits.
I've just been lucky (probably) never to be bitten by it.
And, I still think this is problematic as far as application robustness is concerned. That was certainly a trade-off between robustness and performance.

But for the developer, as we said, this is a bit nasty.
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 1705
  • Country: fi
    • My home page and email address
Re: Poll: Freeing NULL Pointers
« Reply #28 on: May 14, 2020, 05:31:37 pm »
(I'd love to have a Ryzen Threadripper, with say 64 GiB of RAM, and a couple of 512 GiB Samsung M.2 SSDs in RAID-0, for HPC.  Even Dalton would run fast on such a machine, not to mention my own simulator I've been working on...
 But, I'm just a poor burned out husk of a man; can't work enough to afford one.  :-//)

Yes, yes, I understand the rationale and the benefits.
I've just been lucky (probably) never to be bitten by it.
And, I still think this is problematic as far as application robustness is concerned. That was certainly a trade-off between robustness and performance.
I fully agree.  Sorry, I didn't intend to sound repetitive or anything; I just wanted to describe the problem to those others who might be reading the thread, as it is one I am familiar with.

Yes, in general, it Just Works.  When a desktop application runs amok (starts gobbling memory like crazy; it happens), it tends to crash before OOM killer is invoked.  (And while the OOM killer is a bit of a shotgun approach, it is becoming better and better.  Right now, we can even freely assign the "OOM scores", so that the order in which OOM killer targets processes is deterministic; this is excellent for locked down and embedded systems.

Desktop applications should not care at all.  It's up to the user to manage how they use their tools, really, since the defaults do a pretty good job for most use cases.

Most service daemons shouldn't care either.  A multiprocess server with a privileged core and unprivileged/lesser-privileged worker processes can use resource limits (set in global configuration; AFAICR both Apache and Nginx already support this) and perhaps even tweak OOM scores, so that the worker processes will be killed before the core or any other services, so even an OOM will be perfectly survivable without outages.

The painful bit is with security-sensitive applications, like those maintaining keyrings.  I REALLY don't like having a keyring in the browser process!   It would be best to store the keys either in the kernel (both Linux and Mac kernels have keyring facilities), or in locked memory inside a paranoid keyservice per-user/per-session daemon.
 

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 5324
  • Country: fr
Re: Poll: Freeing NULL Pointers
« Reply #29 on: May 14, 2020, 05:49:28 pm »
Yes, certainly, in *most* cases, it's adequate. But in some cases it can be a hazard.
And, if your particular application requires a lot of memory to operate, it can be a real problem. I guess there probably are convoluted (and/or non-portable) ways of programmatically figuring out how much memory you can reasonably allocate and go from there, but it's unconvenient to say the least.
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 1705
  • Country: fi
    • My home page and email address
Re: Poll: Freeing NULL Pointers
« Reply #30 on: May 14, 2020, 06:43:34 pm »
I guess there probably are convoluted (and/or non-portable) ways of programmatically figuring out how much memory you can reasonably allocate and go from there, but it's unconvenient to say the least.
unc.. inc.. noncon.. Definitely not convenient, agreed.

My point is, I guess, that the application should not make that kind of determinations, and instead only allocate what it actually needs.  I similarly dislike apps that try to sniff how many CPU cores/threads I have, because depending on my own workflow, the logical priority of each application varies a lot.  (I do use ionice and nice in real life, often, when running long-running but low-priority stuff, while I do something more interesting on the desktop.)

Apps' own run-time tunables also help.  For example, when dealing with massive I/O, the I/O block size starts to matter.  No, fstat() et al. doesn't really tell the optimum I/O block size, because the optimum I/O block size depends on the workload.  So, I like to use a compile-time default, with a easy user configuration override, say via environment variables, if you don't want to do configs.  Or in the UI, for GUI apps.   A single one, sliding from "background idle" to "max performance" usually suffices, with different steps' exact scales configured at compile time.

It is interesting to note that old Mac OS, pre-X, had per-application memory limits set in the executable preferences (easily edited by the user).  If one wanted to edit a huge image, or one with many layers in Photoshop, kick that to 75% of installed RAM but don't start any other applications.  Then, when doing e.g. publishing-type stuff, editing images destined to be laid out in Pagemaker or Illustrator or Freehand, drop the limits so one can keep an instance of each open at the same time...
That was over two decades ago.

This is not a new problem.  I kinda like the old Mac OS approach;  I did a lot of work with a PowerMac 7200 with 32 MiB of RAM, and the different workloads did need adjusting the limits to squeeze the max out of the hardware.
« Last Edit: May 14, 2020, 06:47:28 pm by Nominal Animal »
 

Offline dunkemhigh

  • Super Contributor
  • ***
  • Posts: 1904
Re: Poll: Freeing NULL Pointers
« Reply #31 on: May 14, 2020, 07:10:25 pm »
Quote
The proper management function is mlock()/mlock2()mlockall().

That's kind of different, then, isn't it? Being aware of that and not using those before accessing the memory is pretty stupid, ISTM. On that basis, I think that disqualifies this example from the discussion of whether NULL is meaningful or not in a pointer context.
 

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 5324
  • Country: fr
Re: Poll: Freeing NULL Pointers
« Reply #32 on: May 14, 2020, 08:06:46 pm »
I guess there probably are convoluted (and/or non-portable) ways of programmatically figuring out how much memory you can reasonably allocate and go from there, but it's unconvenient to say the least.
unc.. inc.. noncon.. Definitely not convenient, agreed.

Inconvenient, I think. Sorry for the slip-up.

My point is, I guess, that the application should not make that kind of determinations, and instead only allocate what it actually needs.

That's a very general statement, but doesn't fit all needs.
In some applications, the amount of memory needed may vary tremendously, and the app may decide how much to allocate at some point depending on how much is available. Whether you find that bad practice is debatable, but there certainly are cases in which it can be useful.
Sure there are other means of dealing with it, such as making that a preference for the application, and letting the user set it instead. Why not.

Whatever the approach, I still think returning pointers to allocated memory that could suddenly vanish for any reason is not sane. There are apparently ways to circumvent this (mlock() et al.?), but it's kind of quirky. I personally am against the approach that considers letting processes just crash instead of giving the developer an opportunity to handle things gracefully. YMMV. If anything, even if the developer doesn''t know what to do, they can always at least give the user a message about the app not having enough memory available to run. A lot more user-friendly than them having to debug core dumps. Anyway. As you mentioned, in some safety- or security-critical apps, that could be completely unacceptable.

As to the C standard, I've re-read the section about memory management in C99, and from my interpretation, the discussed behavior, to my humble eyes, looks non-conforming. Read it carefully, and tell me what you think.

Just my 2 cents.
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 1705
  • Country: fi
    • My home page and email address
Re: Poll: Freeing NULL Pointers
« Reply #33 on: May 15, 2020, 12:38:29 pm »
Inconvenient, I think.
Me fail English often, so I wouldn't know  :-[.

My point is, I guess, that the application should not make that kind of determinations, and instead only allocate what it actually needs.
That's a very general statement, but doesn't fit all needs.
I meant that for userspace applications (category GUI-based tools, as opposed to services) exclusively.  For services and applications dealing with massive datasets, I mentioned other options in a prior message.

I personally am against the approach that considers letting processes just crash instead of giving the developer an opportunity to handle things gracefully. YMMV. If anything, even if the developer doesn''t know what to do, they can always at least give the user a message about the app not having enough memory available to run. A lot more user-friendly than them having to debug core dumps. Anyway. As you mentioned, in some safety- or security-critical apps, that could be completely unacceptable.
I myself am ambivalent about this.  I know why it is done, and that it works for many users, but I have been bitten by it in practice.  I do not know if banning overcommit makes sense.  I do support users and admins who disable it on their own systems, and believe that doing so on machines with plenty of RAM is a good idea for several different reasons; but as a global default or non-user-tunable, I am not sure.

And by "not sure", I mean it literally: I am on the fence on this.  I never use "not sure" as a weaselly "I disagree". 

As to the C standard, I've re-read the section about memory management in C99, and from my interpretation, the discussed behavior, to my humble eyes, looks non-conforming. Read it carefully, and tell me what you think.
It is really the OS kernel (other OSes besides Linux do this; I recall seeing this first on an Unix box) that fails to uphold its promise.
In other words, I believe you can say that these kernels make it impossible for any implementation on them to fulfill the C standard.
I do not blame the C compiler or standard library though, because it is the kernel that is failing in its promise of service, not anything in C; all processes, even those written in raw assembly, are affected by this.

Quote
The proper management function is mlock()/mlock2()mlockall().
That's kind of different, then, isn't it? Being aware of that and not using those before accessing the memory is pretty stupid, ISTM. On that basis, I think that disqualifies this example from the discussion of whether NULL is meaningful or not in a pointer context.
Me fail English, so let me re-state my position.  Apologies for the wall of text, but I am trying to write in a manner that lets even new C programmers to follow the discussion.

Typical userspace applications, both GUI and non-GUI should not care about overcommit or work around it.  If an application can benefit from preallocating memory, or switch between using very little memory but being slow and using a lot of memory to speed up things, I think it should have an easy user tunable to determine that – combining it into a single sliding scale value, from "minimal memory" to "maximum performance", with a bit of tuning how that maps to different details available at compile/build time for different HW arches.  The only applications that should ever need to use mlock() are security-sensitive services, and perhaps some very important low-level services (that'd use mlockall() if the admin so desires, based on some config setting) like remote access services, or critical network infrastructure services.

The problem is that from the userspace perspective, including what the standard C library sees, the interfaces used to request new memory from the OS kernel, and thus passed to the malloc() caller, have fulfilled the promise.  It is just that these kernels have learned to LIE because some human users WANT/NEED THAT, and simply kill the target process if they catch the kernel in that lie (having promised memory, but not being able to fulfill that promise).

The file-backed mmap() approach can be used to sidestep the entire issue.  This is useful for simulators and other programs dealing with large and/or persistent datasets, and it can provide restart/continue support too in case the process keels over for any reason.

The mlock()/mlockall() approach can be used to enforce the kernel promise.  On current Linux, Android, Mac, iOS, and various *BSD systems, the RLIMIT_MEMLOCK resource limit (managed by the system administrator and/or user running the process) defines this limit, per-process.

It is possible for a Linux process to detect and catch this via SIGBUS/SIGSEGV signal handlers, in a per-thread manner, but as it involves manipulating the processor and thread state (particularly including the instruction pointer when the un-fulfillable memory access occurs), it is too complicated to do for standard programs.  I would only consider this approach if the process can safely undo/ignore the failure, and throw away (return back to the kernel) large-ish swathes of memory if that ever occurs; an "opportunistic memory-hog that nevertheless behaves nicely".  The issue then is that if the kernel runs out of memory after providing the RAM backing to such a nicely behaved memory-hog, it can detect the situation when trying to fulfill the promise it made to some other process.  There is no way – and I do not think C even provides any possible way! (POSIX signals could, I guess) – for the kernel to ask back memory, or even to inform processes about memory pressure.  So, even this SIGBUS/SIGSEGV handler way is really no solution at all.

It should be noted that this – memory provided by the kernel not really being there – ONLY ever happens when the system is overburdened already.



Okay, so we know there are lots of use cases, where we'd really like to know how much memory we should speculatively allocate, and still be reasonably sure (or completely sure) that the allocations will be fulfilled by the kernel.  And also things like how many threads we should use.

My opinion is that this is where we'd need a helper library.  Not to ensure the memory we allocate is there, but to give the resource use hints to the process.  Perhaps the library could also provide an interface to ensure memory allocations will be there if both the application requests it, and the library allows.
This library would be a policy manager.  It would be the extension of the human users'/admins' will, telling the application how to behave.  It should also have trivial tuning ability, something like "Application properties", and it could include CPU and I/O priority tuning.

Unfortunately, that is unlikely to happen at all.  It might be incorporated into some mutated form into systemd, so you can do a dbus query for these things, which basically defeats the entire idea, but that is unfortunately the direction current Linux distributions are "progressing".

(If anyone is interested, the entire Init system debacle and all related problems, including parallel and asynchronous service startup, would be trivial to solve, if system service developers would just add calls to a library that is responsible for providing the service status updates, and provides an interface to manage inter-service dependencies.  We've known that for decades, but that hasn't happened, because coordinating something like that among software developers is like herding cats: it won't work, and just leads to hissing (or bikeshedding, really).  It seems the best we can do, is cobble together a super-privileged superdaemon that forcibly inserts its tentacles into all over the userspace, rewriting many of those service daemons, with honestly speaking quite poor quality code, and dropping it in from above, using Linux distribution human politics.)

Similarly, it would be trivial for Linux kernel developers to provide a new syscall, say mpopulate(), that would take as a parameter a memory range, for the userspace to request the kernel to populate those pages, or return a failure code.  A flag parameter would tell the kernel how important the userspace process believes this memory to be, so the kernel can decide whether to try evicting other pages and do other make-more-ram-available stuff or not.

Problem is, most users do not care.  The users who care, can simply tune or turn off overcommit.  Many users never encounter this thing, and even if they do, they think the crash occurred for some other reason ("shit this is badly coded" is one).  Developers claiming they want this feature is not taken seriously, because their users are not demanding these features.  (And the users don't listen to the developers, because humans.  I'm a dev too, and I hate being between a stone and a hard place like this, so I fully understand the frustration others have expressed about overcommit issues.)

A number of years ago, I discussed with various compiler and C library developers whether we could provide a function, say memfill() or memrepeat(), that would repeat an initial part of the specified buffer into the entire buffer.  (It is essentially the third part of the memcpy() - memmove() - memrepeat() triangle; all three functions are implemented in a very similar fashion at the hardware level, and thus actually belong to compiler support rather than C library; i.e. to be available in freestanding environments also, not just in hosted environments.)  That failed, for the same reason, even though it alone changes the result of certain Fortran - C comparison benchmarks.  The main use of memrepeat() is to initialize arrays with structures or floating-point values, faster than copying those structures or floating-point values in a loop; because it deals with the storage representation alone, none of the value requirements (signaling NANs and such) apply.  It is particularly advantageous for odd-sized elements in a continous array.

In summary, whether one can trust malloc() to return NULL when at some point later on, the system cannot fulfill that promise anymore, is not really a C question, but a question about OS kernel services, and how to implement policy (providing human operator intent and wishes, and human administrator limits and overrides, to applications).
« Last Edit: May 15, 2020, 12:45:10 pm by Nominal Animal »
 

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 5324
  • Country: fr
Re: Poll: Freeing NULL Pointers
« Reply #34 on: May 15, 2020, 01:31:09 pm »
As to the C standard, I've re-read the section about memory management in C99, and from my interpretation, the discussed behavior, to my humble eyes, looks non-conforming. Read it carefully, and tell me what you think.
It is really the OS kernel (other OSes besides Linux do this; I recall seeing this first on an Unix box) that fails to uphold its promise.
In other words, I believe you can say that these kernels make it impossible for any implementation on them to fulfill the C standard.
I do not blame the C compiler or standard library though, because it is the kernel that is failing in its promise of service, not anything in C; all processes, even those written in raw assembly, are affected by this.

Oh, I agree! Although, I guess you could say that a fully conforming C std lib would implement malloc (and siblings) locking the allocated memory. But then it would defeat the whole purpose of overcommitting and would likely cause performance issues... My personal view on this is that it would have been more consistent to provide what it takes at the OS level so that malloc can be implemented with locked memory, thus making it default on the language level, and then provide additional memory management functions to allow allocating memory you can afford "losing" at some point. Apparently, the decision was to do the exact opposite. I'm sure we'd find many opinions on this anyway.

I'm more generally for functions that return a status the developer can rely on, so this one certainly tickles me.
« Last Edit: May 15, 2020, 01:34:08 pm by SiliconWizard »
 
The following users thanked this post: Nominal Animal


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf