diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt index 36704512..f04aea5e 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt @@ -413,6 +413,21 @@ fun getAllAIGeneratedValidators(): Map { Validator("config_parse_socket_service", "0") to ConfigParseSocketServiceOptionValue() as OptionValueInformation, Validator("config_parse_service_sockets", "0") to ConfigParseServiceSocketsOptionValue() as OptionValueInformation, + // Batch 2026-10: seccomp-bpf-cgroup + Validator("config_parse_syscall_filter", "0") to ConfigParseSyscallFilterOptionValue() as OptionValueInformation, + Validator("config_parse_syscall_log", "0") to ConfigParseSyscallLogOptionValue() as OptionValueInformation, + Validator("config_parse_restrict_filesystems", "0") to ConfigParseRestrictFilesystemsOptionValue() as OptionValueInformation, + Validator("config_parse_private_bpf", "0") to ConfigParsePrivateBpfOptionValue() as OptionValueInformation, + Validator("config_parse_bpf_delegate_commands", "0") to ConfigParseBpfDelegateCommandsOptionValue() as OptionValueInformation, + Validator("config_parse_bpf_delegate_maps", "0") to ConfigParseBpfDelegateMapsOptionValue() as OptionValueInformation, + Validator("config_parse_bpf_delegate_programs", "0") to ConfigParseBpfDelegateProgramsOptionValue() as OptionValueInformation, + Validator("config_parse_bpf_delegate_attachments", "0") to ConfigParseBpfDelegateAttachmentsOptionValue() as OptionValueInformation, + Validator("config_parse_cgroup_nft_set", "NFT_SET_PARSE_CGROUP") to ConfigParseCgroupNftSetOptionValue() as OptionValueInformation, + Validator("config_parse_bpf_foreign_program", "0") to ConfigParseBpfForeignProgramOptionValue() as OptionValueInformation, + Validator("config_parse_xattr", "0") to ConfigParseXattrOptionValue() as OptionValueInformation, + Validator("config_parse_socket_bindtodevice", "0") to ConfigParseSocketBindtodeviceOptionValue() as OptionValueInformation, + // END Batch 2026-10: seccomp-bpf-cgroup + ) return allValidators diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateAttachmentsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateAttachmentsOptionValue.kt new file mode 100644 index 00000000..6a596ce9 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateAttachmentsOptionValue.kt @@ -0,0 +1,57 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.bpfDelegateList + +/* + * BPFDelegateAttachments= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#BPFDelegateAttachments= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L181 config_parse_bpf_delegate_attachments (0), via DEFINE_CONFIG_PARSE_PTR + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.h#L287 bpf_delegate_attachments_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4221-L4253 bpf_delegate_from_string ("any", or a "," list; unknown names warned and ignored) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4197 DEFINE_STRING_TABLE_LOOKUP(bpf_delegate_attach_type) — exact streq + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/generate-bpf-delegate-configs.py builds bpf_delegate_attach_type_table from enum bpf_attach_type in src/include/uapi/linux/bpf.h + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.private-bpf.sh#L52 BPFDelegateAttachments=BPFFlowDissector,BPFCgroupSysctl,BPFNetfilter + * linux https://man7.org/linux/man-pages/man2/bpf.2.html + */ + +/** + * Validator for `BPFDelegateAttachments=` (the bpffs `delegate_attachs=` mount option; only effective with `PrivateBPF=yes`). + * + * Either `any` (the whole value) or a comma-separated list of the names below — see [bpfDelegateList] + * for the splitting rules (commas only, no spaces; empty fields are skipped). The names are what + * src/core/generate-bpf-delegate-configs.py makes of each `BPF_…` enumerator of `enum bpf_attach_type` in the + * uapi bpf.h systemd bundles at 0f0682b773 (`BPF_FOO_BAR` → `BPFFooBar`; aliases written as + * `X = Y,` are skipped by its regex and therefore not accepted). Regenerate with + * + * python3 src/core/generate-bpf-delegate-configs.py code src/include/uapi/linux/bpf.h + * + * An unknown name is logged ("Unknown BPF delegate option, ignoring") and dropped, so it is flagged. + * The list tracks the bundled kernel header, so a newer systemd may know names this one does not. + * + * Covers: Service.BPFDelegateAttachments, Socket.BPFDelegateAttachments, Mount.BPFDelegateAttachments, Swap.BPFDelegateAttachments. + * + * The empty assignment (mask 0, i.e. nothing delegated) never reaches the grammar. + */ +class ConfigParseBpfDelegateAttachmentsOptionValue : SimpleGrammarOptionValues( + "config_parse_bpf_delegate_attachments", + bpfDelegateList( + FlexibleLiteralChoiceTerminal( + "BPFCgroupInetIngress", "BPFCgroupInetEgress", "BPFCgroupInetSockCreate", "BPFCgroupSockOps", "BPFSkSkbStreamParser", + "BPFSkSkbStreamVerdict", "BPFCgroupDevice", "BPFSkMsgVerdict", "BPFCgroupInet4Bind", "BPFCgroupInet6Bind", + "BPFCgroupInet4Connect", "BPFCgroupInet6Connect", "BPFCgroupInet4PostBind", "BPFCgroupInet6PostBind", "BPFCgroupUdp4Sendmsg", + "BPFCgroupUdp6Sendmsg", "BPFLircMode2", "BPFFlowDissector", "BPFCgroupSysctl", "BPFCgroupUdp4Recvmsg", + "BPFCgroupUdp6Recvmsg", "BPFCgroupGetsockopt", "BPFCgroupSetsockopt", "BPFTraceRawTp", "BPFTraceFentry", + "BPFTraceFexit", "BPFModifyReturn", "BPFLsmMac", "BPFTraceIter", "BPFCgroupInet4Getpeername", + "BPFCgroupInet6Getpeername", "BPFCgroupInet4Getsockname", "BPFCgroupInet6Getsockname", "BPFXdpDevmap", "BPFCgroupInetSockRelease", + "BPFXdpCpumap", "BPFSkLookup", "BPFXdp", "BPFSkSkbVerdict", "BPFSkReuseportSelect", + "BPFSkReuseportSelectOrMigrate", "BPFPerfEvent", "BPFTraceKprobeMulti", "BPFLsmCgroup", "BPFStructOps", + "BPFNetfilter", "BPFTcxIngress", "BPFTcxEgress", "BPFTraceUprobeMulti", "BPFCgroupUnixConnect", + "BPFCgroupUnixSendmsg", "BPFCgroupUnixRecvmsg", "BPFCgroupUnixGetpeername", "BPFCgroupUnixGetsockname", "BPFNetkitPrimary", + "BPFNetkitPeer", "BPFTraceKprobeSession", "BPFTraceUprobeSession", "BPFTraceFsession", "BPFTraceFentryMulti", + "BPFTraceFexitMulti", "BPFTraceFsessionMulti", + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateCommandsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateCommandsOptionValue.kt new file mode 100644 index 00000000..09f4167e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateCommandsOptionValue.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.bpfDelegateList + +/* + * BPFDelegateCommands= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#BPFDelegateCommands= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L178 config_parse_bpf_delegate_commands (0), via DEFINE_CONFIG_PARSE_PTR + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.h#L263 bpf_delegate_commands_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4221-L4253 bpf_delegate_from_string ("any", or a "," list; unknown names warned and ignored) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4194 DEFINE_STRING_TABLE_LOOKUP(bpf_delegate_cmd) — exact streq + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/generate-bpf-delegate-configs.py builds bpf_delegate_cmd_table from enum bpf_cmd in src/include/uapi/linux/bpf.h + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.private-bpf.sh#L49 BPFDelegateCommands=BPFObjPin,BPFBtfLoad,BPFMapFreeze,BPFLinkDetach + * linux https://man7.org/linux/man-pages/man2/bpf.2.html + */ + +/** + * Validator for `BPFDelegateCommands=` (the bpffs `delegate_cmds=` mount option; only effective with `PrivateBPF=yes`). + * + * Either `any` (the whole value) or a comma-separated list of the names below — see [bpfDelegateList] + * for the splitting rules (commas only, no spaces; empty fields are skipped). The names are what + * src/core/generate-bpf-delegate-configs.py makes of each `BPF_…` enumerator of `enum bpf_cmd` in the + * uapi bpf.h systemd bundles at 0f0682b773 (`BPF_FOO_BAR` → `BPFFooBar`; aliases written as + * `X = Y,` are skipped by its regex and therefore not accepted). Regenerate with + * + * python3 src/core/generate-bpf-delegate-configs.py code src/include/uapi/linux/bpf.h + * + * An unknown name is logged ("Unknown BPF delegate option, ignoring") and dropped, so it is flagged. + * The list tracks the bundled kernel header, so a newer systemd may know names this one does not. + * + * Covers: Service.BPFDelegateCommands, Socket.BPFDelegateCommands, Mount.BPFDelegateCommands, Swap.BPFDelegateCommands. + * + * The empty assignment (mask 0, i.e. nothing delegated) never reaches the grammar. + */ +class ConfigParseBpfDelegateCommandsOptionValue : SimpleGrammarOptionValues( + "config_parse_bpf_delegate_commands", + bpfDelegateList( + FlexibleLiteralChoiceTerminal( + "BPFMapCreate", "BPFMapLookupElem", "BPFMapUpdateElem", "BPFMapDeleteElem", "BPFMapGetNextKey", + "BPFProgLoad", "BPFObjPin", "BPFObjGet", "BPFProgAttach", "BPFProgDetach", + "BPFProgTestRun", "BPFProgGetNextId", "BPFMapGetNextId", "BPFProgGetFdById", "BPFMapGetFdById", + "BPFObjGetInfoByFd", "BPFProgQuery", "BPFRawTracepointOpen", "BPFBtfLoad", "BPFBtfGetFdById", + "BPFTaskFdQuery", "BPFMapLookupAndDeleteElem", "BPFMapFreeze", "BPFBtfGetNextId", "BPFMapLookupBatch", + "BPFMapLookupAndDeleteBatch", "BPFMapUpdateBatch", "BPFMapDeleteBatch", "BPFLinkCreate", "BPFLinkUpdate", + "BPFLinkGetFdById", "BPFLinkGetNextId", "BPFEnableStats", "BPFIterCreate", "BPFLinkDetach", + "BPFProgBindMap", "BPFTokenCreate", "BPFProgStreamReadByFd", "BPFProgAssocStructOps", + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateMapsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateMapsOptionValue.kt new file mode 100644 index 00000000..b9c66217 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateMapsOptionValue.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.bpfDelegateList + +/* + * BPFDelegateMaps= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#BPFDelegateMaps= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L179 config_parse_bpf_delegate_maps (0), via DEFINE_CONFIG_PARSE_PTR + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.h#L271 bpf_delegate_maps_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4221-L4253 bpf_delegate_from_string ("any", or a "," list; unknown names warned and ignored) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4195 DEFINE_STRING_TABLE_LOOKUP(bpf_delegate_map_type) — exact streq + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/generate-bpf-delegate-configs.py builds bpf_delegate_map_type_table from enum bpf_map_type in src/include/uapi/linux/bpf.h + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.private-bpf.sh#L50 BPFDelegateMaps=BPFMapTypeArray,BPFMapTypeCpumap,BPFMapTypeRingbuf + * linux https://man7.org/linux/man-pages/man2/bpf.2.html + */ + +/** + * Validator for `BPFDelegateMaps=` (the bpffs `delegate_maps=` mount option; only effective with `PrivateBPF=yes`). + * + * Either `any` (the whole value) or a comma-separated list of the names below — see [bpfDelegateList] + * for the splitting rules (commas only, no spaces; empty fields are skipped). The names are what + * src/core/generate-bpf-delegate-configs.py makes of each `BPF_…` enumerator of `enum bpf_map_type` in the + * uapi bpf.h systemd bundles at 0f0682b773 (`BPF_FOO_BAR` → `BPFFooBar`; aliases written as + * `X = Y,` are skipped by its regex and therefore not accepted). Regenerate with + * + * python3 src/core/generate-bpf-delegate-configs.py code src/include/uapi/linux/bpf.h + * + * An unknown name is logged ("Unknown BPF delegate option, ignoring") and dropped, so it is flagged. + * The list tracks the bundled kernel header, so a newer systemd may know names this one does not. + * + * Covers: Service.BPFDelegateMaps, Socket.BPFDelegateMaps, Mount.BPFDelegateMaps, Swap.BPFDelegateMaps. + * + * The empty assignment (mask 0, i.e. nothing delegated) never reaches the grammar. + */ +class ConfigParseBpfDelegateMapsOptionValue : SimpleGrammarOptionValues( + "config_parse_bpf_delegate_maps", + bpfDelegateList( + FlexibleLiteralChoiceTerminal( + "BPFMapTypeUnspec", "BPFMapTypeHash", "BPFMapTypeArray", "BPFMapTypeProgArray", "BPFMapTypePerfEventArray", + "BPFMapTypePercpuHash", "BPFMapTypePercpuArray", "BPFMapTypeStackTrace", "BPFMapTypeCgroupArray", "BPFMapTypeLruHash", + "BPFMapTypeLruPercpuHash", "BPFMapTypeLpmTrie", "BPFMapTypeArrayOfMaps", "BPFMapTypeHashOfMaps", "BPFMapTypeDevmap", + "BPFMapTypeSockmap", "BPFMapTypeCpumap", "BPFMapTypeXskmap", "BPFMapTypeSockhash", "BPFMapTypeCgroupStorageDeprecated", + "BPFMapTypeReuseportSockarray", "BPFMapTypePercpuCgroupStorageDeprecated", "BPFMapTypeQueue", "BPFMapTypeStack", "BPFMapTypeSkStorage", + "BPFMapTypeDevmapHash", "BPFMapTypeStructOps", "BPFMapTypeRingbuf", "BPFMapTypeInodeStorage", "BPFMapTypeTaskStorage", + "BPFMapTypeBloomFilter", "BPFMapTypeUserRingbuf", "BPFMapTypeCgrpStorage", "BPFMapTypeArena", "BPFMapTypeInsnArray", + "BPFMapTypeRhash", + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateProgramsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateProgramsOptionValue.kt new file mode 100644 index 00000000..646b2410 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateProgramsOptionValue.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.bpfDelegateList + +/* + * BPFDelegatePrograms= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#BPFDelegatePrograms= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L180 config_parse_bpf_delegate_programs (0), via DEFINE_CONFIG_PARSE_PTR + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.h#L279 bpf_delegate_programs_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4221-L4253 bpf_delegate_from_string ("any", or a "," list; unknown names warned and ignored) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4196 DEFINE_STRING_TABLE_LOOKUP(bpf_delegate_prog_type) — exact streq + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/generate-bpf-delegate-configs.py builds bpf_delegate_prog_type_table from enum bpf_prog_type in src/include/uapi/linux/bpf.h + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.private-bpf.sh#L51 BPFDelegatePrograms=BPFProgTypeTracepoint,BPFProgTypeXdp,BPFProgTypeTracing + * linux https://man7.org/linux/man-pages/man2/bpf.2.html + */ + +/** + * Validator for `BPFDelegatePrograms=` (the bpffs `delegate_progs=` mount option; only effective with `PrivateBPF=yes`). + * + * Either `any` (the whole value) or a comma-separated list of the names below — see [bpfDelegateList] + * for the splitting rules (commas only, no spaces; empty fields are skipped). The names are what + * src/core/generate-bpf-delegate-configs.py makes of each `BPF_…` enumerator of `enum bpf_prog_type` in the + * uapi bpf.h systemd bundles at 0f0682b773 (`BPF_FOO_BAR` → `BPFFooBar`; aliases written as + * `X = Y,` are skipped by its regex and therefore not accepted). Regenerate with + * + * python3 src/core/generate-bpf-delegate-configs.py code src/include/uapi/linux/bpf.h + * + * An unknown name is logged ("Unknown BPF delegate option, ignoring") and dropped, so it is flagged. + * The list tracks the bundled kernel header, so a newer systemd may know names this one does not. + * + * Quirk faithfully reproduced: the generator's `(\w+)\b,` full-match also skips enumerators followed + * by a comment, so `BPF_PROG_TYPE_SYSCALL, /* … */` (bpf.h#L1094) never becomes `BPFProgTypeSyscall` + * and that spelling is rejected by systemd (and every later program type is shifted down one bit — + * an upstream bug, see the batch QUESTIONS.md). + * + * Covers: Service.BPFDelegatePrograms, Socket.BPFDelegatePrograms, Mount.BPFDelegatePrograms, Swap.BPFDelegatePrograms. + * + * The empty assignment (mask 0, i.e. nothing delegated) never reaches the grammar. + */ +class ConfigParseBpfDelegateProgramsOptionValue : SimpleGrammarOptionValues( + "config_parse_bpf_delegate_programs", + bpfDelegateList( + FlexibleLiteralChoiceTerminal( + "BPFProgTypeUnspec", "BPFProgTypeSocketFilter", "BPFProgTypeKprobe", "BPFProgTypeSchedCls", "BPFProgTypeSchedAct", + "BPFProgTypeTracepoint", "BPFProgTypeXdp", "BPFProgTypePerfEvent", "BPFProgTypeCgroupSkb", "BPFProgTypeCgroupSock", + "BPFProgTypeLwtIn", "BPFProgTypeLwtOut", "BPFProgTypeLwtXmit", "BPFProgTypeSockOps", "BPFProgTypeSkSkb", + "BPFProgTypeCgroupDevice", "BPFProgTypeSkMsg", "BPFProgTypeRawTracepoint", "BPFProgTypeCgroupSockAddr", "BPFProgTypeLwtSeg6local", + "BPFProgTypeLircMode2", "BPFProgTypeSkReuseport", "BPFProgTypeFlowDissector", "BPFProgTypeCgroupSysctl", "BPFProgTypeRawTracepointWritable", + "BPFProgTypeCgroupSockopt", "BPFProgTypeTracing", "BPFProgTypeStructOps", "BPFProgTypeExt", "BPFProgTypeLsm", + "BPFProgTypeSkLookup", "BPFProgTypeNetfilter", + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfForeignProgramOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfForeignProgramOptionValue.kt new file mode 100644 index 00000000..51ebc47d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfForeignProgramOptionValue.kt @@ -0,0 +1,61 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UNIT_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * BPFProgram= ([Service]/[Socket]/[Mount]/[Swap]/[Slice]/[Scope]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html#BPFProgram=type:program-path + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L5878-L5938 config_parse_bpf_foreign_program (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (":", no flags; separators coalesce) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/bpf-program.c#L25-L46 bpf_cgroup_attach_type_table (exact streq lookup) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L253 unit_path_printf (specifiers) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (PATH_CHECK_ABSOLUTE, normalized) + * linux https://man7.org/linux/man-pages/man2/bpf.2.html (BPF_PROG_ATTACH attach types) + */ + +/** bpf_cgroup_attach_type_table: the cgroup attach types, by their bpftool names. */ +private val BPF_CGROUP_ATTACH_TYPE = FlexibleLiteralChoiceTerminal( + "ingress", "egress", "sock_create", "sock_ops", "device", "bind4", "bind6", "connect4", "connect6", + "post_bind4", "post_bind6", "sendmsg4", "sendmsg6", "sysctl", "recvmsg4", "recvmsg6", "getsockopt", "setsockopt", +) + +/** + * Validator for `BPFProgram=TYPE:PATH`. + * + * config_parse_bpf_foreign_program() splits off the first `:`-delimited word (extract_first_word with + * `:` as the only separator; a run of colons coalesces, so `egress::/x` still leaves `/x`), requires + * something after it (leading colons are skipped too, so `:egress:/x` is the type `egress`), and looks the word up in bpf_cgroup_attach_type_table — one of `ingress`, + * `egress`, `sock_create`, `sock_ops`, `device`, `bind4`, `bind6`, `connect4`, `connect6`, + * `post_bind4`, `post_bind6`, `sendmsg4`, `sendmsg6`, `sysctl`, `recvmsg4`, `recvmsg6`, `getsockopt`, + * `setsockopt` (exact, case-sensitive). + * + * The rest is a path: specifiers are expanded (unit_path_printf), then path_simplify_and_warn() + * requires it to be absolute and normalized (no `..` component). That is [UNIT_PATH]: it must start + * with `/` or a specifier and contain no `..` component; the rest of the value belongs to the path, so + * further colons are part of it. Every failure is "…, ignoring", so it is flagged. + * + * Not checked: that the path is on bpffs (the man page says it generally starts `/sys/fs/bpf/`, but the + * parser does not require it — the attach simply fails at runtime). + * + * Covers: Service.BPFProgram, Socket.BPFProgram, Mount.BPFProgram, Swap.BPFProgram, Slice.BPFProgram, + * Scope.BPFProgram. + * + * The empty assignment removes all programs and never reaches the grammar. + */ +class ConfigParseBpfForeignProgramOptionValue : SimpleGrammarOptionValues( + "config_parse_bpf_foreign_program", + SequenceCombinator( + ZeroOrOne(RegexTerminal(":+", ":+")), + BPF_CGROUP_ATTACH_TYPE, + RegexTerminal(":+", ":+"), + UNIT_PATH, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCgroupNftSetOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCgroupNftSetOptionValue.kt new file mode 100644 index 00000000..c4942d68 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCgroupNftSetOptionValue.kt @@ -0,0 +1,104 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * NFTSet= in the cgroup context ([Service]/[Socket]/[Mount]/[Swap]/[Slice]/[Scope]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html#NFTSet=family:table:set (term anchor as generated from the man page) + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L6779-L6795 config_parse_cgroup_nft_set (NFT_SET_PARSE_CGROUP) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/firewall-util.c#L1207-L1295 config_parse_nft_set + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/firewall-util.c#L1136 nft_set_source_table (only cgroup/user/group allowed for NFT_SET_PARSE_CGROUP, #L1258-L1260) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/firewall-util.c#L1125 nfproto_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L824 nft_identifier_valid (NFT_NAME_MAXLEN 256) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L240 extract_many_words (":" , EXTRACT_CUNESCAPE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L39-L55 extract_first_word (skips leading separators; trailing ones coalesce at #L151-L158) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.nft.sh#L24 NFTSet=cgroup:inet:sd_test:c user:inet:sd_test:u group:inet:sd_test:g + * linux https://www.netfilter.org/projects/nftables/manpage.html (nft(8): address families, table/set names) + */ + +/** The NFT_SET_PARSE_CGROUP sources; address/prefix/ifindex belong to networkd's NFTSet= only. */ +private val NFT_CGROUP_SOURCE = FlexibleLiteralChoiceTerminal("cgroup", "user", "group") + +/** nfproto_table: the nft address families. */ +private val NFT_FAMILY = FlexibleLiteralChoiceTerminal("arp", "bridge", "inet", "ip", "ip6", "netdev") + +/** + * nft_identifier_valid: an ASCII letter, then letters, digits, `/`, `\`, `_` or `.`, shorter than + * NFT_NAME_MAXLEN (256). The lenient side takes anything up to the next `:` or whitespace so a bad + * name is located as one token. + */ +private val NFT_IDENTIFIER = RegexTerminal("""[^\s:"']+""", """[A-Za-z][A-Za-z0-9/\\_.]{0,254}""") + +/** + * extract_many_words() is extract_first_word() in a loop, so runs of `:` coalesce (`cgroup::inet:t:s` + * is still four fields). + */ +private val FIELD_SEPARATOR = RegexTerminal(":+", ":+") + +/** + * extract_first_word() skips leading separators, and after the 4th word it swallows any trailing run of + * separators up to the end of the string (leaving q == NULL, so the isempty(q) check passes) — hence + * `:cgroup:inet:t:s` and `cgroup:inet:t:s:` are both accepted. + */ +private val NFT_SET_TUPLE = SequenceCombinator( + ZeroOrOne(FIELD_SEPARATOR), + NFT_CGROUP_SOURCE, FIELD_SEPARATOR, NFT_FAMILY, FIELD_SEPARATOR, NFT_IDENTIFIER, FIELD_SEPARATOR, NFT_IDENTIFIER, + ZeroOrOne(FIELD_SEPARATOR), +) + +/** The outer split uses EXTRACT_UNQUOTE, so a whole tuple may be quoted. */ +private val NFT_SET_ENTRY = AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("\""), NFT_SET_TUPLE, LiteralChoiceTerminal("\"")), + SequenceCombinator(LiteralChoiceTerminal("'"), NFT_SET_TUPLE, LiteralChoiceTerminal("'")), + NFT_SET_TUPLE, +) + +/** + * Validator for the unit-file `NFTSet=` (cgroup context). + * + * A whitespace-separated list of `SOURCE:FAMILY:TABLE:SET` tuples: + * + * - SOURCE is `cgroup`, `user` or `group`. The shared config_parse_nft_set() also knows `address`, + * `prefix` and `ifindex`, but rejects them for this ltype ("Unknown NFT source"). + * - FAMILY is one of `arp`, `bridge`, `inet`, `ip`, `ip6`, `netdev` (exact match). + * - TABLE and SET must satisfy nft_identifier_valid(). + * + * Exactly four fields: extract_many_words() is asked for four and anything left over fails the tuple. + * Any failure logs a warning and abandons the rest of the line, so one bad tuple is flagged. + * + * The man page's term reads `NFTSet=family:table:set`, but both its body text and the C parser require + * the leading source type; this grammar follows the C. + * + * Leading and trailing `:` around a tuple are accepted (see NFT_SET_TUPLE). + * + * Known over-rejection: the outer whitespace split uses EXTRACT_UNQUOTE, so quotes *inside* a tuple + * (`cgroup:inet:"t":s`) are removed by systemd and accepted; this grammar only accepts a tuple quoted + * as a whole. No real configuration writes that. + * + * Deliberately lenient: backslash escapes inside a name (decoded by EXTRACT_CUNESCAPE before the + * identifier check) are not decoded — `\` is simply accepted as an identifier character, as + * nft_identifier_valid() does after decoding. + * + * Covers: Service.NFTSet, Socket.NFTSet, Mount.NFTSet, Swap.NFTSet, Slice.NFTSet, Scope.NFTSet. (The + * [Network]/[Address]/… NFTSet= of networkd uses ltype NFT_SET_PARSE_NETWORK and is not covered.) + * + * The empty assignment clears the list and never reaches the grammar. + */ +class ConfigParseCgroupNftSetOptionValue : SimpleGrammarOptionValues( + "config_parse_cgroup_nft_set", + SequenceCombinator( + NFT_SET_ENTRY, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), NFT_SET_ENTRY)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePrivateBpfOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePrivateBpfOptionValue.kt new file mode 100644 index 00000000..69b1094a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePrivateBpfOptionValue.kt @@ -0,0 +1,39 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * PrivateBPF= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#PrivateBPF= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L150 config_parse_private_bpf (0), via DEFINE_CONFIG_PARSE_ENUM + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4185-L4190 private_bpf_table / DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-table.c#L32 string_table_lookup_from_string_with_boolean + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23 parse_boolean (STRCASE_IN_SET) + */ + +/** + * Validator for `PrivateBPF=`. + * + * private_bpf_from_string() tries parse_boolean() first and then the table, whose only entries are + * `no` and `yes` — both already boolean spellings. So the accepted set is exactly parse_boolean's: + * `1 yes y true t on` / `0 no n false f off`, compared case-insensitively (STRCASE_IN_SET), so `Yes` + * and `TRUE` are fine. That is the shared [BOOLEAN]; there are no non-boolean names left to put in front + * of it, so no PreferValidAlternativeCombinator is needed. (Were the table to grow a name such as + * `auto`, it would be matched case-sensitively by streq, unlike the boolean spellings.) + * + * Note: DEFINE_CONFIG_PARSE_ENUM has no empty-string branch, so systemd actually logs `PrivateBPF=` as + * a parse error; the plugin never hands empty values to a validator, so that is not flagged. + * + * Covers: Service.PrivateBPF, Socket.PrivateBPF, Mount.PrivateBPF, Swap.PrivateBPF. + */ +class ConfigParsePrivateBpfOptionValue : SimpleGrammarOptionValues( + "config_parse_private_bpf", + SequenceCombinator( + BOOLEAN, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRestrictFilesystemsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRestrictFilesystemsOptionValue.kt new file mode 100644 index 00000000..e2884df5 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRestrictFilesystemsOptionValue.kt @@ -0,0 +1,172 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FILESYSTEM_GROUP_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FILESYSTEM_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WHITESPACE_OR_COMMA_SEPARATOR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * RestrictFileSystems= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#RestrictFileSystems= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L3645-L3712 config_parse_restrict_filesystems (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (WHITESPACE ",", EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/bpf-restrict-fs.c#L212 bpf_restrict_fs_parse_filesystem + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/filesystems.c#L21 filesystem_set_find (group names) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/bpf-restrict-fs.c#L117 fs_type_from_string at unit start ("Invalid filesystem name, ignoring"), runtime only + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/filesystem-sets.py#L9 NAME_TO_MAGIC / FILESYSTEM_SETS (the name tables) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.exec-context.sh#L351-L358 ("foo", "@foo" are logged and ignored) + * linux https://man7.org/linux/man-pages/man2/statfs.2.html (the f_type magics the names map to) + * + * sd-check at 0f0682b773 (one value per file): accepted `EXT4`, `ext5`, `invalid`, `temporary`, + * `~proc,sysfs,foo`, `@auxiliary-api btrfs nfs @temporary unknown`, `ext"4"`, `ext4""`, `""`, `"a\"b"`, + * `ext4 ~proc`, `ext4 ~@foo`, `~ ~@bogus`, `"~@bogus"`, `\~proc`, `foo.bar!`; refused `@unknowngroup`, `@`, + * `@Basic-api`, `~@bogus`, `"@foo"`, `""@foo`, `\@foo`, `"@basic-api"x` ("Unknown filesystem group") and + * `ext"4`, `ext4 "unterminated` ("Trailing garbage"). + */ + +// Regex pieces for one word as extract_first_word(…, WHITESPACE ",", EXTRACT_UNQUOTE) reads it: unquoted +// characters, `\x` escapes (the backslash is dropped and any character may follow, also inside quotes) and +// '…' / "…" runs whose quotes are removed. An unterminated quote is "Trailing garbage", so quoted runs must close. + +/** Any one piece of a word. */ +private const val WORD_PIECE = """[^\s,"'\\]|\\[\s\S]|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'""" + +/** + * The first piece of a word, which must not unquote to `@` (bpf_restrict_fs_parse_filesystem() would look the + * word up as a group and refuse an unknown one) and must not be a raw `~` (at the start of the value that is + * the deny-list marker; elsewhere [TILDE_WORD] covers it). + */ +private const val FIRST_PIECE_NO_AT_NO_TILDE = + """[^\s,"'\\@~]|\\[^@]|"(?:[^"\\@]|\\[^@])(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\@]|\\[^@])(?:[^'\\]|\\[\s\S])*'""" + +/** [FIRST_PIECE_NO_AT_NO_TILDE] after leading empty quotes, where a `~` is just a name character (`""~x` is `~x`). */ +private const val FIRST_PIECE_NO_AT = + """[^\s,"'\\@]|\\[^@]|"(?:[^"\\@]|\\[^@])(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\@]|\\[^@])(?:[^'\\]|\\[\s\S])*'""" + +/** + * The known file system names as exact literals (completion, enum coloring). Not the lenient + * [FILESYSTEM_NAME] itself: since any name is valid here, its "well-formed but unknown" token would only add + * a second, invalid parse for every unknown word, and a list of unknown words would then explode into + * 2^n parses on the new engine. + */ +private val KNOWN_FILESYSTEM_NAME = LiteralChoiceTerminal(*FILESYSTEM_NAME.choices) + +/** + * A known name, bare or quoted as a whole, filling a whole word. [ANY_FILESYSTEM_NAME] steps aside for these, + * so they are matched by [KNOWN_FILESYSTEM_NAME] and each word has a single parse. + */ +private val KNOWN_NAME_WORD = """(?["']?)(?:""" + KNOWN_FILESYSTEM_NAME.choices.joinToString("|") { Regex.escape(it) } + + """)\k(?=[\s,]|\z)""" + +/** + * Any word the unit-file parser stores as a plain file system name, i.e. one that does not unquote to a + * leading `@`. The parser keeps such names unchecked: an unknown one (`EXT4`, `ext5`, `foo`) is only logged + * and skipped when the unit starts and the BPF map is filled ("bpf-restrict-fs: Invalid filesystem name '%s', + * ignoring.", src/core/bpf-restrict-fs.c#L117-L121), so it is accepted here. Issue #565 tracks a separate + * warning for such names. A word that is wholly empty quotes (`""`) is stored as the empty name and accepted. + */ +private val ANY_FILESYSTEM_NAME = RegexTerminal( + "(?!$KNOWN_NAME_WORD)(?:(?:\"\"|'')+(?:(?:$FIRST_PIECE_NO_AT)(?:$WORD_PIECE)*)?|(?:$FIRST_PIECE_NO_AT_NO_TILDE)(?:$WORD_PIECE)*)", + "(?!$KNOWN_NAME_WORD)(?:(?:\"\"|'')+(?:(?:$FIRST_PIECE_NO_AT)(?:$WORD_PIECE)*)?|(?:$FIRST_PIECE_NO_AT_NO_TILDE)(?:$WORD_PIECE)*)", +) + +/** + * A word starting with a raw `~` anywhere except the very start of the value. Only `rvalue[0]` is the + * deny-list marker, so `ext4 ~proc` or `~ ~@bogus` store the literal name `~proc` / `~@bogus`, which never + * matches a file system at unit start. Accepted for the same reason as [ANY_FILESYSTEM_NAME] (#565). + */ +private val TILDE_WORD = RegexTerminal("~(?:$WORD_PIECE)*", "~(?:$WORD_PIECE)*") + +/** A word that does not start with a raw `~`: an unknown name, a known name or `@group`, either possibly quoted. */ +private val ENTRY_NOT_TILDE = AlternativeCombinator( + // First, so the classic engine does not take a known-name prefix (`ext4` of `ext4foo`) and strand the rest. + ANY_FILESYSTEM_NAME, + SequenceCombinator( + LiteralChoiceTerminal("\""), + AlternativeCombinator(SequenceCombinator(LiteralChoiceTerminal("@"), FILESYSTEM_GROUP_NAME), KNOWN_FILESYSTEM_NAME), + LiteralChoiceTerminal("\""), + ), + SequenceCombinator( + LiteralChoiceTerminal("'"), + AlternativeCombinator(SequenceCombinator(LiteralChoiceTerminal("@"), FILESYSTEM_GROUP_NAME), KNOWN_FILESYSTEM_NAME), + LiteralChoiceTerminal("'"), + ), + SequenceCombinator(LiteralChoiceTerminal("@"), FILESYSTEM_GROUP_NAME), + KNOWN_FILESYSTEM_NAME, +) + +/** Any word, including one starting with a literal `~`. */ +private val ENTRY = AlternativeCombinator(TILDE_WORD, ENTRY_NOT_TILDE) + +/** + * Validator for `RestrictFileSystems=`. + * + * An optional leading `~` (deny-list instead of allow-list), then words split on whitespace and commas + * (separators coalesce; stray ones at either end are fine, and `~` alone is an empty deny list). Words are + * unquoted with EXTRACT_UNQUOTE, so quotes and backslash escapes may appear anywhere in a word. + * + * - A word that unquotes to `@group` must be one of filesystem-sets.py's FILESYSTEM_SETS (`@basic-api`, + * `@common-block`, `@network`, `@known`, …). The parser refuses an unknown group ("Unknown filesystem + * group, ignoring"), so it is flagged. The lookup is exact and case-sensitive. + * - Any other word is a file system name. The parser stores it unchecked. An unknown name (`EXT4`, `ext5`, + * `foo`, a literal `~proc` after the first word) is only rejected when the unit starts ("Invalid + * filesystem name, ignoring"), so it is accepted here. Issue #565 tracks a separate inspection that + * warns about values like this. Known names stay a separate alternative for completion and coloring. + * + * Known over-rejection: a word that only becomes `@group` through escaping or partial quoting + * (`\@basic-api`, `'@'basic-api`, `""@basic-api`) is accepted by systemd but flagged here; `"@basic-api"` + * quoted as a whole is accepted. + * + * Covers: Service.RestrictFileSystems, Socket.RestrictFileSystems, Mount.RestrictFileSystems, + * Swap.RestrictFileSystems. + * + * The empty assignment resets the list and never reaches the grammar. + */ +class ConfigParseRestrictFilesystemsOptionValue : SimpleGrammarOptionValues( + "config_parse_restrict_filesystems", + // `~` is a separate alternative rather than ZeroOrOne("~"): the classic engine's ZeroOrOne probes its + // inner combinator a second time and reports that as the furthest point reached, which on `~~x` + // produced a highlight range past the end of the value. + AlternativeCombinator( + // Deny list: `rvalue[0] == '~'`; every following word, even one starting with `~`, is a name. + SequenceCombinator( + LiteralChoiceTerminal("~"), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ZeroOrOne( + SequenceCombinator( + ENTRY, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ) + ), + EOF(), + ), + // Allow list starting with a word: that first word cannot start with a raw `~` (it would be the marker). + SequenceCombinator( + ENTRY_NOT_TILDE, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + EOF(), + ), + // Allow list starting with separators (`,~proc`): every word may start with `~`. + SequenceCombinator( + WHITESPACE_OR_COMMA_SEPARATOR, + ZeroOrOne( + SequenceCombinator( + ENTRY, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ) + ), + EOF(), + ), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSocketBindtodeviceOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSocketBindtodeviceOptionValue.kt new file mode 100644 index 00000000..d5afc0cb --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSocketBindtodeviceOptionValue.kt @@ -0,0 +1,111 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.MatchResult +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NoMatch +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Parse +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ParseStep +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.TerminalCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UNIT_SPECIFIER_LETTERS + +/* + * BindToDevice= ([Socket]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#BindToDevice= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L1097-L1134 config_parse_socket_bindtodevice (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L249 unit_full_printf + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (flags 0: < IFNAMSIZ, not numeric, not ./../all/default) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L769 ifname_valid_char (33..126 except ':' '/' '%') + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L797-L799 parse_ifindex() >= 0 → refused (no IFNAME_VALID_NUMERIC) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L497 safe_atoi (via parse_ifindex: value must be > 0) + * linux https://man7.org/linux/man-pages/man7/socket.7.html (SO_BINDTODEVICE) + * linux https://man7.org/linux/man-pages/man7/netdevice.7.html (IFNAMSIZ) + */ + +// Characters ifname_valid_char accepts. +private const val IFNAME_CHAR = """[\x21-\x7E&&[^:/%]]""" + +// A specifier that can appear inside an interface name. `%%` is left out on purpose: it expands to a +// literal `%`, which ifname_valid_char refuses. +private val IFNAME_SPECIFIER = "%[${UNIT_SPECIFIER_LETTERS.replace("%", "")}]" + +/** + * One interface name, possibly built from specifiers. Without a `%` it must satisfy the same rules as + * [INTERFACE_NAME] (ifname_valid with no flags: 1–15 characters from ifname_valid_char, not all digits, + * not `.`, `..`, `all` or `default`). With specifiers the expanded length and content are unknown, so + * only the literal characters are checked and every `%x` must be a known specifier. + */ +private val IFNAME_WITH_SPECIFIERS = RegexTerminal( + """\S+""", + """(?:${INTERFACE_NAME.semanticMatch.pattern})(?!\S)|(?=\S*%)(?:$IFNAME_CHAR|$IFNAME_SPECIFIER)+(?!\S)""" +) + +/** + * True if parse_ifindex() would take [name] as an interface index: safe_atoi() reads all of it (sign, `0x`, + * `0b`, `0o`, leading-`0` octal, exactly as [SafeAtoTerminal] models it) and the result is a positive `int`. ifname_valid() with no flags + * refuses those, so `0x5`, `+5`, `0b101`, `0o7`, `010` and `0b+1` are invalid, while `-5`, `+0`, `0x0`, + * `+0b1` (no prefix stripping after a sign) and out-of-range `0x80000000` are ordinary names. + * Checked against systemd's own parser (sd-check) at 0f0682b773. + */ +private val IFINDEX = SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = Int.MAX_VALUE.toLong(), + acceptsWhitespace = true, + acceptsMinus = true, +) + +private fun bindToDeviceIsIfindex(name: String): Boolean = IFINDEX.valueOf(name) != null + +/** [IFNAME_WITH_SPECIFIERS], additionally refusing the spellings [bindToDeviceIsIfindex] matches. */ +private class BindToDeviceIfnameTerminal(private val inner: RegexTerminal) : TerminalCombinator { + override fun SyntacticMatch(value: String, offset: Int): MatchResult = inner.SyntacticMatch(value, offset) + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val result = inner.SemanticMatch(value, offset) + if (result.tokens.size == 1 && bindToDeviceIsIfindex(result.tokens[0])) return NoMatch.copy(longestMatch = offset) + return result + } + + override fun parse(value: String, offset: Int): Sequence = inner.parse(value, offset).map { step -> + if (step is Parse) { + step.copy(tokens = step.tokens.map { it.copy(terminal = this, valid = it.valid && !bindToDeviceIsIfindex(it.text)) }) + } else { + step + } + } + + override fun toString(): String = "BindToDeviceIfname($inner)" +} + +/** + * Validator for `BindToDevice=`. + * + * `*` (like the empty value) clears the setting. Anything else is run through unit_full_printf() and + * must then be a valid interface name per ifname_valid() with no flags — so alternative names longer + * than 15 characters are refused, unlike BindNetworkInterface=. An invalid name is "ignoring", so it is + * flagged. Specifiers are allowed (`BindToDevice=%i` in a template socket is a real pattern). + * + * Numeric spellings that parse_ifindex() takes as an index (`0x5`, `+5`, `0b101`) are refused like + * plain all-digit names, see [bindToDeviceIsIfindex]. + * + * Deliberately lenient: a name containing specifiers is not length-checked. + * + * Covers: Socket.BindToDevice. + */ +class ConfigParseSocketBindtodeviceOptionValue : SimpleGrammarOptionValues( + "config_parse_socket_bindtodevice", + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("*"), EOF()), + SequenceCombinator(BindToDeviceIfnameTerminal(IFNAME_WITH_SPECIFIERS), EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallFilterOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallFilterOptionValue.kt new file mode 100644 index 00000000..a8fc03a2 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallFilterOptionValue.kt @@ -0,0 +1,150 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Combinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ERRNO_OR_SECCOMP_ACTION +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SYSCALL_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SYSCALL_OR_GROUP +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WHITESPACE_OR_COMMA_SEPARATOR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * SystemCallFilter= ([Service]/[Socket]/[Mount]/[Swap] and nspawn's [Exec]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#SystemCallFilter= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#SystemCallFilter= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L3290-L3382 config_parse_syscall_filter (0) + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L747-L790 config_parse_syscall_filter (0), nspawn's own function of the same name + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (WHITESPACE ",", no flags) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L2508 parse_syscall_and_errno + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L2688 seccomp_parse_errno_or_action + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L342 parse_errno + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L1293 seccomp_parse_syscall_filter + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L334 syscall_filter_sets[] (group names) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L1035 seccomp_add_syscall_filter_item (nspawn's consumer) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-seccomp.c#L56 TEST(parse_syscall_and_errno) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L729 TEST(parse_errno) + * linux https://man7.org/linux/man-pages/man2/syscalls.2.html + * linux https://man7.org/linux/man-pages/man3/errno.3.html + * + * sd-check at 0f0682b773 (one value per file), a `~` word after the first position: + * .nspawn `@system-service ~@mount`, `@system-service ~@bogus`, `~~@mount`, `@system-service ~@mount:EPERM`: accepted + * (nspawn's parser stores every word unchecked). + * .service `@system-service ~@mount`, `read ~write`, `~~@mount`, `@system-service ~@bogus`: "System call ~@mount is not + * known, ignoring." (the same libseccomp-lookup warning as `madeupcall`); `@system-service ~@mount:EPERM`: + * "Allow-listed system calls cannot take error number"; `~@bogus` (marker + unknown group): "Unknown system + * call group". + */ + +/** `@group` or a bare system call name. */ +private val NAME: Combinator = SYSCALL_OR_GROUP + +/** + * A word starting with `~` after the first position of the value (`@system-service ~@mount`). Only + * `rvalue[0]` is the deny-list marker in both parsers, so this word is taken literally as the name `~@mount`. + * + * - nspawn's parser stores it unchecked; at container start seccomp_add_syscall_filter_item() cannot resolve + * it and drops it with a debug message. That is a runtime-only rejection, so it is accepted here; issue #565 + * tracks a separate warning for it. + * - the unit-file parser hands it to libseccomp, which does not know it ("System call ~@mount is not known, + * ignoring."). That is the same unknown-name warning this grammar already leaves to the shape check for + * `madeupcall`. + * + * The validator registry is keyed only by (parser, ltype), and both files register (config_parse_syscall_filter, + * 0), so the two cannot be told apart; the lenient nspawn behaviour wins. After the `~` the name is not looked + * up as a group by either parser, so any group-shaped word is accepted (`~@bogus`). + */ +private val TILDE_NAME: Combinator = SequenceCombinator( + LiteralChoiceTerminal("~"), + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("@"), RegexTerminal("[^\\s,:]+", "[a-z0-9_-]+")), + SYSCALL_NAME, + ), +) + +/** One allow-list entry after the first position: a name or a literal `~` word, with no `:errno`. */ +private val ALLOW_ENTRY: Combinator = AlternativeCombinator(TILDE_NAME, NAME) + +/** One deny-list entry: a name or a literal `~` word, optionally `:errno`/`:kill`. */ +private val DENY_ENTRY: Combinator = SequenceCombinator( + AlternativeCombinator(TILDE_NAME, NAME), + ZeroOrOne(SequenceCombinator(LiteralChoiceTerminal(":"), ERRNO_OR_SECCOMP_ACTION)), +) + +/** + * Validator for `SystemCallFilter=` — both systemd's (exec context of service/socket/mount/swap units) + * and systemd-nspawn's (`[Exec]` in .nspawn files), which share the (config_parse_syscall_filter, 0) + * pair but are different C functions. + * + * Syntax (core): an optional leading `~` (deny-list instead of allow-list), then a list of entries + * split on whitespace and commas. extract_first_word skips leading separators and coalesces runs, so + * `~ @clock @debug`, `read,write` and a trailing separator are all fine; `~` on its own is an + * (empty) deny list. Each entry is + * + * - `@group` — one of the predefined sets in syscall_filter_sets[] (`@system-service`, `@mount`, …). + * The parser refuses an unknown group ("Unknown system call group, ignoring"), so it is flagged. + * - a system call name, resolved by libseccomp for the native architecture. Unknown names are only + * logged and the set is architecture-dependent, so names are only checked for shape (lower-case + * letters, digits, `_`); `DONOTEXIST` is flagged, `madeupcall` is not. + * - a word starting with `~` after the first position (`@system-service ~@mount`), which both parsers take + * as a literal, never-matching name. nspawn only drops it at container start, so it is accepted (see + * [TILDE_NAME]; issue #565 tracks a warning for it). + * - in deny-list mode only, any of those followed by `:` and an errno name (case-insensitive, + * `EPERM`), a number 0…4095 (decimal, `0x` hex or `0` octal) or `kill`. parse_syscall_and_errno + * rejects an empty name (`:EPERM`), an empty or junk errno (`uname:`, `uname:EINVALaaa`) and an + * out-of-range number (`uname:4096`). Without the `~`, config_parse_syscall_filter refuses any + * errno suffix ("Allow-listed system calls cannot take error number, ignoring"). + * + * nspawn: the same `~` and separators, but each word is stored verbatim and only handed to libseccomp / + * the group table at container start. nspawn does not understand `:errno` at all (`uname:EPERM` is just an + * unresolvable name there). Because the two parsers share one registration, this grammar applies the core + * rules above to nspawn files too, except for the non-first `~` word, where nspawn's lenient behaviour wins. + * + * Covers: Service.SystemCallFilter, Socket.SystemCallFilter, Mount.SystemCallFilter, + * Swap.SystemCallFilter (load-fragment-gperf) and Exec.SystemCallFilter (nspawn-gperf). + * + * The empty assignment resets the filter and never reaches the grammar. + */ +class ConfigParseSyscallFilterOptionValue : SimpleGrammarOptionValues( + "config_parse_syscall_filter", + AlternativeCombinator( + // Deny list: `rvalue[0] == '~'`; a further `~` word (`~~@mount`) is a literal name. + SequenceCombinator( + LiteralChoiceTerminal("~"), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ZeroOrOne( + SequenceCombinator( + DENY_ENTRY, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, DENY_ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ) + ), + EOF(), + ), + // Allow list starting with a word: the first word cannot start with `~` (it would be the marker). + SequenceCombinator( + NAME, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, ALLOW_ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + EOF(), + ), + // Allow list starting with separators (`,~read`): every word may start with `~`. + SequenceCombinator( + WHITESPACE_OR_COMMA_SEPARATOR, + ZeroOrOne( + SequenceCombinator( + ALLOW_ENTRY, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, ALLOW_ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ) + ), + EOF(), + ), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallLogOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallLogOptionValue.kt new file mode 100644 index 00000000..f2010a99 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallLogOptionValue.kt @@ -0,0 +1,62 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SYSCALL_OR_GROUP +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WHITESPACE_OR_COMMA_SEPARATOR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * SystemCallLog= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#SystemCallLog= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L3384-L3454 config_parse_syscall_log (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (WHITESPACE ",", no flags) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L1293 seccomp_parse_syscall_filter (errno_num = -1) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L334 syscall_filter_sets[] (group names) + * linux https://man7.org/linux/man-pages/man2/syscalls.2.html + */ + +/** A possibly-empty whitespace/comma separated list, with stray separators allowed at either end. */ +private val LIST = SequenceCombinator( + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ZeroOrOne( + SequenceCombinator( + SYSCALL_OR_GROUP, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, SYSCALL_OR_GROUP)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ) + ), +) + +/** + * Validator for `SystemCallLog=`. + * + * Same list syntax as `SystemCallFilter=` (see [ConfigParseSyscallFilterOptionValue]): an optional + * leading `~` (log everything *except* the listed calls), then system call names and `@group`s split + * on whitespace and commas, with stray separators tolerated. Unlike SystemCallFilter=, each word goes + * straight to seccomp_parse_syscall_filter() with no parse_syscall_and_errno() step, so there is no + * `:errno` suffix — `uname:EPERM` would be looked up as a system call called `uname:EPERM`, fail to + * resolve, and be dropped with a warning; it is flagged here. + * + * Unknown `@group`s are flagged (warned and ignored by systemd). Bare names are only shape-checked + * (lower-case letters, digits, `_`), because libseccomp resolves them per native architecture. + * + * Covers: Service.SystemCallLog, Socket.SystemCallLog, Mount.SystemCallLog, Swap.SystemCallLog. + * + * The empty assignment resets the list and never reaches the grammar. + */ +class ConfigParseSyscallLogOptionValue : SimpleGrammarOptionValues( + "config_parse_syscall_log", + // `~` is a separate alternative rather than ZeroOrOne("~"): the classic engine's ZeroOrOne probes its + // inner combinator a second time and reports that as the furthest point reached, which on `~~x` + // produced a highlight range past the end of the value. + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("~"), LIST, EOF()), + SequenceCombinator(LIST, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseXattrOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseXattrOptionValue.kt new file mode 100644 index 00000000..fbab9d1d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseXattrOptionValue.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PRINTF_CHAR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.printfCharExcluding + +/* + * XAttrEntryPoint= / XAttrListen= / XAttrAccept= ([Socket]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#XAttrEntryPoint= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L2269-L2327 config_parse_xattr (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-util.c#L1049 split_pair (first "=") + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L249 unit_full_printf (name and value) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/specifier.c#L33 specifier_printf (unknown alphanumeric specifier = -EBADSLT) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-74-AUX-UTILS.socket-xattr.sh#L55-L57 user.varlink=entrypoint / listen / server + * linux https://man7.org/linux/man-pages/man7/xattr.7.html (the user. namespace) + */ + +/** + * Validator for the socket extended-attribute settings, `NAME=VALUE`. + * + * config_parse_xattr() splits at the first `=` (split_pair — no `=` at all is "Failed to parse extended + * attribute expression, ignoring"), expands specifiers in both halves, and then requires the expanded + * name to start with `user.` (the only xattr namespace an unprivileged socket inode can carry, see + * xattr(7)). Everything after the first `=` is the value and may contain further `=` or be empty. + * + * The only other failure is specifier expansion: an unknown alphanumeric `%x` is an error, while `%%`, + * a known specifier, or `%` before a non-alphanumeric character is fine ([PRINTF_CHAR]). + * + * Deliberately strict on one corner: the name must start with a literal `user.`. A name that only + * becomes `user.…` after specifier expansion is theoretically possible but not realistic. + * + * Covers: Socket.XAttrEntryPoint, Socket.XAttrListen, Socket.XAttrAccept. + * + * The empty assignment resets the list and never reaches the grammar. + */ +class ConfigParseXattrOptionValue : SimpleGrammarOptionValues( + "config_parse_xattr", + SequenceCombinator( + LiteralChoiceTerminal("user."), + RegexTerminal("[^=]*", "${printfCharExcluding("=")}*"), + LiteralChoiceTerminal("="), + RegexTerminal("[\\s\\S]*", "$PRINTF_CHAR*"), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchSeccompBpfCgroupCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchSeccompBpfCgroupCombinators.kt new file mode 100644 index 00000000..64b9fb21 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchSeccompBpfCgroupCombinators.kt @@ -0,0 +1,209 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +/* + * Shared terminals for the seccomp / BPF / cgroup validators (batch 2026-10). + * + * Everything here is pinned to systemd 0f0682b773, the commit in systemd-build/build/last_commit_hash + * that the plugin's gperf and man data are generated from. Browse it at + * https://github.com/systemd/systemd/blob/0f0682b773. + * + * These live in their own file rather than Combinators.kt because several validator families were + * written in parallel; the generally useful ones (ERRNO_OR_SECCOMP_ACTION, PRINTF_CHAR, the + * whitespace-or-comma separator) are candidates for promotion. + */ + +/** + * One run of separators for a list split with `extract_first_word(&p, &word, WHITESPACE ",", …)`. + * + * extract_first_word skips any leading separators and coalesces runs of them (unless + * EXTRACT_DONT_COALESCE_SEPARATORS is passed, which none of these callers do), so `a, b`, `a,,b` and + * `a b` all split into the same two words. + * + * https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 + */ +val WHITESPACE_OR_COMMA_SEPARATOR = RegexTerminal("[\\s,]+", "[\\s,]+") + +// --------------------------------------------------------------------------------------------------- +// errno names / numbers — parse_errno (src/basic/parse-util.c#L342) and +// seccomp_parse_errno_or_action (src/shared/seccomp-util.c#L2688). + +/** + * The errno names errno_from_name() knows: the `#define E…` macros of ``, as listed by + * src/basic/generate-errno-list.sh (which drops the parisc-only aliases ECANCELLED, EREFUSED, + * EFSBADCRC and EFSCORRUPTED). Regenerate with + * + * echo | gcc -dM -include errno.h -E - | grep -Ev '^#define[[:space:]]+(ECANCELLED|EREFUSED|EFSBADCRC|EFSCORRUPTED)' \ + * | awk '/^#define[ \t]+E[^ _]+[ \t]+/ { print $2; }' | sort + * + * The lookup table is built by gperf with `--ignore-case` (src/basic/meson.build#L168), so `eperm` + * resolves as well as `EPERM`. + */ +val ERRNO_NAMES = arrayOf( + "E2BIG", "EACCES", "EADDRINUSE", "EADDRNOTAVAIL", "EADV", "EAFNOSUPPORT", "EAGAIN", "EALREADY", "EBADE", "EBADF", + "EBADFD", "EBADMSG", "EBADR", "EBADRQC", "EBADSLT", "EBFONT", "EBUSY", "ECANCELED", "ECHILD", "ECHRNG", + "ECOMM", "ECONNABORTED", "ECONNREFUSED", "ECONNRESET", "EDEADLK", "EDEADLOCK", "EDESTADDRREQ", "EDOM", "EDOTDOT", "EDQUOT", + "EEXIST", "EFAULT", "EFBIG", "EHOSTDOWN", "EHOSTUNREACH", "EHWPOISON", "EIDRM", "EILSEQ", "EINPROGRESS", "EINTR", + "EINVAL", "EIO", "EISCONN", "EISDIR", "EISNAM", "EKEYEXPIRED", "EKEYREJECTED", "EKEYREVOKED", "EL2HLT", "EL2NSYNC", + "EL3HLT", "EL3RST", "ELIBACC", "ELIBBAD", "ELIBEXEC", "ELIBMAX", "ELIBSCN", "ELNRNG", "ELOOP", "EMEDIUMTYPE", + "EMFILE", "EMLINK", "EMSGSIZE", "EMULTIHOP", "ENAMETOOLONG", "ENAVAIL", "ENETDOWN", "ENETRESET", "ENETUNREACH", "ENFILE", + "ENOANO", "ENOBUFS", "ENOCSI", "ENODATA", "ENODEV", "ENOENT", "ENOEXEC", "ENOKEY", "ENOLCK", "ENOLINK", + "ENOMEDIUM", "ENOMEM", "ENOMSG", "ENONET", "ENOPKG", "ENOPROTOOPT", "ENOSPC", "ENOSR", "ENOSTR", "ENOSYS", + "ENOTBLK", "ENOTCONN", "ENOTDIR", "ENOTEMPTY", "ENOTNAM", "ENOTRECOVERABLE", "ENOTSOCK", "ENOTSUP", "ENOTTY", "ENOTUNIQ", + "ENXIO", "EOPNOTSUPP", "EOVERFLOW", "EOWNERDEAD", "EPERM", "EPFNOSUPPORT", "EPIPE", "EPROTO", "EPROTONOSUPPORT", "EPROTOTYPE", + "ERANGE", "EREMCHG", "EREMOTE", "EREMOTEIO", "ERESTART", "ERFKILL", "EROFS", "ESHUTDOWN", "ESOCKTNOSUPPORT", "ESPIPE", + "ESRCH", "ESRMNT", "ESTALE", "ESTRPIPE", "ETIME", "ETIMEDOUT", "ETOOMANYREFS", "ETXTBSY", "EUCLEAN", "EUNATCH", + "EUSERS", "EWOULDBLOCK", "EXDEV", "EXFULL", +) + +/** An errno name as errno_from_name() resolves it (case-insensitively). */ +val ERRNO_NAME = FlexibleLiteralChoiceTerminal(*ERRNO_NAMES, ignoreCase = true) + +/** + * The argument of a `syscall:ERRNO` entry: `kill`, an errno name, or a number. + * seccomp_parse_errno_or_action() special-cases `kill` (exact, lower case) and otherwise calls + * parse_errno(), which takes a name first and then falls back to safe_atoi() (sign, `0x`, `0b`, `0o`, + * leading-`0` octal, see [SafeAtoTerminal]), accepting 0 and 1…ERRNO_MAX (4095), so `-0` is fine but `-3` + * is not. + * + * The entry is a word from a whitespace split, so whitespace never reaches safe_atoi() here + * (`acceptsWhitespace = false`; sd-check rejects `SystemCallFilter=~uname:0b 1` because `0b` is a word of its + * own). A malformed number such as `08` is a syntax error located where strtol() stopped. + * + * The number comes before the name because ERRNO_NAME's lenient shape includes digits (E2BIG): under + * the classic engine, with the name first, `0x1F` would be taken as the one-character name `0` and the + * rest stranded. + */ +val ERRNO_OR_SECCOMP_ACTION = AlternativeCombinator( + LiteralChoiceTerminal("kill"), + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4095L, + acceptsWhitespace = false, + acceptsMinus = true, + ), + ERRNO_NAME, +) + +// --------------------------------------------------------------------------------------------------- +// System call names and groups — seccomp_parse_syscall_filter (src/shared/seccomp-util.c#L1293). + +/** + * The predefined system call groups, without their leading `@` (syscall_filter_sets[], + * src/shared/seccomp-util.c#L334; looked up by exact string compare in syscall_filter_set_find, + * #L1016). An unknown group is logged ("Unknown system call group, ignoring") and dropped. + */ +val SYSCALL_GROUP_NAME = FlexibleLiteralChoiceTerminal( + "default", "aio", "basic-io", "chown", "clock", "cpu-emulation", "debug", "file-system", "io-event", "ipc", + "keyring", "memlock", "module", "mount", "network-io", "obsolete", "pkey", "privileged", "process", "raw-io", + "reboot", "resources", "sandbox", "setuid", "signal", "swap", "sync", "system-service", "timer", "known", +) + +/** + * A bare system call name. These are resolved at runtime by libseccomp's + * seccomp_syscall_resolve_name() for the *native* architecture, so whether e.g. `arm_fadvise64_64` is + * "known" depends on the machine; an unknown name is logged and ignored. The set is therefore not + * enumerated here: the only check is the shape every libseccomp name has (lower case letters, digits + * and underscores — `_llseek`, `epoll_wait`, `s390_pci_mmio_read`). The lenient syntactic side takes + * anything up to a separator or `:`, so `DONOTEXIST` is located and flagged as a single token. It does not + * start at `@` or `~`, which belong to the `@group` and literal-`~` alternatives: otherwise each such word + * would also get a second (invalid) parse here, and a long invalid list would explode into 2^n parses on the + * new engine. + */ +val SYSCALL_NAME = RegexTerminal("[^\\s,:@~][^\\s,:]*", "[a-z_][a-z0-9_]*") + +/** `@group` or a bare system call name. */ +val SYSCALL_OR_GROUP = AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("@"), SYSCALL_GROUP_NAME), + SYSCALL_NAME, +) + +// --------------------------------------------------------------------------------------------------- +// File system names — bpf_restrict_fs_parse_filesystem (src/core/bpf-restrict-fs.c#L212). + +/** + * The file system names fs_type_from_string() resolves: the keys of NAME_TO_MAGIC in + * src/basic/filesystem-sets.py, compiled into a case-sensitive gperf table. A name outside this set is + * accepted by the unit-file parser and only logged and skipped when the unit starts and the BPF map is + * filled ("bpf-restrict-fs: Invalid filesystem name '%s', ignoring.", src/core/bpf-restrict-fs.c#L117-L121). + * Validators must therefore accept such names too (RestrictFileSystems= uses these choices as exact + * literals next to a lenient word); issue #565 tracks a separate warning for them. The list is kept for + * completion and coloring. + */ +val FILESYSTEM_NAME = FlexibleLiteralChoiceTerminal( + "apparmorfs", "adfs", "affs", "afs", "anon_inodefs", "autofs", "balloon-kvm", "bcachefs", "bdev", "binder", + "binfmt_misc", "bpf", "btrfs", "btrfs_test_fs", "cpuset", "ceph", "cgroup2", "cgroup", "cifs", "coda", + "configfs", "cramfs", "dax", "debugfs", "devmem", "devpts", "devtmpfs", "dmabuf", "ecryptfs", "efivarfs", + "efs", "erofs", "ext2", "ext3", "ext4", "exfat", "f2fs", "failfs", "fuseblk", "fuse", + "fusectl", "gfs", "gfs2", "gmem", "hostfs", "hpfs", "hugetlbfs", "iso9660", "jffs2", "minix", + "mqueue", "msdos", "ncp", "ncpfs", "nfs", "nfs4", "nilfs2", "nsfs", "ntfs", "ntfs3", + "nullfs", "ocfs2", "openpromfs", "orangefs", "overlay", "pidfs", "pipefs", "ppc-cmm", "proc", "pstore", + "pvfs2", "qnx4", "qnx6", "ramfs", "resctrl", "reiserfs", "rpc_pipefs", "secretmem", "securityfs", "selinuxfs", + "shiftfs", "smackfs", "smb3", "smbfs", "sockfs", "squashfs", "sysfs", "tmpfs", "tracefs", "udf", + "usbdevfs", "vboxsf", "vfat", "v9fs", "xenfs", "xfs", "z3fold", "zonefs", "zsmalloc", +) + +/** + * The file system groups, without the leading `@` (FILESYSTEM_SETS in src/basic/filesystem-sets.py, + * looked up by exact compare in filesystem_set_find, src/basic/filesystems.c#L21). The unit-file parser + * refuses an unknown group ("Unknown filesystem group, ignoring"), so validators flag it. + */ +val FILESYSTEM_GROUP_NAME = FlexibleLiteralChoiceTerminal( + "basic-api", "anonymous", "application", "auxiliary-api", "common-block", "historical-block", "network", + "privileged-api", "security", "temporary", "known", +) + +// --------------------------------------------------------------------------------------------------- +// Specifier expansion — unit_full_printf_full (src/core/unit-printf.c#L179). + +/** + * The specifier letters unit_full_printf() knows (src/core/unit-printf.c#L210-L238 plus + * COMMON_SYSTEM/CREDS/TMP_SPECIFIERS from src/shared/specifier.h#L76-L99), and `%` for a literal + * percent sign. Same set as ConfigParseUnitStringPrintfOptionValue uses. + */ +const val UNIT_SPECIFIER_LETTERS = "iIjJnNpPfyYcrRCdDELSthsaAbBHlqmMovwWgGuUTV%" + +/** + * A regex fragment for one character (or `%x` pair) of specifier-expanded text, per specifier_printf + * (src/shared/specifier.c#L33): a known specifier expands; an unknown *alphanumeric* one is an error + * (-EBADSLT, the assignment is dropped); `%` followed by anything else — or at the very end — is kept + * literally. Use inside a character-class-free regex, e.g. `(?:$PRINTF_CHAR)*`. + */ +const val PRINTF_CHAR = "(?:[^%]|%[$UNIT_SPECIFIER_LETTERS]|%(?![A-Za-z0-9]))" + +/** [PRINTF_CHAR], but also excluding [excluded] (a character-class body, e.g. `=` or `\\s`) as a plain character. */ +fun printfCharExcluding(excluded: String): String = "(?:[^%$excluded]|%[$UNIT_SPECIFIER_LETTERS]|%(?![A-Za-z0-9]))" + +// --------------------------------------------------------------------------------------------------- +// BPF token delegation lists — bpf_delegate_from_string (src/core/namespace.c#L4221). + +/** + * The value grammar shared by BPFDelegateCommands= / BPFDelegateMaps= / BPFDelegatePrograms= / + * BPFDelegateAttachments=: either the whole value `any`, or a comma-separated list of names from + * [names]. + * + * bpf_delegate_from_string() checks `streq(s, "any")` against the whole value, then splits on `,` + * *only* — no whitespace — with extract_first_word(..., ",", 0), which skips leading commas and + * coalesces runs of them. So `A, B` makes a word ` B` that is not in the table ("Unknown BPF delegate + * option, ignoring") and is flagged here, while `A,,B` and a trailing comma are fine. Each word is + * looked up with DEFINE_STRING_TABLE_LOOKUP, i.e. an exact, case-sensitive streq. + */ +fun bpfDelegateList(names: FlexibleLiteralChoiceTerminal): Combinator { + val commas = RegexTerminal(",+", ",+") + return AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("any"), EOF()), + SequenceCombinator( + ZeroOrOne(commas), + ZeroOrOne( + SequenceCombinator( + names, + ZeroOrMore(SequenceCombinator(commas, names)), + ZeroOrOne(commas), + ) + ), + EOF(), + ), + ) +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateAttachmentsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateAttachmentsOptionValueTest.kt new file mode 100644 index 00000000..f149d101 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateAttachmentsOptionValueTest.kt @@ -0,0 +1,73 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBpfDelegateAttachmentsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first value is from systemd's test/units/TEST-07-PID1.private-bpf.sh (L49-L52). + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateAttachments=BPFFlowDissector,BPFCgroupSysctl,BPFNetfilter + BPFDelegateAttachments=BPFCgroupInetIngress + BPFDelegateAttachments=any + BPFDelegateAttachments=BPFCgroupInetIngress,BPFCgroupInetEgress,BPFCgroupInetSockCreate + BPFDelegateAttachments=BPFTraceFsessionMulti + BPFDelegateAttachments=BPFLircMode2 + BPFDelegateAttachments=BPFCgroupInet4Bind,BPFCgroupInet6Bind + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + BPFDelegateAttachments=any + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + BPFDelegateAttachments=BPFCgroupInetIngress + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + BPFDelegateAttachments=BPFTraceFsessionMulti,BPFCgroupInetIngress + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateAttachments=BPFCgroupInetSkbIngress + BPFDelegateAttachments=BPFFlowDissector, BPFNetfilter + BPFDelegateAttachments=BPF_NETFILTER + BPFDelegateAttachments=ingress + BPFDelegateAttachments=BPFTcxIngress BPFTcxEgress + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateCommandsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateCommandsOptionValueTest.kt new file mode 100644 index 00000000..be3529fa --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateCommandsOptionValueTest.kt @@ -0,0 +1,73 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBpfDelegateCommandsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first value is from systemd's test/units/TEST-07-PID1.private-bpf.sh (L49-L52). + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateCommands=BPFObjPin,BPFBtfLoad,BPFMapFreeze,BPFLinkDetach + BPFDelegateCommands=BPFProgLoad + BPFDelegateCommands=any + BPFDelegateCommands=BPFMapCreate,BPFMapLookupElem,BPFMapUpdateElem + BPFDelegateCommands=BPFMapCreate + BPFDelegateCommands=BPFProgAssocStructOps + BPFDelegateCommands=BPFProgLoad,BPFMapCreate,,BPFLinkCreate, + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + BPFDelegateCommands=any + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + BPFDelegateCommands=BPFMapCreate + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + BPFDelegateCommands=BPFProgAssocStructOps,BPFMapCreate + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateCommands=BPF_PROG_LOAD + BPFDelegateCommands=BPFProgload + BPFDelegateCommands=BPFProgLoad, BPFMapCreate + BPFDelegateCommands=BPFProgLoad BPFMapCreate + BPFDelegateCommands=any,BPFProgLoad + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateMapsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateMapsOptionValueTest.kt new file mode 100644 index 00000000..43b02250 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateMapsOptionValueTest.kt @@ -0,0 +1,73 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBpfDelegateMapsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first value is from systemd's test/units/TEST-07-PID1.private-bpf.sh (L49-L52). + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateMaps=BPFMapTypeArray,BPFMapTypeCpumap,BPFMapTypeRingbuf + BPFDelegateMaps=BPFMapTypeHash + BPFDelegateMaps=any + BPFDelegateMaps=BPFMapTypeUnspec,BPFMapTypeHash,BPFMapTypeArray + BPFDelegateMaps=BPFMapTypeUnspec + BPFDelegateMaps=BPFMapTypeCgroupStorageDeprecated + BPFDelegateMaps=BPFMapTypeRhash + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + BPFDelegateMaps=any + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + BPFDelegateMaps=BPFMapTypeUnspec + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + BPFDelegateMaps=BPFMapTypeRhash,BPFMapTypeUnspec + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateMaps=BPFMapTypeCgroupStorage + BPFDelegateMaps=BPFMapTypeArray, BPFMapTypeHash + BPFDelegateMaps=BPF_MAP_TYPE_ARRAY + BPFDelegateMaps=Array + BPFDelegateMaps=Any + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateProgramsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateProgramsOptionValueTest.kt new file mode 100644 index 00000000..60ef19dd --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateProgramsOptionValueTest.kt @@ -0,0 +1,73 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBpfDelegateProgramsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first value is from systemd's test/units/TEST-07-PID1.private-bpf.sh (L49-L52). + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegatePrograms=BPFProgTypeTracepoint,BPFProgTypeXdp,BPFProgTypeTracing + BPFDelegatePrograms=BPFProgTypeKprobe + BPFDelegatePrograms=any + BPFDelegatePrograms=BPFProgTypeUnspec,BPFProgTypeSocketFilter,BPFProgTypeKprobe + BPFDelegatePrograms=BPFProgTypeUnspec + BPFDelegatePrograms=BPFProgTypeLwtSeg6local + BPFDelegatePrograms=BPFProgTypeNetfilter + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + BPFDelegatePrograms=any + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + BPFDelegatePrograms=BPFProgTypeUnspec + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + BPFDelegatePrograms=BPFProgTypeNetfilter,BPFProgTypeUnspec + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegatePrograms=BPFProgTypeXDP + BPFDelegatePrograms=BPFProgTypeXdp BPFProgTypeTracing + BPFDelegatePrograms=BPFProgTypeLwtSeg6Local + BPFDelegatePrograms=tracepoint + BPFDelegatePrograms=BPFProgTypeSyscall + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfForeignProgramOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfForeignProgramOptionValueTest.kt new file mode 100644 index 00000000..fee4e7cf --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfForeignProgramOptionValueTest.kt @@ -0,0 +1,102 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBpfForeignProgramOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first two are the systemd.resource-control(5) examples. + // language="unit file (systemd)" + val file = """ + [Service] + BPFProgram=egress:/sys/fs/bpf/egress-hook + BPFProgram=bind6:/sys/fs/bpf/sock-addr-hook + BPFProgram=ingress:/sys/fs/bpf/ingress + BPFProgram=sock_create:/sys/fs/bpf/a + BPFProgram=sock_ops:/sys/fs/bpf/a + BPFProgram=device:/sys/fs/bpf/a + BPFProgram=bind4:/sys/fs/bpf/a + BPFProgram=connect4:/sys/fs/bpf/a + BPFProgram=connect6:/sys/fs/bpf/a + BPFProgram=post_bind4:/sys/fs/bpf/a + BPFProgram=post_bind6:/sys/fs/bpf/a + BPFProgram=sendmsg4:/sys/fs/bpf/a + BPFProgram=sendmsg6:/sys/fs/bpf/a + BPFProgram=sysctl:/sys/fs/bpf/a + BPFProgram=recvmsg4:/sys/fs/bpf/a + BPFProgram=recvmsg6:/sys/fs/bpf/a + BPFProgram=getsockopt:/sys/fs/bpf/a + BPFProgram=setsockopt:/sys/fs/bpf/a + BPFProgram=egress:/sys/fs/bpf/%N-egress + BPFProgram=egress:%t/bpf/prog + BPFProgram=egress::/sys/fs/bpf/double-colon + BPFProgram=egress:/sys/fs/bpf/with:colon + BPFProgram=:egress:/sys/fs/bpf/leading-colon + BPFProgram=::ingress::/sys/fs/bpf/x + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + BPFProgram=egress:/sys/fs/bpf/egress-hook + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + BPFProgram=ingress:/sys/fs/bpf/x + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + BPFProgram=device:/sys/fs/bpf/x + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val slice = """ + [Slice] + BPFProgram=sock_create:/sys/fs/bpf/x + """.trimIndent() + setupFileInEditor("file.slice", slice) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BPFProgram=/usr/lib/systemd/bpf/custom.bpf + BPFProgram=egress + BPFProgram=egress: + BPFProgram=xdp:/sys/fs/bpf/x + BPFProgram=Egress:/sys/fs/bpf/x + BPFProgram=egress:sys/fs/bpf/x + BPFProgram=egress:/sys/fs/bpf/../x + BPFProgram=cgroup_inet_egress:/sys/fs/bpf/x + BPFProgram=:/sys/fs/bpf/x + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(9, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCgroupNftSetOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCgroupNftSetOptionValueTest.kt new file mode 100644 index 00000000..6eaacd02 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCgroupNftSetOptionValueTest.kt @@ -0,0 +1,90 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseCgroupNftSetOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first two are from systemd's test/units/TEST-07-PID1.nft.sh and the man page example. + // language="unit file (systemd)" + val file = """ + [Service] + NFTSet=cgroup:inet:sd_test:c user:inet:sd_test:u group:inet:sd_test:g + NFTSet=cgroup:inet:filter:my_service user:inet:filter:serviceuser + NFTSet=cgroup:ip:t:s + NFTSet=user:ip6:my.table:set/v6 + NFTSet=group:arp:a:b group:bridge:a:b group:netdev:a:b + NFTSet="cgroup:inet:filter:quoted" + NFTSet=cgroup::inet:t:s + NFTSet=:cgroup:inet:t:s + NFTSet=cgroup:inet:t:s: + NFTSet=::cgroup:inet:t:s:: user:inet:t:u: + NFTSet="cgroup:inet:t:s:" + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + NFTSet=cgroup:inet:sd_test:c + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + NFTSet=user:inet:sd_test:u + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + NFTSet=group:inet:sd_test:g + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val slice = """ + [Slice] + NFTSet=cgroup:inet:sd_test:c + """.trimIndent() + setupFileInEditor("file.slice", slice) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + NFTSet=address:inet:sd_test:addr4 + NFTSet=cgroup:ipv4:t:s + NFTSet=cgroup:inet:1table:s + NFTSet=cgroup:inet:t + NFTSet=cgroup:inet:t:s:extra + NFTSet=inet:filter:my_service + NFTSet=cgroup:inet:t:s,user:inet:t:u + NFTSet=cgroup:inet:t-x:s + NFTSet=CGROUP:inet:t:s + NFTSet=cgroup:inet:t: + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(10, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrivateBpfOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrivateBpfOptionValueTest.kt new file mode 100644 index 00000000..29e94048 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrivateBpfOptionValueTest.kt @@ -0,0 +1,83 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParsePrivateBpfOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + PrivateBPF=yes + PrivateBPF=no + PrivateBPF=true + PrivateBPF=false + PrivateBPF=on + PrivateBPF=off + PrivateBPF=1 + PrivateBPF=0 + PrivateBPF=y + PrivateBPF=n + PrivateBPF=t + PrivateBPF=f + PrivateBPF=Yes + PrivateBPF=TRUE + PrivateBPF=oFf + PrivateBPF=fAlSe + PrivateBPF=Y + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + PrivateBPF=yes + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + PrivateBPF=no + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + PrivateBPF=off + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + PrivateBPF=maybe + PrivateBPF=2 + PrivateBPF=private + PrivateBPF=yes no + PrivateBPF=Auto + PrivateBPF=Private + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRestrictFilesystemsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRestrictFilesystemsOptionValueTest.kt new file mode 100644 index 00000000..d7d7f5af --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRestrictFilesystemsOptionValueTest.kt @@ -0,0 +1,128 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseRestrictFilesystemsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first six are the man page examples and TEST-07-PID1.exec-context.sh values. + // language="unit file (systemd)" + val file = """ + [Service] + RestrictFileSystems=ext4 tmpfs + RestrictFileSystems=ext2 ext4 + RestrictFileSystems=~ext4 tmpfs + RestrictFileSystems=~tmpfs,ext4 + RestrictFileSystems=~@basic-api + RestrictFileSystems=~proc devtmpfs sysfs + RestrictFileSystems=~ + RestrictFileSystems=@common-block @network @known + RestrictFileSystems=btrfs_test_fs balloon-kvm ppc-cmm cgroup2 cgroup nfs4 nfs + RestrictFileSystems="ext4" 'xfs' + RestrictFileSystems=~ fuse, fuseblk, + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + RestrictFileSystems=@temporary + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + RestrictFileSystems=~@privileged-api + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + RestrictFileSystems=@security @application + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testRuntimeOnlyRejectionsAreAccepted() { + // sd-check at 0f0682b773 accepts every one of these (one value per probe file). The unit-file parser + // stores any word that does not unquote to `@group` unchecked; unknown names are only logged and + // skipped when the unit starts ("bpf-restrict-fs: Invalid filesystem name '…', ignoring.", + // src/core/bpf-restrict-fs.c#L117-L121). Issue #565 tracks a separate warning for them. The kf6 + // autotest values ("invalid", "temporary", "@auxiliary-api btrfs nfs @temporary unknown") are here. + // A `~` word after the first position is the literal name `~proc` / `~@foo`, and quotes and + // backslash escapes are removed anywhere in a word (EXTRACT_UNQUOTE). + // language="unit file (systemd)" + val file = """ + [Service] + RestrictFileSystems=~proc,sysfs,foo + RestrictFileSystems=EXT4 + RestrictFileSystems=ext5 + RestrictFileSystems=invalid + RestrictFileSystems=temporary + RestrictFileSystems=@auxiliary-api btrfs nfs @temporary unknown + RestrictFileSystems=ext4foo nfs4x foo.bar! + RestrictFileSystems=ext4 ~proc + RestrictFileSystems=ext4 ~@foo + RestrictFileSystems=ext4 ~ + RestrictFileSystems=~ ~@bogus + RestrictFileSystems=~~proc + RestrictFileSystems="~@bogus" + RestrictFileSystems=\~proc + RestrictFileSystems=ext"4" + RestrictFileSystems=ext4"" + RestrictFileSystems="" + RestrictFileSystems="a\"b" + RestrictFileSystems='a\'b' + RestrictFileSystems="@basic-api" '@network' + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // "@foo" and "@bar" are TEST-07's deliberately unknown groups. The parser refuses unknown groups + // ("bpf-restrict-fs: Unknown filesystem group, ignoring: …", sd-check at 0f0682b773), also when + // the word only unquotes to `@…` ("@foo", ""@foo, \@foo) and when the `~` marker comes first. + // An unterminated quote is "Trailing garbage in RestrictFileSystems". + // language="unit file (systemd)" + val file = """ + [Service] + RestrictFileSystems=ext4 @foo @basic-api + RestrictFileSystems=~proc foo @bar @basic-api + RestrictFileSystems=@basic_api + RestrictFileSystems=@unknowngroup + RestrictFileSystems=@ + RestrictFileSystems=@Basic-api + RestrictFileSystems=~@bogus + RestrictFileSystems="@foo" + RestrictFileSystems=""@foo + RestrictFileSystems=\@foo + RestrictFileSystems="@basic-api"x + RestrictFileSystems=ext"4 + RestrictFileSystems=ext4 "unterminated + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(13, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketBindtodeviceOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketBindtodeviceOptionValueTest.kt new file mode 100644 index 00000000..385c1fb3 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketBindtodeviceOptionValueTest.kt @@ -0,0 +1,83 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseSocketBindtodeviceOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // eth0/lo/wlan0 are from the Debian corpus and systemd's tests. The numeric-looking names after `**` + // are ones parse_ifindex() does not take as an index, so systemd accepts them (sd-check at 0f0682b773). + // language="unit file (systemd)" + val file = """ + [Socket] + BindToDevice=eth0 + BindToDevice=lo + BindToDevice=wlan0 + BindToDevice=* + BindToDevice=enp0s31f6 + BindToDevice=br-lan.100 + BindToDevice=abcdefghijklmno + BindToDevice=%i + BindToDevice=vrf-%i + BindToDevice=1a + BindToDevice=alliance + BindToDevice=** + BindToDevice=-5 + BindToDevice=+0 + BindToDevice=0x0 + BindToDevice=+0b1 + BindToDevice=0x+5 + BindToDevice=0x80000000 + BindToDevice=+2147483648 + BindToDevice=0b2 + BindToDevice=0xg + BindToDevice=00x5 + BindToDevice=0b-0 + BindToDevice=-0x0 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // 08 is all digits, which ifname_valid() refuses outright. + // 0x5 … 0o+7: parse_ifindex() reads them as an ifindex, which ifname_valid() refuses + // ("Invalid interface name, ignoring", sd-check at 0f0682b773). + // language="unit file (systemd)" + val file = """ + [Socket] + BindToDevice=abcdefghijklmnop + BindToDevice=eth0:1 + BindToDevice=eth/0 + BindToDevice=all + BindToDevice=default + BindToDevice=42 + BindToDevice=.. + BindToDevice=eth0 eth1 + BindToDevice=%z + BindToDevice=100%% + BindToDevice=0x5 + BindToDevice=0X5 + BindToDevice=+5 + BindToDevice=+0x5 + BindToDevice=0b101 + BindToDevice=0o7 + BindToDevice=0b+1 + BindToDevice=010 + BindToDevice=+010 + BindToDevice=0x7fffffff + BindToDevice=0o+7 + BindToDevice=08 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(22, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallFilterOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallFilterOptionValueTest.kt new file mode 100644 index 00000000..adb46fc6 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallFilterOptionValueTest.kt @@ -0,0 +1,176 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseSyscallFilterOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidAllowLists() { + // Real values from the Debian corpus and systemd's own units/tests. + // language="unit file (systemd)" + val file = """ + [Service] + SystemCallFilter=@system-service + SystemCallFilter=@system-service @mount + SystemCallFilter=@system-service @module @raw-io bpf + SystemCallFilter=@default @file-system @basic-io @system-service + SystemCallFilter=read write open execve + SystemCallFilter=ptrace,reboot swapoff + SystemCallFilter=read, write + SystemCallFilter=_llseek _newselect epoll_wait s390_pci_mmio_read + SystemCallFilter=@known + SystemCallFilter=,read, + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testUnknownSyscallNamesAreAcceptedDeliberately() { + // systemd warns "System call invalid is not known, ignoring." for these (sd-check at 0f0682b773), + // but whether a name resolves depends on libseccomp and the native architecture, so only the + // shape of a syscall name is checked. Documented leniency. + // language="unit file (systemd)" + val file = """ + [Service] + SystemCallFilter=invalid + SystemCallFilter=madeupcall + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidDenyListsAndErrno() { + // ~uname:EILSEQ, :kill, :255 come from systemd's test units; :4095/:0 from test-seccomp.c. + // language="unit file (systemd)" + val file = """ + [Socket] + SystemCallFilter=~@mount + SystemCallFilter=~ @clock @debug @module @mount @raw-io @reboot @swap @cpu-emulation @obsolete + SystemCallFilter=~@privileged @resources + SystemCallFilter=~uname:EILSEQ + SystemCallFilter=~uname:ENOENT uname:EILSEQ + SystemCallFilter=~uname:kill + SystemCallFilter=~uname:255 + SystemCallFilter=~@sync:4095 + SystemCallFilter=~uname:0 + SystemCallFilter=~uname:0x1F + SystemCallFilter=~uname:eperm + SystemCallFilter=~uname:ePerm uname:Eilseq + SystemCallFilter=~uname:0777 uname:0b11 uname:0o17 uname:+5 uname:-0 uname:+0x10 uname:0b+1 uname:0o+7 uname:0B-0 uname:-0x0 uname:0o-0 + SystemCallFilter=~@sync:EPERM,@mount:kill uname:E2BIG + SystemCallFilter=~ + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testValidOtherUnitTypesAndNspawn() { + // language="unit file (systemd)" + val mount = """ + [Mount] + SystemCallFilter=@system-service + """.trimIndent() + setupFileInEditor("file.mount", mount) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + SystemCallFilter=~@swap + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val nspawn = """ + [Exec] + SystemCallFilter=@system-service ~@mount + SystemCallFilter=~@clock add_key + """.trimIndent() + setupFileInEditor("file.nspawn", nspawn) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testTildeWordAfterTheFirstPositionIsAccepted() { + // nspawn only honours `~` as the first character, so "~@mount" below is stored verbatim as a name. + // sd-check at 0f0682b773 accepts all of these in a .nspawn file (nspawn's parser stores words + // unchecked); the word is only dropped at container start by seccomp_add_syscall_filter_item(), a + // runtime-only rejection that issue #565 will warn about. + // language="unit file (systemd)" + val nspawn = """ + [Exec] + SystemCallFilter=@system-service ~@mount + SystemCallFilter=@system-service ~@bogus + SystemCallFilter=~~@mount + SystemCallFilter=,~read + """.trimIndent() + setupFileInEditor("file.nspawn", nspawn) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // In a unit file sd-check says "System call ~@mount is not known, ignoring." for the same words: the + // libseccomp lookup warning that this grammar already leaves unflagged for names like "madeupcall". + // Both files share the (config_parse_syscall_filter, 0) registration, so the lenient nspawn + // behaviour applies here as well. Documented leniency. + // language="unit file (systemd)" + val service = """ + [Service] + SystemCallFilter=@system-service ~@mount + SystemCallFilter=read ~write + SystemCallFilter=~~@mount + SystemCallFilter=~read ~write:EPERM + """.trimIndent() + setupFileInEditor("file.service", service) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + SystemCallFilter=@bogus-group + SystemCallFilter=~write open execve DONOTEXIST + SystemCallFilter=uname:EILSEQ + SystemCallFilter=~uname: + SystemCallFilter=~:255 + SystemCallFilter=~uname:4096 + SystemCallFilter=~uname:-3 + SystemCallFilter=~uname:12.3 + SystemCallFilter=~uname:EINVALaaa + SystemCallFilter=~uname:255:EILSEQ + SystemCallFilter=~uname:KILL + SystemCallFilter=~uname:Kill + SystemCallFilter=@service + SystemCallFilter=~uname:0b-1 + SystemCallFilter=~uname:+0b1 + SystemCallFilter=~uname:08 + SystemCallFilter=~uname:0x + SystemCallFilter=@system-service ~@mount:EPERM + SystemCallFilter=~@bogus + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(19, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallLogOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallLogOptionValueTest.kt new file mode 100644 index 00000000..93f4a207 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallLogOptionValueTest.kt @@ -0,0 +1,77 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseSyscallLogOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // "~ some_name another_name" (kf6 autotest): systemd warns that both names are unknown, but syscall + // names are resolved by libseccomp per architecture, so only their shape is checked (deliberate). + // "@system-service", "read write open" and "ptrace,reboot swapoff" are from systemd's + // test/units/TEST-*.sh and the man page. + // language="unit file (systemd)" + val file = """ + [Service] + SystemCallLog=@system-service + SystemCallLog=read write open + SystemCallLog=ptrace,reboot swapoff + SystemCallLog=~@sync @mount + SystemCallLog=~ read, write + SystemCallLog=~ + SystemCallLog=_llseek, + SystemCallLog=~ some_name another_name + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + SystemCallLog=@network-io + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + SystemCallLog=~@mount + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + SystemCallLog=swapon swapoff + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + SystemCallLog=@not-a-group + SystemCallLog=uname:EPERM + SystemCallLog=~uname:EPERM + SystemCallLog=read Write + SystemCallLog=~~read + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseXattrOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseXattrOptionValueTest.kt new file mode 100644 index 00000000..68281e56 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseXattrOptionValueTest.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseXattrOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first three are from systemd's test/units/TEST-74-AUX-UTILS.socket-xattr.sh and the + // systemd.socket(5) Varlink example. + // language="unit file (systemd)" + val file = """ + [Socket] + XAttrEntryPoint=user.varlink=entrypoint + XAttrListen=user.varlink=listen + XAttrAccept=user.varlink=server + XAttrEntryPoint=user.unit=%n + XAttrListen=user.%N.tag=a=b=c + XAttrAccept=user.empty= + XAttrAccept=user.percent=100%% + XAttrAccept=user.literal=50%-off + XAttrAccept=user.trailing=5% + XAttrAccept=user.spaces=hello world + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Socket] + XAttrEntryPoint=security.selinux=foo + XAttrListen=trusted.x=y + XAttrAccept=user.varlink + XAttrEntryPoint=User.varlink=entrypoint + XAttrListen=user.x=%z + XAttrAccept=user.%9=y + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +}