CVE-2026-74700
Full vulnerability record from the local CVENow database.
Description
In the Linux kernel, the following vulnerability has been resolved: net/sched: cls_api: Always acquire rtnl_lock when destroying locked classifiers Another challenge with unlocked filters. There is a short window in tc_new_tfilter where a tcf_proto can be found and briefly referenced by a totally unrelated, unlocked classifier's request and cause a race. Feng created a poc which created this race with two threads, one creating a u32 filter and other a flower filter in the same chain/prio: 1. Both threads enter tc_new_tfilter, both find the chain empty, both drop filter_chain_lock 2. u32 finishes tcf_proto_create("u32") first, calls tcf_chain_tp_insert_unique() -> inserts u32_tp into the chain 3. flower finishes tcf_proto_create("flower") later, calls tcf_chain_tp_insert_unique() -> tcf_chain_tp_find() now sees u32_tp already there, takes a reference on it, destroys flower's own tp_new and returns u32_tp to the caller. Flower then hits the kind mismatch check (because it requested for kind "flower" but tp->ops->kind is "u32") and goes through the errout path which calls tcf_proto_put() on u32_tp. If the u32 thread has already gone through its own errout (its change() call failed on the PoC's empty options) and dropped its create and insert refs, flower's put is the last one and drops u32_tp's refcnt to zero. At this point tp->ops->destroy() runs in a context that never took rtnl_lock. When that happens, it might cause a UAF like the following (illustrated by the PoC): [ +0.000710] BUG: KASAN: slab-use-after-free in u32_init (net/sched/cls_u32.c:393) [ +0.000281] Read of size 8 at addr ffff888120022f00 by task poc_feng_xue/524 Call Trace: u32_init (net/sched/cls_u32.c:393) tc_new_tfilter (net/sched/cls_api.c:2378) Allocated by task 526: u32_init (net/sched/cls_u32.c:378) tc_new_tfilter (net/sched/cls_api.c:2378) Freed by task 522: kfree u32_destroy (net/sched/cls_u32.c:662) tcf_proto_destroy (net/sched/cls_api.c:446) tcf_proto_put (net/sched/cls_api.c:459) tc_new_tfilter (net/sched/cls_api.c:2459) Fix this by having tcf_proto_destroy() take rtnl_lock around tp->ops->destroy() for locked classifiers whenever rtnl is not held. To explain why I used a temp variable "not_lockless" I'd like to point to a semi-related note on rtnl_held vs TCF_PROTO_OPS_DOIT_UNLOCKED (adding here for future cleanup if deemed necessary): The rtnl_held parameter and the TCF_PROTO_OPS_DOIT_UNLOCKED flag are redundant sources of truth for whether rtnl_lock is held. Among the nine classifier destroy(..rtnl_held..) callbacks, only flower consults the rtnl_held parameter which it propagates to tc_setup_cb_destroy() and tc_setup_cb_call(). The other eight (u32, flow, bpf, cgroup, route, basic, fw, mall) ignore it entirely;-> those that call tc_setup_cb_destroy() (u32, bpf, mall) hardcode true always instead of forwarding the parameter. A future cleanup should remove the rtnl_held parameter from the destroy callback signature entirely and have callers rely solely on their knowledge whether they are running in an unlocked context.
View original CVE JSON
{
"id": "CVE-2026-74700",
"sourceIdentifier": "416baaa9-dc9f-4396-8d5f-8c081fb06d67",
"published": "2026-08-22T16:16:44.610",
"lastModified": "2026-10-30T13:20:04.270",
"vulnStatus": "Received",
"cveTags": [],
"descriptions": [
{
"lang": "en",
"value": "In the Linux kernel, the following vulnerability has been resolved:\n\nnet/sched: cls_api: Always acquire rtnl_lock when destroying locked classifiers\n\nAnother challenge with unlocked filters.\nThere is a short window in tc_new_tfilter where a tcf_proto can be found\nand briefly referenced by a totally unrelated, unlocked classifier's request\nand cause a race.\n\nFeng created a poc which created this race with two threads, one creating a\nu32 filter and other a flower filter in the same chain/prio:\n\n1. Both threads enter tc_new_tfilter, both find the chain empty, both\n drop filter_chain_lock\n2. u32 finishes tcf_proto_create(\"u32\") first, calls\n tcf_chain_tp_insert_unique() -> inserts u32_tp into the chain\n3. flower finishes tcf_proto_create(\"flower\") later, calls\n tcf_chain_tp_insert_unique() -> tcf_chain_tp_find() now sees u32_tp\n already there, takes a reference on it, destroys flower's own tp_new\n and returns u32_tp to the caller.\n\nFlower then hits the kind mismatch check (because it requested for kind\n\"flower\" but tp->ops->kind is \"u32\") and goes through the errout path\nwhich calls tcf_proto_put() on u32_tp. If the u32 thread has already\ngone through its own errout (its change() call failed on the PoC's empty\noptions) and dropped its create and insert refs, flower's put is the\nlast one and drops u32_tp's refcnt to zero.\n\nAt this point tp->ops->destroy() runs in a context that never took\nrtnl_lock. When that happens, it might cause a UAF like the following\n(illustrated by the PoC):\n\n[ +0.000710] BUG: KASAN: slab-use-after-free in u32_init (net/sched/cls_u32.c:393)\n[ +0.000281] Read of size 8 at addr ffff888120022f00 by task poc_feng_xue/524\n\n Call Trace:\n u32_init (net/sched/cls_u32.c:393)\n tc_new_tfilter (net/sched/cls_api.c:2378)\n\n Allocated by task 526:\n u32_init (net/sched/cls_u32.c:378)\n tc_new_tfilter (net/sched/cls_api.c:2378)\n\n Freed by task 522:\n kfree\n u32_destroy (net/sched/cls_u32.c:662)\n tcf_proto_destroy (net/sched/cls_api.c:446)\n tcf_proto_put (net/sched/cls_api.c:459)\n tc_new_tfilter (net/sched/cls_api.c:2459)\n\nFix this by having tcf_proto_destroy() take rtnl_lock around\ntp->ops->destroy() for locked classifiers whenever rtnl is not held.\n\nTo explain why I used a temp variable \"not_lockless\" I'd like to point to a\nsemi-related note on rtnl_held vs TCF_PROTO_OPS_DOIT_UNLOCKED (adding here\nfor future cleanup if deemed necessary):\nThe rtnl_held parameter and the TCF_PROTO_OPS_DOIT_UNLOCKED flag are\nredundant sources of truth for whether rtnl_lock is held. Among the nine\nclassifier destroy(..rtnl_held..) callbacks, only flower consults the\nrtnl_held parameter which it propagates to tc_setup_cb_destroy()\nand tc_setup_cb_call(). The other eight (u32, flow, bpf, cgroup, route, basic,\nfw, mall) ignore it entirely;-> those that call tc_setup_cb_destroy()\n(u32, bpf, mall) hardcode true always instead of forwarding the parameter.\n\nA future cleanup should remove the rtnl_held parameter from the destroy callback\nsignature entirely and have callers rely solely on their knowledge whether\nthey are running in an unlocked context."
},
{
"lang": "es",
"value": "En el kernel de Linux, la siguiente vulnerabilidad ha sido resuelta:\n\nnet/sched: cls_api: Adquirir siempre rtnl_lock al destruir clasificadores bloqueados\n\nOtro desafío con filtros desbloqueados.\nExiste una ventana corta en tc_new_tfilter donde un tcf_proto puede ser encontrado y brevemente referenciado por la solicitud de un clasificador totalmente no relacionado y desbloqueado, y causar una condición de carrera.\n\nFeng creó una PoC que generó esta condición de carrera con dos hilos, uno creando un filtro u32 y el otro un filtro flower en la misma cadena/prioridad:\n\n1. Ambos hilos entran en tc_new_tfilter, ambos encuentran la cadena vacía, ambos sueltan filter_chain_lock\n2. u32 termina tcf_proto_create('u32') primero, llama a tcf_chain_tp_insert_unique() -> inserta u32_tp en la cadena\n3. flower termina tcf_proto_create('flower') más tarde, llama a tcf_chain_tp_insert_unique() -> tcf_chain_tp_find() ahora ve u32_tp ya allí, toma una referencia sobre él, destruye el propio tp_new de flower y devuelve u32_tp al llamador.\n\nFlower entonces encuentra la verificación de tipo no coincidente (porque solicitó el tipo 'flower' pero tp -> ops -> kind es 'u32') y pasa por la ruta de error que llama a tcf_proto_put() en u32_tp. Si el hilo u32 ya ha pasado por su propio error (su llamada a change() falló en las opciones vacías de la PoC) y ha soltado sus referencias de creación e inserción, el put de flower es el último y reduce el refcnt de u32_tp a cero.\n\nEn este punto, tp -> ops -> destroy() se ejecuta en un contexto que nunca tomó rtnl_lock. Cuando eso sucede, podría causar un UAF como el siguiente (ilustrado por la PoC):\n\n[ +0.000710] BUG: KASAN: uso después de liberación de slab en u32_init (net/sched/cls_u32.c:393)\n[ +0.000281] Lectura de tamaño 8 en la dirección ffff888120022f00 por la tarea poc_feng_xue/524\n\n Rastro de Llamada:\n u32_init (net/sched/cls_u32.c:393)\n tc_new_tfilter (net/sched/cls_api.c:2378)\n\n Asignado por la tarea 526:\n u32_init (net/sched/cls_u32.c:378)\n tc_new_tfilter (net/sched/cls_api.c:2378)\n\n Liberado por la tarea 522:\n kfree\n u32_destroy (net/sched/cls_u32.c:662)\n tcf_proto_destroy (net/sched/cls_api.c:446)\n tcf_proto_put (net/sched/cls_api.c:459)\n tc_new_tfilter (net/sched/cls_api.c:2459)\n\nSolucione esto haciendo que tcf_proto_destroy() tome rtnl_lock alrededor de tp -> ops -> destroy() para clasificadores bloqueados siempre que rtnl no esté retenido.\n\nPara explicar por qué usé una variable temporal 'not_lockless' me gustaría señalar una nota semi-relacionada sobre rtnl_held vs TCF_PROTO_OPS_DOIT_UNLOCKED (añadiendo aquí para una futura limpieza si se considera necesario):\nEl parámetro rtnl_held y la bandera TCF_PROTO_OPS_DOIT_UNLOCKED son fuentes de verdad redundantes sobre si rtnl_lock está retenido. Entre las nueve devoluciones de llamada destroy(..rtnl_held..) del clasificador, solo flower consulta el parámetro rtnl_held, el cual propaga a tc_setup_cb_destroy() y tc_setup_cb_call(). Los otros ocho (u32, flow, bpf, cgroup, route, basic, fw, mall) lo ignoran por completo; -> aquellos que llaman a tc_setup_cb_destroy() (u32, bpf, mall) codifican 'true' siempre en lugar de reenviar el parámetro.\n\nUna futura limpieza debería eliminar el parámetro rtnl_held de la firma de la devolución de llamada destroy por completo y hacer que los llamadores confíen únicamente en su conocimiento sobre si se están ejecutando en un contexto desbloqueado."
}
],
"affected": [
{
"source": "416baaa9-dc9f-4396-8d5f-8c081fb06d67",
"affectedData": [
{
"vendor": "Linux",
"product": "Linux",
"defaultStatus": "unaffected",
"programFiles": [
"net/sched/cls_api.c"
],
"repo": "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git",
"versions": [
{
"version": "12db03b65c2b90752e4c37666977fd4a1b5f5824",
"lessThan": "73e8544e0df5d11b1914bcc25ae284946b51786d",
"versionType": "git",
"status": "affected"
},
{
"version": "12db03b65c2b90752e4c37666977fd4a1b5f5824",
"lessThan": "34e77d8e3570f9df3952496ddb402833695662fd",
"versionType": "git",
"status": "affected"
},
{
"version": "12db03b65c2b90752e4c37666977fd4a1b5f5824",
"lessThan": "b648c8a56531aeabd1c14f6b5cf1891e269b3756",
"versionType": "git",
"status": "affected"
},
{
"version": "12db03b65c2b90752e4c37666977fd4a1b5f5824",
"lessThan": "d6222af7274f08e7a1848131dc30319993d8f377",
"versionType": "git",
"status": "affected"
},
{
"version": "12db03b65c2b90752e4c37666977fd4a1b5f5824",
"lessThan": "a81f9c44d87fb59d99fce72e29e02cab3a49a1c3",
"versionType": "git",
"status": "affected"
},
{
"version": "12db03b65c2b90752e4c37666977fd4a1b5f5824",
"lessThan": "a347304b2ca1a5377d5bd2d8a72e4b4f12afe648",
"versionType": "git",
"status": "affected"
}
]
},
{
"vendor": "Linux",
"product": "Linux",
"defaultStatus": "affected",
"programFiles": [
"net/sched/cls_api.c"
],
"repo": "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git",
"versions": [
{
"version": "5.1",
"status": "affected"
},
{
"version": "0",
"lessThan": "5.1",
"versionType": "semver",
"status": "unaffected"
},
{
"version": "6.1.191",
"lessThanOrEqual": "6.1.*",
"versionType": "semver",
"status": "unaffected"
},
{
"version": "6.6.152",
"lessThanOrEqual": "6.6.*",
"versionType": "semver",
"status": "unaffected"
},
{
"version": "6.12.104",
"lessThanOrEqual": "6.12.*",
"versionType": "semver",
"status": "unaffected"
},
{
"version": "6.18.45",
"lessThanOrEqual": "6.18.*",
"versionType": "semver",
"status": "unaffected"
},
{
"version": "7.1.9",
"lessThanOrEqual": "7.1.*",
"versionType": "semver",
"status": "unaffected"
},
{
"version": "7.2",
"lessThanOrEqual": "*",
"versionType": "original_commit_for_fix",
"status": "unaffected"
}
]
}
]
}
],
"metrics": {
"cvssMetricV31": [
{
"source": "416baaa9-dc9f-4396-8d5f-8c081fb06d67",
"type": "Secondary",
"cvssData": {
"version": "3.1",
"vectorString": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
"baseScore": 7.8,
"baseSeverity": "HIGH",
"attackVector": "LOCAL",
"attackComplexity": "LOW",
"privilegesRequired": "LOW",
"userInteraction": "NONE",
"scope": "UNCHANGED",
"confidentialityImpact": "HIGH",
"integrityImpact": "HIGH",
"availabilityImpact": "HIGH"
},
"exploitabilityScore": 1.8,
"impactScore": 5.9
}
],
"ssvcV203": [
{
"source": "134c704f-9b21-4f2e-91b3-4a467353bcc0",
"ssvcData": {
"timestamp": "2026-10-16T19:14:51.766758Z",
"id": "CVE-2026-74700",
"options": [
{
"exploitation": "none"
},
{
"automatable": "no"
},
{
"technicalImpact": "total"
}
],
"role": "CISA Coordinator",
"version": "2.0.3"
}
}
]
},
"references": [
{
"url": "https://git.kernel.org/stable/c/34e77d8e3570f9df3952496ddb402833695662fd",
"source": "416baaa9-dc9f-4396-8d5f-8c081fb06d67"
},
{
"url": "https://git.kernel.org/stable/c/73e8544e0df5d11b1914bcc25ae284946b51786d",
"source": "416baaa9-dc9f-4396-8d5f-8c081fb06d67"
},
{
"url": "https://git.kernel.org/stable/c/a347304b2ca1a5377d5bd2d8a72e4b4f12afe648",
"source": "416baaa9-dc9f-4396-8d5f-8c081fb06d67"
},
{
"url": "https://git.kernel.org/stable/c/a81f9c44d87fb59d99fce72e29e02cab3a49a1c3",
"source": "416baaa9-dc9f-4396-8d5f-8c081fb06d67"
},
{
"url": "https://git.kernel.org/stable/c/b648c8a56531aeabd1c14f6b5cf1891e269b3756",
"source": "416baaa9-dc9f-4396-8d5f-8c081fb06d67"
},
{
"url": "https://git.kernel.org/stable/c/d6222af7274f08e7a1848131dc30319993d8f377",
"source": "416baaa9-dc9f-4396-8d5f-8c081fb06d67"
}
]
}