APPENDIX III — C may have reached the boundary of what its own design principles permit it to become
Can the C language itself be changed so that the class of problems examined in this book can be prevented by the language?
There is an obvious objection to asking this question. C is not an unconstrained design exercise. It is an international programming-language standard with an established purpose, an enormous existing body of programs and implementations, and a committee with an explicit charter governing how the language is to evolve.[1]
The charter is therefore a useful place to begin. But an important distinction should be made at the outset: the charter is not itself the normative C Standard. It is a statement of the principles that WG14 uses to guide the evolution of the language. Those principles therefore constrain what the committee considers an appropriate evolution of C; they do not, by themselves, constitute technical requirements imposed by ISO/IEC 9899.
THE CHARACTER OF C
The current WG14 charter describes the maintenance and evolution of C as involving a number of competing objectives. Among its stated principles are upholding the character of the language, keeping the language small and simple, facilitating portability, allowing programming freedom, codifying existing practice, easing migration to newer editions, avoiding quiet changes, and enabling secure and functionally safe programming.[1]
These principles are not new inventions of the current charter. WG14's own comparison of the current charter with its predecessor shows that several of them are continuations of much older principles: keeping the spirit of C, keeping the language small and simple, trusting the programmer, not preventing the programmer from doing what needs to be done, treating existing code as important, and making migration of existing code bases a consideration.[2]
At the same time, the current charter explicitly recognises security and safety as legitimate objectives. It identifies memory safety, type safety and related properties as important to security and reliability, and calls for C to enable secure programming. It also discusses increasing the size of analyzable subsets of the language and addressing forms of undefined behaviour that can create significant security or reliability problems.[1]
These principles do not produce a simple answer.
They establish a tension.
The charter asks C to address security and memory safety while also asking C to remain recognisably C. It asks for improvement without abandoning the language's established character, and for stronger guarantees without simply discarding programming freedom, portability, compatibility and the existing programming model.
That tension is not incidental to the question of memory safety. It is the central question.
WHAT WOULD MEMORY SAFETY REQUIRE?
It is useful to distinguish between making C safer and making memory safety a language-level guarantee.
There are many techniques for detecting or mitigating memory errors in C without changing the language itself. Runtime instrumentation, static analysis, sanitizers, hardware capabilities and compiler transformations can all provide useful protection while retaining much of the existing C programming model.[3] [4]
A language-level guarantee is a stronger proposition. A language must have some defined semantic basis on which the validity of a memory operation can be established. In the literature, memory safety is commonly divided into at least spatial safety—preventing access outside the bounds of an object—and temporal safety—preventing access to an object outside its valid lifetime.[3] [5]
Consequently, a language attempting to guarantee memory safety needs some way of representing or enforcing relationships such as:
- which object a memory access concerns;
- which part of that object may be accessed;
- whether the object is still alive;
- whether a pointer or reference remains valid; and
- whether operations that copy, transform or derive pointers preserve those properties.
This does not mean that every one of these properties must be represented by a particular runtime data structure or programmer-visible annotation. Different systems use different mechanisms. Bounds metadata, ownership systems, capabilities, lifetime tracking, static proofs, contracts and runtime checks are all possible approaches.[3] [5]
The important point is narrower: if the language itself promises that an invalid memory access cannot occur, the language and its implementation must possess sufficient information and enforcement mechanisms to distinguish valid accesses from invalid ones.
C already has a substantial semantic model of objects, storage duration, arrays, pointers and access to objects. The difficulty is that those concepts do not amount to a single, comprehensive mechanism by which the validity of every memory access can be established automatically.
This is particularly visible with dynamically allocated objects.
A programmer may know that a pointer returned from an allocation represents the beginning of an array containing N objects, that the allocation remains valid until it is released, and that indexing must remain within that allocation.
The C programming model does not ordinarily require all of that information to travel with the pointer as an inseparable safety invariant.
The programmer maintains the relationship.
The language provides only part of it.
Research into C's memory model illustrates why these relationships can become technically subtle. WG14 discussions of pointer provenance, for example, have examined whether pointer values should carry semantic information identifying the allocation from which they originated, and how such information interacts with pointer arithmetic, integer conversions and object lifetime.[6]
That is important because it demonstrates that the problem is not merely the absence of a bounds check. The semantics of pointers themselves can become part of the safety mechanism.
THE NECESSARY CHANGE
If C were to provide a comprehensive language-level memory-safety guarantee, some of the relationships currently maintained primarily by programmer discipline would have to become enforceable properties of the language or its implementation.
That does not prescribe a particular solution. Bounds, ownership, lifetimes, capabilities, contracts, static analysis and runtime enforcement represent different points in the design space.
But it does establish a constraint.
The system must have enough information to distinguish an access that is valid from one that is not.
That immediately raises a question about the character of C.
A language with richer semantic relationships between values, objects, pointers and lifetimes may be capable of expressing stronger guarantees. But additional semantic relationships can also introduce additional rules that programmers, compilers, analysis tools and implementations must understand.
The WG14 charter explicitly places value on keeping C small and simple, addressing language issues with minimal new machinery where possible, preserving programming freedom and facilitating portability.[1]
There is therefore a genuine design trade-off.
This is not an argument that safety mechanisms are inherently undesirable. Nor is it proof that a particular mechanism would be incompatible with C.
The question is whether the machinery required to provide the desired guarantee can be introduced without violating the principles that WG14 has identified as important to the continued evolution of C.
THE EXISTING PROGRAM
There is another constraint, and it is potentially more difficult.
The history of the C charter places considerable importance on existing programs and on migration between language editions. The current charter retains that concern, while WG14's historical principles explicitly treated existing code as important and migration of existing code bases as an issue.[1] [2]
This matters because comprehensive memory safety cannot simply be declared into existence by changing the wording of a diagnostic.
Consider an existing program that constructs or manipulates a pointer in a way for which the language cannot establish the relevant bounds. Or a program that retains a pointer after the lifetime of the object to which it referred has ended. Or a program in which an integer value is used to calculate a memory extent without the language having a semantic relationship connecting that integer to the object being accessed.
A language that promises comprehensive memory safety cannot simultaneously treat every such operation as valid and guarantee that the resulting access is safe.
There are only a limited number of possibilities. The program can acquire additional information. The operation can be restricted. The implementation can insert checks or otherwise enforce the necessary property. Or the operation can fall outside the part of the language for which the guarantee applies.
This is not unique to C. Research attempting to retrofit full spatial and temporal memory safety onto C has repeatedly encountered trade-offs involving compatibility, metadata, runtime checks, source-code changes and the semantics of existing programs.[3] [4]
The important consequence is that a comprehensive guarantee necessarily has consequences for at least some existing programming practices.
That creates a direct tension with the charter's emphasis on existing code and practical migration.
SAFE AND UNSAFE CODE
There is, however, an important distinction that prevents the problem from becoming a simple choice between unrestricted C and an entirely different language.
A language can define a subset for which stronger safety guarantees apply while retaining operations outside that subset.
This approach is familiar in systems-language research. A program or operation can be classified as unsafe rather than being claimed to satisfy the same guarantees as the safe portion of the language.[5]
This distinction is particularly relevant to C because programming freedom is one of the principles explicitly identified by WG14.[1] [2]
C is deliberately capable of expressing machine-dependent and low-level operations. Its value as a systems programming language depends in part upon that freedom.
It would therefore be a very different proposition to say:
C must no longer permit this operation.
than to say:
This operation is outside the guarantees provided by the memory-safe subset.
The second approach preserves more of the existing programming model.
It does not, however, solve the entire problem.
The important question becomes whether the safe subset is sufficiently expressive to have practical value, and whether its boundary is sufficiently well-defined that unsafe operations cannot silently manufacture values which the safe portion of the language subsequently treats as satisfying safety invariants.
If an unsafe operation can freely produce a pointer that the safe portion of the language accepts as carrying valid bounds and lifetime information, then the safety boundary is not a meaningful semantic boundary.
A useful safe subset therefore needs more than a syntactic label. It needs a defined relationship between the operations permitted within the subset and the guarantees the language makes about them.
That boundary would itself become part of the language's semantics.
THE ESCAPE HATCH IS NOT THE SOLUTION
An unsafe subset therefore cannot simply be used to declare the memory-safety problem solved.
If every operation that makes safety difficult is placed outside the guarantee, then the language may preserve compatibility while providing only a limited memory-safety property.
The safe portion must cover enough ordinary programming that its guarantees have practical value.
At the same time, the boundary must be meaningful. The safe part cannot blindly trust information produced by the unsafe part if that information is supposed to establish a safety property.
This is a familiar problem in systems that combine safe and unsafe components: the interface between them becomes part of the safety argument.
The result is that an explicit safe/unsafe distinction is best understood as a possible architectural approach, not as a proof that C can acquire comprehensive memory safety without fundamental changes.
THE CHARTER MAKES THE QUESTION HARDER
The charter therefore produces an unusual situation.
It does not say that memory safety is outside the scope of C.
Quite the opposite.
Security, safety, analyzability and memory safety are explicitly within the concerns that WG14 has identified.[1]
But the same charter places constraints on how those objectives should be pursued.
C is expected to remain simple.
It is expected to preserve programming freedom.
It is expected to facilitate portability.
It is expected to take existing programs seriously.
It is expected to make migration practical.
It is expected to preserve the character of the language.
And the committee explicitly recognises that the various principles are not absolute: proposals may depart from them, but greater deviation requires greater justification.[1]
This is an important qualification.
The charter does not establish a rule saying that C can never become substantially safer.
Nor does it establish a numerical limit on the amount of complexity that may be added.
What it does establish is a framework within which such changes must be evaluated.
The fact that a feature could make C safer is therefore not, by itself, sufficient reason for that feature to belong in the C Standard.
The proposal must also be assessed against the other objectives governing the evolution of the language.
COULD C BECOME A MEMORY-SAFE LANGUAGE?
If by "C becoming memory safe" we mean that essentially all conforming C programs would receive a comprehensive guarantee against spatial and temporal memory errors, the compatibility problem becomes severe.
Existing C programs contain programming techniques whose safety cannot necessarily be established from the information currently associated with their pointers, objects and lifetimes. Research on retrofitting full spatial and temporal safety onto C demonstrates that achieving such guarantees requires substantial design choices concerning metadata, pointer semantics, object lifetime, instrumentation and compatibility.[3] [4] [6]
A change of that magnitude could require existing programs to acquire additional information, accept additional restrictions, or operate under a different semantic model.
That would create tension with several of WG14's established principles concerning existing code, migration, programming freedom and the character of C.[1] [2]
If, instead, "C becoming memory safe" means that C acquires a well-defined memory-safe subset while retaining an explicitly unsafe portion, the situation is more plausible.
Such an approach could preserve more programming freedom. It could allow programmers to write code under stronger guarantees where those guarantees are applicable, while retaining low-level techniques where they are genuinely required.
It is also consistent with the fact that WG14's current charter explicitly recognises subsetting as one possible way of addressing language concerns.[1]
But this approach has a cost of its own.
A genuinely useful safe subset must have sufficiently precise semantics to justify its guarantees. The language must somehow establish the relevant object, bounds and lifetime relationships, and must prevent those guarantees from being invalidated across the boundary into unsafe code.
The more comprehensive those guarantees become, the more significant the additional semantic machinery may become.
The question therefore cannot be answered simply by pointing to a mechanism used by another language and observing that C could theoretically adopt it.
The relevant question is whether that mechanism—or an equivalent mechanism—can be incorporated while preserving enough of C's existing programming model to remain recognisably C.
THE CHARACTER OF C
This may be the most important part of the question.
The C committee's charter does not describe C merely as a collection of syntax and library functions. It explicitly refers to the character of the language, including its existing semantics, idioms, habits and economy of expression.[1]
WG14's historical principles make the same point even more directly. The current charter's own mapping to earlier charters identifies "keep the spirit of C", "trust the programmer", "don't prevent the programmer from doing what needs to be done", and "keep the language small and simple" as longstanding principles.[2]
These principles help explain why C has remained useful for systems programming. The language exposes programmers to representation, memory and machine-level behaviour to an unusual degree, while remaining sufficiently general to be implemented across a wide range of systems.
That does not mean that every historical characteristic of C is immutable. The charter expressly allows the language to evolve.
It does mean that a proposal which replaces a substantial portion of C's programming model with a new semantic discipline carries a greater burden of justification than a proposal which merely adds a small, self-contained facility.
For example, a memory-safety system based on pervasive ownership rules, lifetime tracking, specialised pointer types or extensive static reasoning might provide strong guarantees.
The technical question would be whether such a system works.
But there would also be a second question:
Does a programming model requiring those mechanisms still constitute an evolution of C consistent with the principles by which WG14 maintains C?
That is a substantially harder question.
THE POSSIBILITY OF AN EVOLUTIONARY ANSWER
The charter leaves room for evolution.
It explicitly permits the committee to address deficiencies, recognises security and safety as legitimate concerns, allows subsetting, and states that its principles are not absolute.[1]
It would therefore be incorrect to conclude that the charter makes memory safety impossible.
It does not.
Nor does the charter require WG14 to preserve every historical property of C indefinitely.
What it does mean is that increasingly fundamental changes require increasingly strong justification.
A proposal for memory safety in C would therefore need to demonstrate more than the ability to prevent memory errors. It would need to show how the proposed mechanism interacts with C's existing object and pointer model, how existing programs would be affected, how interoperability would work, what restrictions programmers would face, how implementations would support it, and whether the resulting language remained sufficiently simple, portable and recognisably C.
That is a demanding standard.
SO CAN C DO IT?
There are really two different questions.
Can a language descended from C provide comprehensive memory safety?
There is no obvious theoretical reason why it could not. Research has demonstrated multiple ways of imposing spatial and temporal memory-safety properties on C-like languages and on C implementations themselves.[3] [4] [5]
Can the C Standard itself acquire comprehensive memory safety while remaining within the principles that govern its evolution?
That is much less certain.
The difficulty is not the invention of safe mechanisms.
The difficulty is satisfying the competing requirements simultaneously.
The stronger the guarantee, the more completely the language must specify and enforce the information on which that guarantee depends.
The more completely it specifies and enforces those relationships, the more constraints it may place on existing programs and programming techniques.
The more constraints it places on existing programs, the greater the potential compatibility and migration cost.
The more machinery it introduces to represent and enforce those constraints, the greater the pressure against the charter's requirements for simplicity and conceptual economy.
And the more the resulting programming model differs from traditional C, the stronger the argument becomes that the result is no longer merely an evolution of C but a new language sharing C's syntax, implementation heritage or ecosystem.
None of this proves that memory-safe C is impossible.
It establishes something narrower but, in my view, more important: the problem cannot be evaluated independently of the principles under which C is maintained.
THE QUESTION THE CHARTER LEAVES US WITH
The most interesting conclusion is therefore not that C cannot be made safe.
Nor is it that C simply needs to adopt mechanisms used by another language.
The real question is whether the principles that govern the evolution of C leave enough room for memory safety to become a language-level property without requiring changes so fundamental that the resulting language would no longer satisfy those principles.
The current charter explicitly says that C should enable secure and functionally safe programming. It simultaneously requires attention to simplicity, portability, programming freedom, existing code, migration and the established character of the language.[1] [2]
Those objectives are compatible up to a point.
The question is where that point lies.
If meaningful memory-safety guarantees can be achieved with a sufficiently small addition to the language while preserving the existing programming model, then the charter provides a basis for pursuing them.
If achieving those guarantees instead requires a fundamentally different treatment of pointers, object identity, ownership, lifetimes or memory access, substantial incompatibility with existing programs, or a programming model substantially more complex than traditional C, then the charter itself becomes part of the argument against incorporating the complete solution into C.
That would not mean that memory safety is unimportant.
It would mean that there may be a point beyond which achieving the desired property requires changing so much of the language's character that the resulting system should be regarded as something other than C.
That is the boundary this appendix is concerned with.
THE QUESTION WORTH ASKING
The most useful way to frame the problem may therefore be this.
The question is not:
Can we invent a memory-safe language that looks like C?
Of course we can.
Nor is it:
Can we add checks around C programs to make them safer?
We already know that we can. There is a substantial body of research demonstrating runtime instrumentation, static analysis, hardware assistance and compiler-based approaches for detecting or preventing memory errors in C.[3] [4] [7]
The question is:
Can the C programming language itself acquire meaningful memory-safety guarantees without ceasing to satisfy the principles under which the C Standard is maintained?
That question has a technical component, but it is not purely technical.
It requires us to consider what information the language must represent, what guarantees it can make, what restrictions those guarantees require, what happens to existing programs, and whether those restrictions are compatible with C's established character.
WG14 has explicitly accepted memory safety and secure programming as legitimate objectives. It has not, however, abandoned the other objectives that have historically defined the evolution of C.[1] [2]
That leaves an intriguing possibility.
Perhaps C can become substantially safer while remaining recognisably C.
Perhaps an explicit distinction between guaranteed-safe and programmer-responsible operations provides enough room to achieve useful language-level guarantees without abandoning C's low-level character.
Or perhaps the guarantees required to solve the problems examined in this book demand changes to the pointer, object, lifetime and programming model that are too fundamental for the C Standard to absorb.
If the latter is true, then the conclusion is not that C has failed.
It is that C's own design principles impose a boundary on what C can become.
And if comprehensive memory safety lies beyond that boundary, then the engineering question changes. Instead of asking how far C can be transformed, we must ask where C should stop—and where another language, a safer subset, or a different systems-programming model should begin.
References:
The current WG14 charter and its principles concerning the character of C, simplicity, portability, programming freedom, existing code, migration, secure programming, memory safety and analyzability.
https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3280.htm
Documents the relationship between the current charter and earlier C charter principles, including keeping the spirit of C, keeping the language small and simple, trusting the programmer, programming freedom, existing code and migration.
https://open-std.org/jtc1/sc22/wg14/www/docs/n3279.htm
Discusses spatial and temporal memory safety and the technical challenges of enforcing both properties for C programs.
https://doi.org/10.1002/spe.2105
Examines the design decisions, implementation choices and trade-offs involved in providing full spatial and temporal memory safety for C.
https://doi.org/10.1109/MSEC.2024.3363142
Provides a formal treatment of memory safety and discusses different notions of memory safety and the reasoning principles they support.
https://link.springer.com/chapter/10.1007/978-3-319-89722-6_4
Explores the semantics of C pointers, object identity, provenance, pointer arithmetic and the relationship between pointer representations and the objects from which pointers originate.
https://open-std.org/jtc1/sc22/wg14/www/docs/n2012.htm
Describes spatial and temporal memory-safety problems and discusses approaches for protecting against them in software and hardware.
https://www.cisa.gov/sites/default/files/2023-12/CSAC_TAC_Recommendations-Memory-Safety_Final_20231205_508.pdf