棄用的介面、語言特性、屬性和約定

在理想情況下,可以在一個開發週期內將所有棄用 API 的例項轉換為新 API,並完全刪除舊 API。然而,由於核心的龐大規模、維護層次結構和時間限制,一次性完成此類轉換並不總是可行。這意味著在舊例項被刪除的同時,新例項可能會悄悄進入核心,這隻會增加刪除 API 的工作量。為了告知開發人員哪些內容已被棄用以及原因,特此建立此列表,以便在提議將棄用內容納入核心時作為參考。

__deprecated

儘管此屬性在視覺上將介面標記為已棄用,但它在構建時不再產生警告,因為核心的既定目標之一是無警告構建,並且沒有人真正採取行動刪除這些棄用的介面。儘管在標頭檔案中使用__deprecated來標記舊 API 很好,但這並非完整的解決方案。此類介面必須要麼從核心中完全刪除,要麼新增到此檔案中,以勸阻其他人將來使用它們。

BUG() 和 BUG_ON()

請改用 WARN() 和 WARN_ON(),並儘可能優雅地處理“不可能”的錯誤情況。儘管 BUG() 系列 API 最初旨在作為“不可能情況”斷言並“安全地”終止核心執行緒,但它們最終被證明風險太大。(例如,“鎖需要以何種順序釋放?各種狀態是否已恢復?”)非常常見的是,使用 BUG() 會破壞系統穩定性或使其完全崩潰,這使得除錯或獲取可用的崩潰報告變得不可能。Linus 對此有 非常強烈的看法

請注意,WARN() 系列應僅用於“預期不可達”的情況。如果您想警告“可達但不理想”的情況,請使用 pr_warn() 系列函式。系統所有者可能已設定 panic_on_warn sysctl,以確保其系統在面臨“不可達”條件時不會繼續執行。(例如,請參閱像 這個 提交。)

分配器引數中的開放式算術運算

動態大小計算(尤其是乘法)不應在記憶體分配器(或類似)函式引數中執行,因為它們存在溢位風險。這可能導致值迴繞,並導致分配的記憶體小於呼叫者預期的大小。使用這些分配可能導致堆記憶體的線性溢位和其他異常行為。(一個例外是字面值,編譯器可以警告它們是否可能溢位。然而,在這些情況下,首選方法是按照以下建議重構程式碼,以避免開放式算術運算。)

例如,不要使用 count * size 作為引數,如:

foo = kmalloc(count * size, GFP_KERNEL);

相反,應使用分配器的雙因子形式:

foo = kmalloc_array(count, size, GFP_KERNEL);

具體來說,kmalloc() 可以替換為 kmalloc_array()kzalloc() 可以替換為 kcalloc()

如果沒有雙因子形式可用,則應使用溢位飽和輔助函式:

bar = dma_alloc_coherent(dev, array_size(count, size), &dma, GFP_KERNEL);

另一個需要避免的常見情況是計算帶有尾隨其他結構陣列的結構體大小,如:

header = kzalloc(sizeof(*header) + count * sizeof(*header->item),
                 GFP_KERNEL);

相反,請使用輔助函式:

header = kzalloc(struct_size(header, item, count), GFP_KERNEL);

注意

如果您正在對包含零長度或單元素陣列作為尾隨陣列成員的結構體使用 struct_size(),請重構此類陣列用法並改用柔性陣列成員

對於其他計算,請組合使用 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() 系列函式。

simple_strtol()、simple_strtoll()、simple_strtoul()、simple_strtoull()

simple_strtol()simple_strtoll()simple_strtoul()simple_strtoull() 函式會明確忽略溢位,這可能導致呼叫者獲得意外結果。相應的 kstrtol()kstrtoll()kstrtoul()kstrtoull() 函式通常是正確的替代方案,但請注意,這些函式要求字串以空字元或換行符終止。

strcpy()

strcpy() 不對目標緩衝區執行邊界檢查。這可能導致超出緩衝區末尾的線性溢位,從而引發各種異常行為。儘管 CONFIG_FORTIFY_SOURCE=y 和各種編譯器標誌有助於降低使用此函式的風險,但沒有充分的理由新增此函式的使用。安全的替代方案是 strscpy(),但必須注意任何使用了 strcpy() 返回值的情況,因為 strscpy() 不返回指向目標緩衝區的指標,而是返回已複製的非空位元組數(或截斷時的負 errno 值)。

對空終止字串使用 strncpy()

使用 strncpy() 並不能保證目標緩衝區會以空字元終止。由於缺少終止符,這可能導致各種線性讀溢位和其他異常行為。如果源內容短於目標緩衝區大小,它還會用空字元填充目標緩衝區,這對於僅使用空終止字串的呼叫者來說可能是不必要的效能損失。

當目標需要空終止時,替代方案是 strscpy(),但必須注意任何使用了 strncpy() 返回值的情況,因為 strscpy() 不返回指向目標緩衝區的指標,而是返回已複製的非空位元組數(或截斷時的負 errno 值)。任何仍需要空字元填充的情況應改用 strscpy_pad()

如果呼叫方使用非空終止字串,則應使用 strtomem(),並且目標應標記為 __nonstring 屬性以避免未來的編譯器警告。對於仍需要空字元填充的情況,可以使用 strtomem_pad()

strlcpy()

strlcpy() 會首先讀取整個源緩衝區(因為其返回值應與 strlen() 匹配)。這種讀取可能會超出目標大小限制。這既低效,如果源字串不是空終止的,還可能導致線性讀溢位。安全的替代方案是 strscpy(),但必須注意任何使用了 strlcpy() 返回值的情況,因為 strscpy() 在截斷時將返回負 errno 值。

%p 格式說明符

傳統上,在格式字串中使用“%p”會導致 dmesg、proc、sysfs 等中常見的地址暴露缺陷。為了避免這些缺陷被利用,核心中所有“%p”的使用都被列印為雜湊值,使其無法用於定址。不應在核心中新增“%p”的使用。對於文字地址,使用“%pS”可能更好,因為它會生成更有用的符號名稱。對於幾乎所有其他情況,根本不要新增“%p”。

轉述 Linus 當前的指導意見

  • 如果雜湊化的“%p”值毫無意義,請問自己指標本身是否重要。也許它應該被完全移除?

  • 如果您真的認為真實的指標值很重要,那麼為什麼某些系統狀態或使用者許可權級別被認為是“特殊”的?如果您認為自己可以充分證明其合理性(在註釋和提交日誌中),足以經受住 Linus 的審查,那麼您可以使用“%px”,並確保擁有合理的許可權。

如果您在除錯過程中遇到“%p”雜湊導致問題,可以暫時使用除錯標誌“no_hash_pointers”啟動。

變長陣列(VLAs)

使用棧 VLA 會產生比靜態大小棧陣列差得多的機器碼。儘管這些非平凡的效能問題足以成為消除 VLA 的理由,但它們也存在安全風險。棧陣列的動態增長可能超出棧段中的剩餘記憶體。這可能導致崩潰,可能覆蓋棧末尾的敏感內容(當構建時沒有 CONFIG_THREAD_INFO_IN_TASK=y),或覆蓋棧附近記憶體(當構建時沒有 CONFIG_VMAP_STACK=y)。

隱式 switch case 穿透

C 語言允許在 case 語句末尾缺少“break”語句時,switch case 會穿透到下一個 case。然而,這在程式碼中引入了歧義,因為不總是清楚缺失的 break 是有意為之還是一個 bug。例如,僅從程式碼來看,不明顯 STATE_ONE 是否有意設計為穿透到 STATE_TWO

switch (value) {
case STATE_ONE:
        do_something();
case STATE_TWO:
        do_other();
        break;
default:
        WARN("unknown state");
}

由於缺少“break”語句導致的缺陷由來已久,我們不再允許隱式穿透。為了識別有意穿透的情況,我們引入了一個偽關鍵字宏“fallthrough”,它擴充套件為 gcc 的擴充套件 __attribute__((__fallthrough__))。(當 C17/C18 [[fallthrough]] 語法更普遍地受 C 編譯器、靜態分析器和 IDE 支援時,我們可以切換到使用該語法作為宏偽關鍵字。)

所有 switch/case 塊必須以以下之一結尾:

  • break;

  • fallthrough;

  • continue;

  • goto <label>;

  • return [expression];

零長度和單元素陣列

核心中經常需要提供一種方式來宣告結構體中包含一組動態大小的尾隨元素。核心程式碼在這些情況下應始終使用“柔性陣列成員”。舊式單元素或零長度陣列不應再使用。

在較早的 C 程式碼中,動態大小的尾隨元素是透過在結構體末尾指定一個單元素陣列來實現的:

struct something {
        size_t count;
        struct foo items[1];
};

這導致了透過 sizeof() 進行脆弱的大小計算(需要減去單個尾隨元素的大小才能獲得“頭部”的正確大小)。GNU C 擴充套件引入了零長度陣列,以避免這類大小問題:

struct something {
        size_t count;
        struct foo items[0];
};

但這導致了其他問題,並且沒有解決兩種樣式共有的某些問題,例如無法檢測到此類陣列何時意外地被用於結構體末尾_之外_(這可能直接發生,或者當此類結構體存在於聯合、結構體中的結構體等情況時)。

C99 引入了“柔性陣列成員”,其陣列宣告完全沒有數值大小:

struct something {
        size_t count;
        struct foo items[];
};

這是核心期望動態大小的尾隨元素宣告的方式。它允許編譯器在柔性陣列未出現在結構體末尾時生成錯誤,這有助於防止某種未定義行為錯誤被無意中引入程式碼庫。它還允許編譯器正確分析陣列大小(透過 sizeof()、CONFIG_FORTIFY_SOURCECONFIG_UBSAN_BOUNDS)。例如,沒有任何機制警告我們對零長度陣列應用 sizeof() 運算子總是會得到零:

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);

在上面的程式碼的最後一行,size 結果為 ,而人們可能以為它代表最近為尾隨陣列 items 分配的動態記憶體的總位元組大小。以下是此問題的幾個示例:連結 1連結 2。相反,柔性陣列成員具有不完整型別,因此不能應用 sizeof() 運算子,所以任何對此類運算子的誤用都將在構建時立即被發現。

對於單元素陣列,必須敏銳地意識到此類陣列至少佔用與該型別單個物件一樣多的空間,因此它們會增加其所在結構體的大小。每當人們想計算包含此類陣列作為成員的結構體所需分配的動態記憶體總大小時,這都很容易出錯:

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);

在上面的示例中,當我們使用 struct_size() 輔助函式時,必須記住計算 count - 1,否則我們將會——無意中——為過多的 items 物件分配記憶體。實現這一點的最簡潔且最不容易出錯的方法是使用柔性陣列成員,並結合 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() 輔助函式。(請注意,在 UAPI 標頭檔案中,它被命名為 __DECLARE_FLEX_ARRAY()。)這些情況是當柔性陣列單獨存在於結構體中,或者作為聯合體的一部分時。C99 規範不允許這些情況,但沒有技術原因(從現有在這些位置使用此類陣列以及 DECLARE_FLEX_ARRAY() 使用的變通方法可以看出)。例如,要轉換這個:

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);
        };
};