요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
BUG()와 BUG_ON()
deprecated.rst:32-52BUG 계열 대신 WARN 또는 WARN_ON을 사용하고 불가능하다고 생각한 error condition도 가능한 범위에서 정상적으로 복구한다. BUG가 kernel thread를 안전하게 죽이는 assert처럼 보이지만 실제로는 보유 lock 해제 순서, device state 복구와 resource 정리가 생략되어 system 전체를 불안정하게 만들 수 있다.
WARN 계열은 실행되지 않아야 하는 code path에만 사용한다. 도달 가능하지만 바람직하지 않은 입력이나 상태를 알릴 때는 pr_warn 계열을 쓴다. System owner가 panic_on_warn를 설정했다면 WARN 한 번도 panic으로 이어질 수 있기 때문이다.
Allocator 인자에서 직접 크기 계산하지 않기
deprecated.rst:54-110Allocator argument 안에서 count * size 같은 동적 계산을 직접 하면 integer overflow로 값이 wrap되어 요청보다 작은 buffer가 할당될 수 있다. 이후 정상 크기라고 믿고 쓰면 heap linear overflow가 된다.
/* 피해야 할 코드 */
foo = kmalloc(count * size, GFP_KERNEL);
/* 2-factor allocator */
foo = kmalloc_array(count, size, GFP_KERNEL);
/* 별도 2-factor API가 없을 때 */
bar = dma_alloc_coherent(dev, array_size(count, size), &dma, GFP_KERNEL);
kmalloc은 kmalloc_array, kzalloc은 kcalloc으로 바꿀 수 있다. Trailing array가 있는 struct는 sizeof(*header) + count * sizeof(*header->item) 대신 struct_size(header, item, count)를 사용한다.
복합식은 size_mul(), size_add(), size_sub()를 조합한다. 이 helper는 overflow 때 saturate되어 작은 값으로 wrap되는 일을 막는다. 명시적 error 처리가 필요한 code는 check_mul_overflow(), check_add_overflow(), check_sub_overflow(), check_shl_overflow() 계열을 사용한다.
Overflow를 무시하는 문자열 숫자 변환
deprecated.rst:112-120simple_strtol(), simple_strtoll(), simple_strtoul(), simple_strtoull()은 overflow를 명시적으로 무시해 caller가 예상하지 못한 값을 받을 수 있다. 각각 kstrtol(), kstrtoll(), kstrtoul(), kstrtoull()로 바꾼다. 새 helper는 입력 문자열이 NUL 또는 newline으로 끝나야 한다는 조건도 확인해야 한다.
strcpy, strncpy와 strlcpy 대체
deprecated.rst:122-163| 폐기 함수 | 문제 | 대체 |
|---|---|---|
| strcpy() | Destination 크기를 검사하지 않아 write overflow가 가능하다. | NUL 문자열에는 strscpy() |
| strncpy() | 잘리면 NUL 종료를 보장하지 않고 불필요한 NUL padding을 수행한다. | strscpy(), padding이 필요하면 strscpy_pad() |
| strlcpy() | strlen semantics 때문에 source 전체를 읽어 비종료 source에서 read overflow가 가능하다. | strscpy() |
strscpy()는 destination pointer가 아니라 복사한 non-NUL byte 수를 반환하고 truncate되면 negative errno를 반환한다. 기존 code가 strcpy·strncpy·strlcpy return value를 사용했다면 단순 함수명 치환으로 끝내지 말고 caller semantics를 함께 바꾼다.
NUL-terminated string이 아닌 고정 길이 byte field는 strtomem()과 __nonstring attribute를 사용한다. NUL padding까지 필요하면 strtomem_pad()를 쓴다.
일반 %p pointer 출력
deprecated.rst:165-187Dmesg, proc와 sysfs에 raw address를 출력하면 kernel address 노출 취약점이 된다. 현재 일반 %p는 hash된 값을 출력하므로 address로 사용할 수도 없다. 새 %p 사용은 추가하지 않는다.
- Text address는 symbol name을 보여 주는 %pS가 더 유용할 수 있다.
- Hash된 pointer가 아무 의미가 없다면 pointer 자체를 출력할 필요가 있는지 먼저 검토한다.
- 실제 pointer가 정말 필요하다면 권한, 노출 경로와 정당성을 주석과 commit message에 설명한 뒤 %px를 검토한다.
- Debug 중 hash가 방해될 때만 no_hash_pointers boot option을 일시적으로 사용할 수 있다.
Stack Variable Length Array
deprecated.rst:189-199Stack VLA는 compile-time 고정 배열보다 나쁜 machine code를 만들고 runtime 크기에 따라 남은 kernel stack을 초과할 수 있다. CONFIG_THREAD_INFO_IN_TASK가 없으면 stack 끝의 민감한 내용을, CONFIG_VMAP_STACK이 없으면 인접 memory를 덮을 위험도 있다. Kernel stack에는 VLA를 사용하지 않는다.
암묵적 switch fall-through
deprecated.rst:200-235Case 끝의 break 누락이 의도인지 bug인지 code만 보고 구분하기 어려워 실제 결함이 반복됐다. 따라서 암묵적인 fall-through는 허용하지 않고 의도한 경우 pseudo-keyword인 fallthrough를 명시한다.
- 모든 switch case block은 break로 끝낸다.
- 다음 case를 이어 실행한다면 fallthrough로 끝낸다.
- Loop 제어가 목적이면 continue를 사용한다.
- 공통 정리 경로로 이동한다면 goto <label>을 사용한다.
- 함수를 끝낸다면 return 또는 return expression을 사용한다.
Zero-length·one-element array 대신 flexible array
deprecated.rst:237-374Struct 뒤에 가변 개수 element를 붙일 때 items[1]이나 GNU extension인 items[0]을 사용하지 않고 C99 flexible array member인 items[]를 사용한다.
struct something {
size_t count;
struct foo items[];
};
instance = kmalloc(struct_size(instance, items, count), GFP_KERNEL);
instance->count = count;
memcpy(instance->items, source,
flex_array_size(instance, items, instance->count));
items[1]은 enclosing struct 크기에 element 하나를 포함하므로 allocation 때 count - 1 보정이 필요하고 쉽게 한 element를 과다 할당한다. items[0]은 sizeof(instance->items)가 항상 0이라 실제 trailing storage 크기 계산을 조용히 망가뜨릴 수 있다.
Flexible array는 incomplete type이므로 sizeof를 잘못 적용하면 build 때 발견되고, struct 마지막이 아닌 위치에 두는 실수도 compiler가 진단할 수 있다. CONFIG_FORTIFY_SOURCE와 CONFIG_UBSAN_BOUNDS의 object-size 분석도 더 정확해진다.
Flexible array가 struct의 유일한 member이거나 union 안에 있어 C99 문법상 직접 둘 수 없는 두 경우에는 DECLARE_FLEX_ARRAY()를 사용한다. UAPI header에서는 __DECLARE_FLEX_ARRAY()를 사용한다.
Struct object의 type-aware allocation
deprecated.rst:376-399Open-coded kmalloc assignment는 할당 대상 변수의 type을 kernel과 compiler가 충분히 관찰하지 못하게 해 alignment, wrap-around와 hardening 검사를 제한한다. 단일 object, array와 flexible object에 대응하는 kmalloc_obj 계열 macro를 사용한다.
ptr = kmalloc_obj(*ptr, gfp);
ptr = kzalloc_obj(*ptr, gfp);
ptr = kmalloc_objs(*ptr, count, gfp);
ptr = kzalloc_objs(*ptr, count, gfp);
__auto_type ptr = kmalloc_obj(struct foo, gfp);
이 형태는 sizeof 대상과 assignment target의 type 관계를 API에 드러낸다. 단순한 문법 축약이 아니라 compiler와 allocator hardening이 object type을 활용할 수 있게 하는 interface다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. SPDX-License-Identifier: GPL-2.0
.. _deprecated:
=====================================================================
Deprecated Interfaces, Language Features, Attributes, and Conventions
=====================================================================
In a perfect world, it would be possible to convert all instances of
some deprecated API into the new API and entirely remove the old API in
a single development cycle. However, due to the size of the kernel, the
maintainership hierarchy, and timing, it's not always feasible to do these
kinds of conversions at once. This means that new instances may sneak into
the kernel while old ones are being removed, only making the amount of
work to remove the API grow. In order to educate developers about what
has been deprecated and why, this list has been created as a place to
point when uses of deprecated things are proposed for inclusion in the
kernel.
__deprecated
------------
While this attribute does visually mark an interface as deprecated,
it `does not produce warnings during builds any more
<https://git.kernel.org/linus/771c035372a036f83353eef46dbb829780330234>`_
because one of the standing goals of the kernel is to build without
warnings and no one was actually doing anything to remove these deprecated
interfaces. While using `__deprecated` is nice to note an old API in
a header file, it isn't the full solution. Such interfaces must either
be fully removed from the kernel, or added to this file to discourage
others from using them in the future.
BUG() and BUG_ON()
------------------
Use WARN() and WARN_ON() instead, and handle the "impossible"
error condition as gracefully as possible. While the BUG()-family
of APIs were originally designed to act as an "impossible situation"
assert and to kill a kernel thread "safely", they turn out to just be
too risky. (e.g. "In what order do locks need to be released? Have
various states been restored?") Very commonly, using BUG() will
destabilize a system or entirely break it, which makes it impossible
to debug or even get viable crash reports. Linus has `very strong
<https://lore.kernel.org/lkml/CA+55aFy6jNLsywVYdGp83AMrXBo_P-pkjkphPGrO=82SPKCpLQ@mail.gmail.com/>`_
feelings `about this
<https://lore.kernel.org/lkml/CAHk-=whDHsbK3HTOpTF=ue_o04onRwTEaK_ZoJp_fjbqq4+=Jw@mail.gmail.com/>`_.
Note that the WARN()-family should only be used for "expected to
be unreachable" situations. If you want to warn about "reachable
but undesirable" situations, please use the pr_warn()-family of
functions. System owners may have set the *panic_on_warn* sysctl,
to make sure their systems do not continue running in the face of
"unreachable" conditions. (For example, see commits like `this one
<https://git.kernel.org/linus/d4689846881d160a4d12a514e991a740bcb5d65a>`_.)
open-coded arithmetic in allocator arguments
--------------------------------------------
Dynamic size calculations (especially multiplication) should not be
performed in memory allocator (or similar) function arguments due to the
risk of them overflowing. This could lead to values wrapping around and a
smaller allocation being made than the caller was expecting. Using those
allocations could lead to linear overflows of heap memory and other
misbehaviors. (One exception to this is literal values where the compiler
can warn if they might overflow. However, the preferred way in these
cases is to refactor the code as suggested below to avoid the open-coded
arithmetic.)
For example, do not use ``count * size`` as an argument, as in::
foo = kmalloc(count * size, GFP_KERNEL);
Instead, the 2-factor form of the allocator should be used::
foo = kmalloc_array(count, size, GFP_KERNEL);
Specifically, kmalloc() can be replaced with kmalloc_array(), and
kzalloc() can be replaced with kcalloc().
If no 2-factor form is available, the saturate-on-overflow helpers should
be used::
bar = dma_alloc_coherent(dev, array_size(count, size), &dma, GFP_KERNEL);
Another common case to avoid is calculating the size of a structure with
a trailing array of others structures, as in::
header = kzalloc(sizeof(*header) + count * sizeof(*header->item),
GFP_KERNEL);
Instead, use the helper::
header = kzalloc(struct_size(header, item, count), GFP_KERNEL);
.. note:: If you are using struct_size() on a structure containing a zero-length
or a one-element array as a trailing array member, please refactor such
array usage and switch to a `flexible array member
<#zero-length-and-one-element-arrays>`_ instead.
For other calculations, please compose the use of the size_mul(),
size_add(), and size_sub() helpers. For example, in the case of::
foo = krealloc(current_size + chunk_size * (count - 3), GFP_KERNEL);
Instead, use the helpers::
foo = krealloc(size_add(current_size,
size_mul(chunk_size,
size_sub(count, 3))), GFP_KERNEL);
For more details, also see array3_size() and flex_array_size(),
as well as the related check_mul_overflow(), check_add_overflow(),
check_sub_overflow(), and check_shl_overflow() family of functions.
simple_strtol(), simple_strtoll(), simple_strtoul(), simple_strtoull()
----------------------------------------------------------------------
The simple_strtol(), simple_strtoll(),
simple_strtoul(), and simple_strtoull() functions
explicitly ignore overflows, which may lead to unexpected results
in callers. The respective kstrtol(), kstrtoll(),
kstrtoul(), and kstrtoull() functions tend to be the
correct replacements, though note that those require the string to be
NUL or newline terminated.
strcpy()
--------
strcpy() performs no bounds checking on the destination buffer. This
could result in linear overflows beyond the end of the buffer, leading to
all kinds of misbehaviors. While `CONFIG_FORTIFY_SOURCE=y` and various
compiler flags help reduce the risk of using this function, there is
no good reason to add new uses of this function. The safe replacement
is strscpy(), though care must be given to any cases where the return
value of strcpy() was used, since strscpy() does not return a pointer to
the destination, but rather a count of non-NUL bytes copied (or negative
errno when it truncates).
strncpy() on NUL-terminated strings
-----------------------------------
Use of strncpy() does not guarantee that the destination buffer will
be NUL terminated. This can lead to various linear read overflows and
other misbehavior due to the missing termination. It also NUL-pads
the destination buffer if the source contents are shorter than the
destination buffer size, which may be a needless performance penalty
for callers using only NUL-terminated strings.
When the destination is required to be NUL-terminated, the replacement is
strscpy(), though care must be given to any cases where the return value
of strncpy() was used, since strscpy() does not return a pointer to the
destination, but rather a count of non-NUL bytes copied (or negative
errno when it truncates). Any cases still needing NUL-padding should
instead use strscpy_pad().
If a caller is using non-NUL-terminated strings, strtomem() should be
used, and the destinations should be marked with the `__nonstring
<https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html>`_
attribute to avoid future compiler warnings. For cases still needing
NUL-padding, strtomem_pad() can be used.
strlcpy()
---------
strlcpy() reads the entire source buffer first (since the return value
is meant to match that of strlen()). This read may exceed the destination
size limit. This is both inefficient and can lead to linear read overflows
if a source string is not NUL-terminated. The safe replacement is strscpy(),
though care must be given to any cases where the return value of strlcpy()
is used, since strscpy() will return negative errno values when it truncates.
%p format specifier
-------------------
Traditionally, using "%p" in format strings would lead to regular address
exposure flaws in dmesg, proc, sysfs, etc. Instead of leaving these to
be exploitable, all "%p" uses in the kernel are being printed as a hashed
value, rendering them unusable for addressing. New uses of "%p" should not
be added to the kernel. For text addresses, using "%pS" is likely better,
as it produces the more useful symbol name instead. For nearly everything
else, just do not add "%p" at all.
Paraphrasing Linus's current `guidance <https://lore.kernel.org/lkml/CA+55aFwQEd_d40g4mUCSsVRZzrFPUJt74vc6PPpb675hYNXcKw@mail.gmail.com/>`_:
- If the hashed "%p" value is pointless, ask yourself whether the pointer
itself is important. Maybe it should be removed entirely?
- If you really think the true pointer value is important, why is some
system state or user privilege level considered "special"? If you think
you can justify it (in comments and commit log) well enough to stand
up to Linus's scrutiny, maybe you can use "%px", along with making sure
you have sensible permissions.
If you are debugging something where "%p" hashing is causing problems,
you can temporarily boot with the debug flag "`no_hash_pointers
<https://git.kernel.org/linus/5ead723a20e0447bc7db33dc3070b420e5f80aa6>`_".
Variable Length Arrays (VLAs)
-----------------------------
Using stack VLAs produces much worse machine code than statically
sized stack arrays. While these non-trivial `performance issues
<https://git.kernel.org/linus/02361bc77888>`_ are reason enough to
eliminate VLAs, they are also a security risk. Dynamic growth of a stack
array may exceed the remaining memory in the stack segment. This could
lead to a crash, possible overwriting sensitive contents at the end of the
stack (when built without `CONFIG_THREAD_INFO_IN_TASK=y`), or overwriting
memory adjacent to the stack (when built without `CONFIG_VMAP_STACK=y`)
Implicit switch case fall-through
---------------------------------
The C language allows switch cases to fall through to the next case
when a "break" statement is missing at the end of a case. This, however,
introduces ambiguity in the code, as it's not always clear if the missing
break is intentional or a bug. For example, it's not obvious just from
looking at the code if `STATE_ONE` is intentionally designed to fall
through into `STATE_TWO`::
switch (value) {
case STATE_ONE:
do_something();
case STATE_TWO:
do_other();
break;
default:
WARN("unknown state");
}
As there have been a long list of flaws `due to missing "break" statements
<https://cwe.mitre.org/data/definitions/484.html>`_, we no longer allow
implicit fall-through. In order to identify intentional fall-through
cases, we have adopted a pseudo-keyword macro "fallthrough" which
expands to gcc's extension `__attribute__((__fallthrough__))
<https://gcc.gnu.org/onlinedocs/gcc/Statement-Attributes.html>`_.
(When the C17/C18 `[[fallthrough]]` syntax is more commonly supported by
C compilers, static analyzers, and IDEs, we can switch to using that syntax
for the macro pseudo-keyword.)
All switch/case blocks must end in one of:
* break;
* fallthrough;
* continue;
* goto <label>;
* return [expression];
Zero-length and one-element arrays
----------------------------------
There is a regular need in the kernel to provide a way to declare having
a dynamically sized set of trailing elements in a structure. Kernel code
should always use `"flexible array members" <https://en.wikipedia.org/wiki/Flexible_array_member>`_
for these cases. The older style of one-element or zero-length arrays should
no longer be used.
In older C code, dynamically sized trailing elements were done by specifying
a one-element array at the end of a structure::
struct something {
size_t count;
struct foo items[1];
};
This led to fragile size calculations via sizeof() (which would need to
remove the size of the single trailing element to get a correct size of
the "header"). A `GNU C extension <https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html>`_
was introduced to allow for zero-length arrays, to avoid these kinds of
size problems::
struct something {
size_t count;
struct foo items[0];
};
But this led to other problems, and didn't solve some problems shared by
both styles, like not being able to detect when such an array is accidentally
being used _not_ at the end of a structure (which could happen directly, or
when such a struct was in unions, structs of structs, etc).
C99 introduced "flexible array members", which lacks a numeric size for
the array declaration entirely::
struct something {
size_t count;
struct foo items[];
};
This is the way the kernel expects dynamically sized trailing elements
to be declared. It allows the compiler to generate errors when the
flexible array does not occur last in the structure, which helps to prevent
some kind of `undefined behavior
<https://git.kernel.org/linus/76497732932f15e7323dc805e8ea8dc11bb587cf>`_
bugs from being inadvertently introduced to the codebase. It also allows
the compiler to correctly analyze array sizes (via sizeof(),
`CONFIG_FORTIFY_SOURCE`, and `CONFIG_UBSAN_BOUNDS`). For instance,
there is no mechanism that warns us that the following application of the
sizeof() operator to a zero-length array always results in zero::
struct something {
size_t count;
struct foo items[0];
};
struct something *instance;
instance = kmalloc(struct_size(instance, items, count), GFP_KERNEL);
instance->count = count;
size = sizeof(instance->items) * instance->count;
memcpy(instance->items, source, size);
At the last line of code above, ``size`` turns out to be ``zero``, when one might
have thought it represents the total size in bytes of the dynamic memory recently
allocated for the trailing array ``items``. Here are a couple examples of this
issue: `link 1
<https://git.kernel.org/linus/f2cd32a443da694ac4e28fbf4ac6f9d5cc63a539>`_,
`link 2
<https://git.kernel.org/linus/ab91c2a89f86be2898cee208d492816ec238b2cf>`_.
Instead, `flexible array members have incomplete type, and so the sizeof()
operator may not be applied <https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html>`_,
so any misuse of such operators will be immediately noticed at build time.
With respect to one-element arrays, one has to be acutely aware that `such arrays
occupy at least as much space as a single object of the type
<https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html>`_,
hence they contribute to the size of the enclosing structure. This is prone
to error every time people want to calculate the total size of dynamic memory
to allocate for a structure containing an array of this kind as a member::
struct something {
size_t count;
struct foo items[1];
};
struct something *instance;
instance = kmalloc(struct_size(instance, items, count - 1), GFP_KERNEL);
instance->count = count;
size = sizeof(instance->items) * instance->count;
memcpy(instance->items, source, size);
In the example above, we had to remember to calculate ``count - 1`` when using
the struct_size() helper, otherwise we would have --unintentionally-- allocated
memory for one too many ``items`` objects. The cleanest and least error-prone way
to implement this is through the use of a `flexible array member`, together with
struct_size() and flex_array_size() helpers::
struct something {
size_t count;
struct foo items[];
};
struct something *instance;
instance = kmalloc(struct_size(instance, items, count), GFP_KERNEL);
instance->count = count;
memcpy(instance->items, source, flex_array_size(instance, items, instance->count));
There are two special cases of replacement where the DECLARE_FLEX_ARRAY()
helper needs to be used. (Note that it is named __DECLARE_FLEX_ARRAY() for
use in UAPI headers.) Those cases are when the flexible array is either
alone in a struct or is part of a union. These are disallowed by the C99
specification, but for no technical reason (as can be seen by both the
existing use of such arrays in those places and the work-around that
DECLARE_FLEX_ARRAY() uses). For example, to convert this::
struct something {
...
union {
struct type1 one[0];
struct type2 two[0];
};
};
The helper must be used::
struct something {
...
union {
DECLARE_FLEX_ARRAY(struct type1, one);
DECLARE_FLEX_ARRAY(struct type2, two);
};
};
Open-coded kmalloc assignments for struct objects
-------------------------------------------------
Performing open-coded kmalloc()-family allocation assignments prevents
the kernel (and compiler) from being able to examine the type of the
variable being assigned, which limits any related introspection that
may help with alignment, wrap-around, or additional hardening. The
kmalloc_obj()-family of macros provide this introspection, which can be
used for the common code patterns for single, array, and flexible object
allocations. For example, these open coded assignments::
ptr = kmalloc(sizeof(*ptr), gfp);
ptr = kzalloc(sizeof(*ptr), gfp);
ptr = kmalloc_array(count, sizeof(*ptr), gfp);
ptr = kcalloc(count, sizeof(*ptr), gfp);
ptr = kmalloc(sizeof(struct foo, gfp);
become, respectively::
ptr = kmalloc_obj(*ptr, gfp);
ptr = kzalloc_obj(*ptr, gfp);
ptr = kmalloc_objs(*ptr, count, gfp);
ptr = kzalloc_objs(*ptr, count, gfp);
__auto_type ptr = kmalloc_obj(struct foo, gfp);
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
Deprecated pattern 목록이 필요한 이유
1-18이상적으로는 deprecated API의 모든 사용을 새 API로 바꾸고 한 development cycle 안에 old API를 완전히 제거할 수 있어야 한다. 그러나 kernel 규모, maintainer hierarchy, 일정 때문에 한 번에 전환하기 어려운 경우가 많다.
기존 사용을 제거하는 동안 새 사용이 들어오면 API 제거 작업량이 오히려 늘어난다. 무엇이 왜 deprecated되었는지 developer에게 알리고 새 사용이 kernel inclusion 대상으로 제안될 때 가리킬 기준으로 이 목록을 만들었다.
__deprecated attribute의 한계
20-30__deprecated는 interface가 deprecated되었음을 source에서 시각적으로 표시하지만 이제 build warning을 생성하지 않는다. Kernel은 warning 없이 build하는 것을 목표로 하고, warning이 있어도 실제로 old interface를 제거하는 사람이 없었기 때문이다.
Header에서 old API를 표시하는 데에는 유용하지만 완전한 해결책은 아니다. Interface를 kernel에서 완전히 제거하거나 이 문서에 추가하여 향후 사용을 막아야 한다.
BUG()와 BUG_ON() 대신 복구 가능한 처리
32-52BUG()와 BUG_ON() 대신 WARN()과 WARN_ON()을 사용하고 “불가능한” error condition도 가능한 한 정상적으로 처리한다. BUG family는 impossible-state assertion과 kernel thread의 안전한 종료를 목표로 했지만 실제로는 lock 해제 순서와 state 복원 여부를 보장하기 어려워 위험하다.
BUG()는 흔히 system을 불안정하게 하거나 완전히 망가뜨려 debugging과 유효한 crash report 수집까지 불가능하게 한다.
WARN family는 도달하지 않을 것으로 예상하는 상황에만 사용한다. 도달 가능하지만 바람직하지 않은 상황을 알리려면 pr_warn family를 사용한다. System owner가 unreachable condition 뒤 실행을 계속하지 않도록 panic_on_warn sysctl을 설정했을 수 있다.
- BUG 사용에 관한 Linus의 설명 1
https://lore.kernel.org/lkml/CA+55aFy6jNLsywVYdGp83AMrXBo_P-pkjkphPGrO=82SPKCpLQ@mail.gmail.com/ - BUG 사용에 관한 Linus의 설명 2
https://lore.kernel.org/lkml/CAHk-=whDHsbK3HTOpTF=ue_o04onRwTEaK_ZoJp_fjbqq4+=Jw@mail.gmail.com/ - panic_on_warn 관련 예
https://git.kernel.org/linus/d4689846881d160a4d12a514e991a740bcb5d65a
Allocator argument의 open-coded arithmetic
54-110Dynamic size 계산, 특히 multiplication을 memory allocator argument 안에서 직접 수행하면 overflow로 값이 wrap되어 caller 예상보다 작은 allocation이 생길 수 있다. 이를 사용하면 heap linear overflow와 다른 잘못된 동작이 발생한다. Compiler가 overflow를 경고할 수 있는 literal은 예외일 수 있지만 이 경우도 helper로 바꾸는 편이 낫다.
count * size를 직접 넘기지 않는다.
/* 나쁜 예 */
foo = kmalloc(count * size, GFP_KERNEL);
/* 권장 */
foo = kmalloc_array(count, size, GFP_KERNEL);
kmalloc()은 kmalloc_array(), kzalloc()은 kcalloc()로 교체한다. Two-factor allocator가 없으면 overflow 시 saturate하는 helper를 사용한다.
bar = dma_alloc_coherent(dev, array_size(count, size), &dma,
GFP_KERNEL);
Trailing array가 있는 structure 크기도 sizeof와 multiplication으로 직접 계산하지 않는다.
/* 나쁜 예 */
header = kzalloc(sizeof(*header) + count * sizeof(*header->item),
GFP_KERNEL);
/* 권장 */
header = kzalloc(struct_size(header, item, count), GFP_KERNEL);
Zero-length 또는 one-element trailing array를 가진 structure에 struct_size()를 사용 중이라면 flexible array member로 먼저 전환한다.
다른 계산은 size_mul(), size_add(), size_sub()를 조합한다.
/* 나쁜 예 */
foo = krealloc(current_size + chunk_size * (count - 3), GFP_KERNEL);
/* 권장 */
foo = krealloc(size_add(current_size,
size_mul(chunk_size,
size_sub(count, 3))), GFP_KERNEL);
그 밖에 array3_size(), flex_array_size(), check_mul_overflow(), check_add_overflow(), check_sub_overflow(), check_shl_overflow() family도 사용한다.
simple_strto*()와 string copy API
112-163simple_strtol(), simple_strtoll(), simple_strtoul(), simple_strtoull()은 overflow를 명시적으로 무시하므로 caller가 예상하지 못한 결과를 받을 수 있다. 각각 kstrtol(), kstrtoll(), kstrtoul(), kstrtoull()로 교체한다. 새 API는 string이 NUL 또는 newline으로 끝나야 한다.
strcpy()
strcpy()는 destination bound를 검사하지 않아 buffer 끝을 넘는 linear overflow를 만들 수 있다. CONFIG_FORTIFY_SOURCE=y와 compiler flag가 위험을 줄이지만 새 사용을 추가할 이유는 없다. strscpy()로 교체한다. strcpy()는 destination pointer를 return하지만 strscpy()는 복사한 non-NUL byte 수 또는 truncation 시 negative errno를 return하므로 return value 사용을 함께 수정해야 한다.
NUL-terminated string에 strncpy() 사용
strncpy()는 destination의 NUL termination을 보장하지 않아 linear read overflow 등을 일으킬 수 있다. Source가 destination보다 짧으면 전체 destination을 NUL padding하여 불필요한 성능 비용도 낸다.
NUL termination이 필요하면 strscpy()를 사용하고 return 의미 차이를 처리한다. NUL padding이 필요하면 strscpy_pad()를 쓴다. Non-NUL-terminated string이면 strtomem()을 사용하고 향후 compiler warning을 피하려고 destination에 __nonstring attribute를 붙인다. Padding도 필요하면 strtomem_pad()를 쓴다.
strlcpy()
strlcpy()는 strlen()과 같은 return value를 만들기 위해 source 전체를 먼저 읽는다. 이 read는 destination size limit을 넘을 수 있어 비효율적이며 source가 NUL-terminated가 아니면 linear read overflow를 만들 수 있다. strscpy()로 교체하되 truncation 시 negative errno를 return하는 차이를 처리한다.
%p format specifier
165-187전통적으로 format string의 %p는 dmesg, proc, sysfs 등에서 실제 address를 노출하는 취약점을 만들었다. 현재 kernel의 %p는 exploit을 막으려고 hash된 값을 출력하므로 address로 사용할 수 없다. 새 %p 사용을 추가하지 않는다.
Text address에는 유용한 symbol name을 출력하는 %pS가 더 적절할 수 있다. 그 밖의 대부분은 pointer를 아예 출력하지 않는다.
- Hash된 %p 값이 쓸모없다면 pointer 자체가 정말 필요한지 검토하고 가능하면 출력 전체를 제거한다.
- 실제 pointer가 반드시 필요하다면 특정 system state나 privilege level을 특별히 신뢰할 이유를 묻는다. Comment와 commit log로 충분히 정당화하고 sensible permission을 보장할 수 있을 때만 %px를 검토한다.
- %p hashing이 debugging을 방해하면 임시로 no_hash_pointers debug boot flag를 사용할 수 있다.
Variable Length Array
189-199Stack VLA는 static-size stack array보다 훨씬 나쁜 machine code를 만든다. 성능 문제뿐 아니라 security risk도 있다. Stack array가 dynamic하게 커지면 stack segment의 남은 memory를 넘을 수 있다.
그 결과 crash, CONFIG_THREAD_INFO_IN_TASK=y가 없을 때 stack 끝의 민감한 content overwrite, CONFIG_VMAP_STACK=y가 없을 때 stack 인접 memory overwrite가 생길 수 있다.
Implicit switch fall-through 금지
200-236C는 case 끝에 break가 없으면 다음 case로 fall through한다. 하지만 누락이 의도인지 bug인지 알기 어려워 code가 모호해진다.
switch (value) {
case STATE_ONE:
do_something();
case STATE_TWO:
do_other();
break;
default:
WARN("unknown state");
}
Missing break로 생긴 defect가 많아 implicit fall-through는 허용하지 않는다. 의도적인 경우 GCC __attribute__((__fallthrough__))로 확장되는 pseudo-keyword macro fallthrough를 사용한다. C17/C18 [[fallthrough]]를 compiler, analyzer, IDE가 더 널리 지원하면 macro도 그 syntax로 바꿀 수 있다.
모든 switch/case block은 다음 중 하나로 끝나야 한다.
- break;
- fallthrough;
- continue;
- goto <label>;
- return [expression];
Zero-length와 one-element array를 flexible array로 전환
237-284Structure 끝에 dynamic-size element 집합을 두어야 하는 경우 kernel code는 항상 flexible array member를 사용한다. 오래된 one-element 또는 zero-length array style은 더 이상 사용하지 않는다.
옛 C code는 trailing element를 one-element array로 선언했다.
struct something {
size_t count;
struct foo items[1];
};
Header size를 구할 때 trailing element 하나의 크기를 빼야 하므로 sizeof 계산이 취약했다. GNU C zero-length extension은 이 문제를 피하려 했지만 다른 문제를 만들었고 array가 structure 마지막이 아닌 위치에 잘못 쓰이는 것도 검출하지 못했다.
struct something {
size_t count;
struct foo items[0];
};
C99 flexible array member는 size를 완전히 생략한다. Kernel이 기대하는 선언은 이 형태다.
struct something {
size_t count;
struct foo items[];
};
Compiler는 flexible array가 structure 마지막에 없으면 error를 내어 undefined behavior 유입을 막는다. sizeof(), CONFIG_FORTIFY_SOURCE, CONFIG_UBSAN_BOUNDS도 array size를 올바르게 분석할 수 있다.
Zero-length array의 sizeof() 함정
285-310Zero-length array에 sizeof()를 적용하면 항상 0이지만 compiler warning이 없다. 다음 code에서 trailing items에 allocation한 전체 byte 수를 계산한다고 생각할 수 있으나 size는 0이 되어 memcpy가 아무 것도 복사하지 않는다.
struct something {
size_t count;
struct foo items[0];
};
struct something *instance;
instance = kmalloc(struct_size(instance, items, count), GFP_KERNEL);
instance->count = count;
size = sizeof(instance->items) * instance->count;
memcpy(instance->items, source, size);
Flexible array member는 incomplete type이어서 sizeof()를 적용할 수 없으므로 이런 오용을 build time에 즉시 발견한다.
One-element array allocation과 올바른 helper
312-349One-element array는 element 하나만큼 실제 공간을 차지하므로 enclosing structure 크기에 포함된다. Dynamic allocation size를 계산할 때 오류를 만들기 쉽다.
struct something {
size_t count;
struct foo items[1];
};
struct something *instance;
instance = kmalloc(struct_size(instance, items, count - 1), GFP_KERNEL);
instance->count = count;
size = sizeof(instance->items) * instance->count;
memcpy(instance->items, source, size);
count - 1을 기억하지 않으면 item 하나를 더 allocation한다. 가장 명확하고 오류가 적은 방법은 flexible array member와 struct_size(), flex_array_size()를 함께 쓰는 것이다.
struct something {
size_t count;
struct foo items[];
};
struct something *instance;
instance = kmalloc(struct_size(instance, items, count), GFP_KERNEL);
instance->count = count;
memcpy(instance->items, source,
flex_array_size(instance, items, instance->count));
DECLARE_FLEX_ARRAY()가 필요한 두 경우
350-375Flexible array가 struct 안에 혼자 있거나 union member인 두 경우에는 DECLARE_FLEX_ARRAY() helper를 사용한다. UAPI header에서는 __DECLARE_FLEX_ARRAY()라는 이름을 쓴다. C99 specification은 기술적 이유 없이 이 배치를 금지하므로 helper가 workaround를 제공한다.
/* 오래된 형태 */
struct something {
...
union {
struct type1 one[0];
struct type2 two[0];
};
};
/* 권장 형태 */
struct something {
...
union {
DECLARE_FLEX_ARRAY(struct type1, one);
DECLARE_FLEX_ARRAY(struct type2, two);
};
};
Struct object allocation에는 kmalloc_obj family 사용
376-398Open-coded kmalloc family assignment는 kernel과 compiler가 destination variable type을 검사하지 못하게 한다. Alignment, wrap-around, 추가 hardening에 유용한 introspection을 제한한다. kmalloc_obj family macro는 single, array, flexible object allocation pattern에 type 정보를 제공한다.
/* open-coded */
ptr = kmalloc(sizeof(*ptr), gfp);
ptr = kzalloc(sizeof(*ptr), gfp);
ptr = kmalloc_array(count, sizeof(*ptr), gfp);
ptr = kcalloc(count, sizeof(*ptr), gfp);
ptr = kmalloc(sizeof(struct foo, gfp);
/* type-aware replacement */
ptr = kmalloc_obj(*ptr, gfp);
ptr = kzalloc_obj(*ptr, gfp);
ptr = kmalloc_objs(*ptr, count, gfp);
ptr = kzalloc_objs(*ptr, count, gfp);
__auto_type ptr = kmalloc_obj(struct foo, gfp);
다섯 번째 open-coded 예제의 괄호 형태는 Linux v6.18.37 원문을 그대로 보존했다.
폐기 목록이 필요한 이유
deprecated.rst:5-30이상적으로는 낡은 API 사용처를 한 development cycle 안에 모두 새 API로 바꾸고 old API를 제거해야 한다. 실제 kernel은 규모가 크고 maintainer hierarchy와 merge timing이 달라 일괄 변환이 어렵다. 기존 사용처를 줄이는 동안 새 사용처가 들어오면 제거 작업은 오히려 늘어난다.
__deprecated attribute는 header에서 낡은 interface를 눈에 띄게 표시하지만 더 이상 build warning을 만들지 않는다. Kernel은 warning 없이 build되는 것을 목표로 하고, warning만 켜 두었을 때 아무도 실제 제거를 진행하지 않았기 때문이다. Interface를 완전히 제거하거나 이 문서에 이유와 대안을 남겨 새 사용을 막아야 한다.