diff --git a/package/yast2-bootloader.changes b/package/yast2-bootloader.changes index 6801308d3..9edde4f58 100644 --- a/package/yast2-bootloader.changes +++ b/package/yast2-bootloader.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Fri Apr 26 13:07:51 UTC 2024 - Stefan Schubert + +- Creating kernel options for systemd-boot. (bsc#1220892) +- 5.0.9 + ------------------------------------------------------------------- Fri Apr 5 08:08:09 UTC 2024 - Josef Reidinger diff --git a/package/yast2-bootloader.spec b/package/yast2-bootloader.spec index c6f1f2203..bf26c4a40 100644 --- a/package/yast2-bootloader.spec +++ b/package/yast2-bootloader.spec @@ -17,7 +17,7 @@ Name: yast2-bootloader -Version: 5.0.8 +Version: 5.0.9 Release: 0 Summary: YaST2 - Bootloader Configuration License: GPL-2.0-or-later diff --git a/src/lib/bootloader/generic_widgets.rb b/src/lib/bootloader/generic_widgets.rb index 55c3aa94a..f3c938124 100644 --- a/src/lib/bootloader/generic_widgets.rb +++ b/src/lib/bootloader/generic_widgets.rb @@ -3,6 +3,7 @@ require "yast" require "bootloader/bootloader_factory" +require "bootloader/cpu_mitigations" require "cwm/widget" @@ -110,4 +111,109 @@ def help ) end end + + # Represents decision if smt is enabled + class CpuMitigationsWidget < CWM::ComboBox + def initialize + textdomain "bootloader" + + super + end + + def label + _("CPU Mitigations") + end + + def items + ::Bootloader::CpuMitigations::ALL.map do |m| + [m.value.to_s, m.to_human_string] + end + end + + def help + _( + "

CPU Mitigations
\n" \ + "The option selects which default settings should be used for CPU \n" \ + "side channels mitigations. A highlevel description is on our Technical Information \n" \ + "Document TID 7023836. Following options are available:

" + ) + end + + def init + if Bootloader::BootloaderFactory.current.respond_to?(:cpu_mitigations) + self.value = Bootloader::BootloaderFactory.current.cpu_mitigations.value.to_s + else + disable + end + end + + def store + return unless enabled? + + Bootloader::BootloaderFactory.current.cpu_mitigations = + ::Bootloader::CpuMitigations.new(value.to_sym) + end + end + + # represents kernel command line + class KernelAppendWidget < CWM::InputField + def initialize + textdomain "bootloader" + + super + end + + def label + _("O&ptional Kernel Command Line Parameter") + end + + def help + _( + "

Optional Kernel Command Line Parameter lets you define " \ + "additional parameters to pass to the kernel.

" + ) + end + + def init + current_bl = ::Bootloader::BootloaderFactory.current + case current_bl + when ::Bootloader::SystemdBoot + self.value = current_bl.kernel_params.serialize.gsub(/mitigations=\S+/, "") + when ::Bootloader::Grub2Base + self.value = current_bl.grub_default.kernel_params.serialize.gsub(/mitigations=\S+/, "") + else + disable + end + end + + def store + return unless enabled? + + current_bl = ::Bootloader::BootloaderFactory.current + case current_bl + when ::Bootloader::SystemdBoot + current_bl.kernel_params.replace(value) + when ::Bootloader::Grub2Base + current_bl.grub_default.kernel_params.replace(value) + else + log.error("Bootloader type #{current_bl} not found.") + end + end + end end diff --git a/src/lib/bootloader/grub2_widgets.rb b/src/lib/bootloader/grub2_widgets.rb index 6669245bc..c676618c3 100644 --- a/src/lib/bootloader/grub2_widgets.rb +++ b/src/lib/bootloader/grub2_widgets.rb @@ -122,66 +122,6 @@ def store end end - # Represents decision if smt is enabled - class CpuMitigationsWidget < CWM::ComboBox - include Grub2Helper - - def initialize - textdomain "bootloader" - - super - end - - def label - _("CPU Mitigations") - end - - def items - ::Bootloader::CpuMitigations::ALL.map do |m| - [m.value.to_s, m.to_human_string] - end - end - - def help - _( - "

CPU Mitigations
\n" \ - "The option selects which default settings should be used for CPU \n" \ - "side channels mitigations. A highlevel description is on our Technical Information \n" \ - "Document TID 7023836. Following options are available:

" - ) - end - - def init - if grub2.respond_to?(:cpu_mitigations) - self.value = grub2.cpu_mitigations.value.to_s - else - # do not crash when use no bootloader. This widget is also used in security dialog. - # (bsc#1184968) - disable - end - end - - def store - grub2.cpu_mitigations = ::Bootloader::CpuMitigations.new(value.to_sym) if enabled? - end - end - # Represents decision if generic MBR have to be installed on disk class GenericMBRWidget < CWM::CheckBox include Grub2Helper @@ -268,36 +208,6 @@ def store end end - # represents kernel command line - class KernelAppendWidget < CWM::InputField - include Grub2Helper - - def initialize - textdomain "bootloader" - - super - end - - def label - _("O&ptional Kernel Command Line Parameter") - end - - def help - _( - "

Optional Kernel Command Line Parameter lets you define " \ - "additional parameters to pass to the kernel.

" - ) - end - - def init - self.value = grub_default.kernel_params.serialize.gsub(/mitigations=\S+/, "") - end - - def store - grub_default.kernel_params.replace(value) - end - end - # Represents Protective MBR action class PMBRWidget < CWM::ComboBox include Grub2Helper diff --git a/src/lib/bootloader/grub2base.rb b/src/lib/bootloader/grub2base.rb index 866152959..788b95120 100644 --- a/src/lib/bootloader/grub2base.rb +++ b/src/lib/bootloader/grub2base.rb @@ -21,7 +21,6 @@ Yast.import "BootStorage" Yast.import "HTML" Yast.import "Initrd" -Yast.import "Kernel" Yast.import "Mode" Yast.import "Pkg" Yast.import "Product" @@ -381,31 +380,13 @@ def propose_xen_hypervisor grub_default.xen_hypervisor_params.add_parameter("vga", "gfx-1024x768x16", placer) end - def propose_resume - swap_parts = Yast::BootStorage.available_swap_partitions - largest_swap_name, lagest_swap_size = (swap_parts.max_by { |_part, size| size } || []) - - propose = Yast::Kernel.propose_hibernation? && largest_swap_name - - return "" unless propose - - if lagest_swap_size < Yast::BootStorage.ram_size - log.info "resume parameter is not added because swap (#{largest_swap_name}) is too small" - - return "" - end - - # try to use label or udev id for device name... FATE #302219 - UdevMapping.to_mountby_device(largest_swap_name) - end - def propose_encrypted grub_default.cryptodisk.value = !!Yast::BootStorage.encrypted_boot? end def propose_grub_default if grub_default.kernel_params.empty? - kernel_line = Yast::BootArch.DefaultKernelParams(propose_resume) + kernel_line = Yast::BootArch.DefaultKernelParams(Yast::BootStorage.propose_resume) grub_default.kernel_params.replace(kernel_line) end grub_default.gfxmode ||= "auto" diff --git a/src/lib/bootloader/systemdboot.rb b/src/lib/bootloader/systemdboot.rb index 56c1c81e5..7bbe53fd0 100644 --- a/src/lib/bootloader/systemdboot.rb +++ b/src/lib/bootloader/systemdboot.rb @@ -5,6 +5,7 @@ require "bootloader/sysconfig" require "bootloader/cpu_mitigations" require "cfa/systemd_boot" +require "cfa/grub2/default" Yast.import "Report" Yast.import "Arch" @@ -18,6 +19,8 @@ class SystemdBoot < BootloaderBase include Yast::Logger include Yast::I18n + CMDLINE = "/etc/kernel/cmdline" + # @!attribute menue_timeout # @return [Integer] menue timeout attr_accessor :menue_timeout @@ -30,14 +33,63 @@ def initialize super textdomain "bootloader" + # For kernel parameters we are using the same data structure + # like grub2 in order to be compatible with all calls. + @kernel_container = ::CFA::Grub2::Default.new + @explicit_cpu_mitigations = false + end + + def kernel_params + @kernel_container.kernel_params end + # rubocop:disable Metrics/AbcSize def merge(other) - log.info "merging with system: timeout=#{other.menue_timeout} " \ - "secure_boot=#{other.secure_boot}" + log.info "merging: timeout: #{menue_timeout}=>#{other.menue_timeout}" + log.info " secure_boot: #{secure_boot}=>#{other.secure_boot}" + log.info " mitigations: #{cpu_mitigations.to_human_string}=>" \ + "#{other.cpu_mitigations.to_human_string}" + log.info " kernel_params: #{kernel_params.serialize}=>" \ + "#{other.kernel_params.serialize}" super self.menue_timeout = other.menue_timeout unless other.menue_timeout.nil? self.secure_boot = other.secure_boot unless other.secure_boot.nil? + + kernel_serialize = kernel_params.serialize + # handle specially noresume as it should lead to remove all other resume + kernel_serialize.gsub!(/resume=\S+/, "") if other.kernel_params.parameter("noresume") + + # prevent double cpu_mitigations params + kernel_serialize.gsub!(/mitigations=\S+/, "") if other.kernel_params.parameter("mitigations") + + new_kernel_params = "#{kernel_serialize} #{other.kernel_params.serialize}" + # deduplicate identicatel parameter. Keep always the last one ( so reverse is needed ). + new_params = new_kernel_params.split.reverse.uniq.reverse.join(" ") + + @kernel_container.kernel_params.replace(new_params) + + # explicitly set mitigations means overwrite of our + self.cpu_mitigations = other.cpu_mitigations if other.explicit_cpu_mitigations + + log.info "merging result: timeout: #{menue_timeout}" + log.info " secure_boot: #{secure_boot}" + log.info " mitigations: #{cpu_mitigations.to_human_string}" + log.info " kernel_params: #{kernel_params.serialize}" + end + # rubocop:enable Metrics/AbcSize + + def cpu_mitigations + CpuMitigations.from_kernel_params(kernel_params) + end + + def explicit_cpu_mitigations + @explicit_cpu_mitigations ? cpu_mitigations : nil + end + + def cpu_mitigations=(value) + log.info "set mitigations to #{value.to_human_string}" + @explicit_cpu_mitigations = true + value.modify_kernel_params(kernel_params) end def read @@ -45,6 +97,15 @@ def read read_menue_timeout self.secure_boot = Systeminfo.secure_boot_active? + + lines = "" + filename = File.join(Yast::Installation.destdir, CMDLINE) + if File.exist?(filename) + File.open(filename).each do |line| + lines = + line + end + end + @kernel_container.kernel_params.replace(lines) end # Write bootloader settings to disk @@ -57,12 +118,19 @@ def write(etc_only: false) end write_menue_timeout + File.open(File.join(Yast::Installation.destdir, CMDLINE), "w+") do |fw| + fw.puts(kernel_params.serialize) + end true end def propose super log.info("Propose settings...") + if @kernel_container.kernel_params.empty? + kernel_line = Yast::BootArch.DefaultKernelParams(Yast::BootStorage.propose_resume) + @kernel_container.kernel_params.replace(kernel_line) + end self.menue_timeout = Yast::ProductFeatures.GetIntegerFeature("globals", "boot_timeout").to_i self.secure_boot = Systeminfo.secure_boot_supported? end @@ -137,10 +205,10 @@ def write_sysconfig(prewrite: false) SDBOOTUTIL = "/usr/bin/sdbootutil" def create_menue_entries - cmdline_file = File.join(Yast::Installation.destdir, "/etc/kernel/cmdline") + cmdline_file = File.join(Yast::Installation.destdir, CMDLINE) if Yast::Stage.initial # sdbootutil script needs the "root=" entry in kernel parameters. - # This will be written to /etc/kernel/cmdline which will be used in an + # This will be written to CMDLINE which will be used in an # installed system by the administrator only. So we can use it because # the system will be installed new. This file will be deleted after # calling sdbootutil. diff --git a/src/lib/bootloader/systemdboot_widgets.rb b/src/lib/bootloader/systemdboot_widgets.rb index 0877bea6e..66ee98755 100644 --- a/src/lib/bootloader/systemdboot_widgets.rb +++ b/src/lib/bootloader/systemdboot_widgets.rb @@ -116,10 +116,8 @@ def label def contents VBox( VSpacing(1), - HBox( - HSpacing(1), - HStretch() - ), + MarginBox(1, 0.5, KernelAppendWidget.new), + MarginBox(1, 0.5, Left(CpuMitigationsWidget.new)), VStretch() ) end diff --git a/src/modules/BootStorage.rb b/src/modules/BootStorage.rb index 414a2a815..a4b4251f6 100644 --- a/src/modules/BootStorage.rb +++ b/src/modules/BootStorage.rb @@ -48,6 +48,7 @@ def main Yast.import "Arch" Yast.import "Mode" + Yast.import "Kernel" # FATE#305008: Failover boot configurations for md arrays with redundancy # list includes physical disks used for md raid @@ -254,6 +255,24 @@ def root_partitions stage1_partitions_for(root_filesystem) end + def propose_resume + swap_parts = Yast::BootStorage.available_swap_partitions + largest_swap_name, largest_swap_size = (swap_parts.max_by { |_part, size| size } || []) + + propose = Yast::Kernel.propose_hibernation? && largest_swap_name + + return "" unless propose + + if largest_swap_size < Yast::BootStorage.ram_size + log.info "resume parameter is not added because swap (#{largest_swap_name}) is too small" + + return "" + end + + # try to use label or udev id for device name... FATE #302219 + ::Bootloader::UdevMapping.to_mountby_device(largest_swap_name) + end + private def detect_disks diff --git a/src/modules/Bootloader.rb b/src/modules/Bootloader.rb index cdb6a18c8..160c3107a 100644 --- a/src/modules/Bootloader.rb +++ b/src/modules/Bootloader.rb @@ -22,6 +22,7 @@ require "bootloader/bootloader_factory" require "bootloader/autoyast_converter" require "bootloader/autoinst_profile/bootloader_section" +require "bootloader/systemdboot" require "installation/autoinst_issues/invalid_value" require "cfa/matcher" @@ -336,21 +337,26 @@ def kernel_param(flavor, key) return :missing end - ReadOrProposeIfNeeded() # ensure we have some data - current_bl = ::Bootloader::BootloaderFactory.current - # currently only grub2 bootloader supported - return :missing unless current_bl.respond_to?(:grub_default) - - grub_default = current_bl.grub_default - params = case flavor - when :common then grub_default.kernel_params - when :xen_guest then grub_default.xen_kernel_params - when :xen_host then grub_default.xen_hypervisor_params - else raise ArgumentError, "Unknown flavor #{flavor}" + if current_bl.is_a?(::Bootloader::SystemdBoot) + # systemd-boot + kernel_params = current_bl.kernel_params + elsif current_bl.respond_to?(:grub_default) + # all grub bootloader types + grub_default = current_bl.grub_default + kernel_params = case flavor + when :common then grub_default.kernel_params + when :xen_guest then grub_default.xen_kernel_params + when :xen_host then grub_default.xen_hypervisor_params + else raise ArgumentError, "Unknown flavor #{flavor}" + end + else + return :missing end - res = params.parameter(key) + ReadOrProposeIfNeeded() # ensure we have some data + + res = kernel_params.parameter(key) BOOLEAN_MAPPING[res] || res end @@ -382,10 +388,10 @@ def kernel_param(flavor, key) def modify_kernel_params(*args) ReadOrProposeIfNeeded() # ensure we have data to modify current_bl = ::Bootloader::BootloaderFactory.current - # currently only grub2 bootloader supported - return :missing unless current_bl.respond_to?(:grub_default) - - grub_default = current_bl.grub_default + # currently only grub2 bootloader and systemd-boot supported + if !current_bl.respond_to?(:grub_default) && !current_bl.is_a?(::Bootloader::SystemdBoot) + return :missing + end values = args.pop raise ArgumentError, "Missing parameters to modify #{args.inspect}" if !values.is_a? Hash @@ -403,12 +409,17 @@ def modify_kernel_params(*args) values[key] = remap_values[values[key]] if remap_values.key?(values[key]) end - params = args.map do |flavor| - case flavor - when :common then grub_default.kernel_params - when :xen_guest then grub_default.xen_kernel_params - when :xen_host then grub_default.xen_hypervisor_params - else raise ArgumentError, "Unknown flavor #{flavor}" + if current_bl.is_a?(::Bootloader::SystemdBoot) + params = [current_bl.kernel_params] + else + grub_default = current_bl.grub_default + params = args.map do |flavor| + case flavor + when :common then grub_default.kernel_params + when :xen_guest then grub_default.xen_kernel_params + when :xen_host then grub_default.xen_hypervisor_params + else raise ArgumentError, "Unknown flavor #{flavor}" + end end end diff --git a/test/data/etc/kernel/cmdline b/test/data/etc/kernel/cmdline new file mode 100644 index 000000000..3535cd4cd --- /dev/null +++ b/test/data/etc/kernel/cmdline @@ -0,0 +1 @@ +splash=silent quiet security=apparmor mitigations=off diff --git a/test/grub2_widgets_test.rb b/test/grub2_widgets_test.rb index 5a6565990..7f40371ac 100644 --- a/test/grub2_widgets_test.rb +++ b/test/grub2_widgets_test.rb @@ -102,7 +102,7 @@ def stub_widget_value(id, value) end end -describe Bootloader::Grub2Widget::CpuMitigationsWidget do +describe Bootloader::CpuMitigationsWidget do before do assign_bootloader end @@ -215,7 +215,7 @@ def stub_widget_value(id, value) end end -describe Bootloader::Grub2Widget::KernelAppendWidget do +describe Bootloader::KernelAppendWidget do before do assign_bootloader end @@ -231,6 +231,7 @@ def stub_widget_value(id, value) it "stores text as kernel command line option" do expect(subject).to receive(:value).and_return("showopts quiet") + expect(subject).to receive(:enabled?).and_return(true) subject.store expect(bootloader.grub_default.kernel_params.serialize).to eq "showopts quiet" diff --git a/test/systemdboot_test.rb b/test/systemdboot_test.rb index 7810fd980..c860ffd6c 100644 --- a/test/systemdboot_test.rb +++ b/test/systemdboot_test.rb @@ -11,46 +11,90 @@ end let(:destdir) { File.expand_path("data/", __dir__) } - let(:kerneldir) { File.join(destdir, "etc", "kernel") } + let(:cmdline_content) { "splash=silent quiet security=apparmor mitigations=off" } before do allow(Yast::BootStorage).to receive(:available_swap_partitions).and_return([]) allow(Yast::Arch).to receive(:architecture).and_return("x86_64") allow(Yast::Package).to receive(:Available).and_return(true) - FileUtils.mkdir_p(kerneldir) - end - - after do - FileUtils.remove_entry(kerneldir) if Dir.exist?(kerneldir) end describe "#read" do - it "reads bootloader flags from sysconfig" do + before do expect(Bootloader::Systeminfo).to receive(:secure_boot_active?).and_return(true) - allow(Yast::Installation).to receive(:destdir).and_return(File.expand_path("data/", __dir__)) + allow(Yast::Installation).to receive(:destdir).and_return(destdir) + end + it "reads bootloader flags from sysconfig" do subject.read expect(subject.secure_boot).to eq true end + + it "reads entries from /etc/kernel/cmdline" do + subject.read + + expect(subject.cpu_mitigations.to_human_string).to eq "Off" + expect(subject.kernel_params.serialize).to eq cmdline_content + end end describe "#write" do - it "installs bootloader and creates menue entries" do + before do allow(subject).to receive(:secure_boot).and_return(false) allow(Yast::Stage).to receive(:initial).and_return(true) + allow(Yast::Installation).to receive(:destdir).and_return(destdir) + subject.kernel_params.replace(cmdline_content) + subject.menue_timeout = 10 + end + + it "installs the bootloader" do + allow(Yast::Execute).to receive(:on_target!) + .with("/usr/bin/sdbootutil", "--verbose", "add-all-kernels") + allow_any_instance_of(CFA::SystemdBoot).to receive(:save) # install bootloader expect(Yast::Execute).to receive(:on_target!) .with("/usr/bin/sdbootutil", "--verbose", "install") + + subject.write + end + + it "writes kernel cmdline" do + allow(Yast::Execute).to receive(:on_target!) + .with("/usr/bin/sdbootutil", "--verbose", "install") + allow(Yast::Execute).to receive(:on_target!) + .with("/usr/bin/sdbootutil", "--verbose", "add-all-kernels") + allow_any_instance_of(CFA::SystemdBoot).to receive(:save) + + subject.write + # Checking written kernel parameters + subject.read + expect(subject.cpu_mitigations.to_human_string).to eq "Off" + expect(subject.kernel_params.serialize).to eq cmdline_content + end + + it "creates menue entries" do + allow(Yast::Execute).to receive(:on_target!) + .with("/usr/bin/sdbootutil", "--verbose", "install") + allow_any_instance_of(CFA::SystemdBoot).to receive(:save) + # create menue entries - allow(Yast::Installation).to receive(:destdir).and_return(destdir) expect(Yast::Execute).to receive(:on_target!) .with("/usr/bin/sdbootutil", "--verbose", "add-all-kernels") + + subject.write + end + + it "saves menue timeout" do + allow(Yast::Execute).to receive(:on_target!) + .with("/usr/bin/sdbootutil", "--verbose", "install") + allow(Yast::Execute).to receive(:on_target!) + .with("/usr/bin/sdbootutil", "--verbose", "add-all-kernels") + # Saving menue timeout expect_any_instance_of(CFA::SystemdBoot).to receive(:save) - subject.menue_timeout = 10 subject.write end end @@ -77,18 +121,47 @@ end describe "#merge" do - it "overwrite secure boot and menue timeout if specified in merged one" do + it "overwrite secure boot, mitigations and menue timeout if specified in merged one" do + other_cmdline = "splash=silent quiet mitigations=auto" other = described_class.new other.secure_boot = true other.menue_timeout = 12 + other.kernel_params.replace(other_cmdline) subject.secure_boot = false subject.menue_timeout = 10 + subject.kernel_params.replace(cmdline_content) subject.merge(other) expect(subject.secure_boot).to eq true expect(subject.menue_timeout).to eq 12 + expect(subject.cpu_mitigations.to_human_string).to eq "Auto" + expect(subject.kernel_params.serialize).to eq "security=apparmor splash=silent quiet mitigations=auto" + end + end + + describe "#propose" do + it "proposes timeout to product/role default" do + allow(Yast::ProductFeatures).to receive(:GetIntegerFeature) + .with("globals", "boot_timeout").and_return(2) + subject.propose + + expect(subject.menue_timeout).to eq 2 + end + + it "proposes secure boot" do + allow(Bootloader::Systeminfo).to receive(:secure_boot_supported?).and_return(true) + subject.propose + + expect(subject.secure_boot).to eq true + end + + it "proposes kernel cmdline" do + expect(Yast::BootArch).to receive(:DefaultKernelParams).and_return(cmdline_content) + + subject.propose + expect(subject.kernel_params.serialize).to eq cmdline_content end end end diff --git a/test/systemdboot_widgets_test.rb b/test/systemdboot_widgets_test.rb index 80260ac4d..24333cbeb 100644 --- a/test/systemdboot_widgets_test.rb +++ b/test/systemdboot_widgets_test.rb @@ -85,6 +85,61 @@ def stub_widget_value(id, value) end end +describe Bootloader::CpuMitigationsWidget do + before do + assign_systemd_bootloader + end + + it_behaves_like "labeled widget" + it_behaves_like "CWM::ComboBox" + + context "when none bootloader is selected" do + before do + assign_bootloader("none") + end + + describe "#init" do + it "disables widget" do + expect(subject).to receive(:disable) + + subject.init + end + end + + describe "#store" do + it "does nothing on disabled widget" do + expect(subject).to receive(:enabled?).and_return(false) + expect(subject).to_not receive(:value) + + subject.store + end + end + end +end + +describe Bootloader::KernelAppendWidget do + before do + assign_systemd_bootloader + end + + it_behaves_like "labeled widget" + + it "is initialized to kernel command line option" do + bootloader.kernel_params.replace("verbose showopts") + expect(subject).to receive(:value=).with("verbose showopts") + + subject.init + end + + it "stores text as kernel command line option" do + expect(subject).to receive(:value).and_return("showopts quiet") + expect(subject).to receive(:enabled?).and_return(true) + subject.store + + expect(bootloader.kernel_params.serialize).to eq "showopts quiet" + end +end + describe Bootloader::SystemdBootWidget::KernelTab do before do assign_systemd_bootloader