#!/usr/bin/perl use strict; use integer; use bytes; eval 'use File::Copy qw(copy move)'; eval 'use File::Temp qw(mkstemp mktemp)'; eval 'use POSIX qw(uname)'; eval 'use Cwd qw(realpath)'; my $device; my $diskboot; my $instdev; my $diskboot_start; my $default_backup; my $default = "/etc/default/grub_installdevice"; my $debug = 0; $debug = 1 if ($ARGV[0] =~ m/^(--debug|-d)$/); sub is_part ($) { my ($dev) = @_; my $ret; $dev = realpath($dev); if ($dev =~ qr{/dev/(.+)}) { $ret = 1 if (-e "/sys/class/block/$1/partition"); } $ret; } sub is_abstraction ($) { my ($path) = @_; my @abs; chomp( @abs = qx{grub2-probe --target=abstraction $path} ); die "Failed to probe $path for target abstraction\n" if ($? != 0); @abs; } sub default_installdevice () { my $ret; if ( -w $default ) { open( IN, "< $default") || return; while ( ) { chomp; (m{^/dev}) && ($ret = $_, last); } close ( IN ); } $ret; } sub new_installdevice ($) { my ($dev) = @_; my $cfg; die unless (open( IN, "< $default")); while ( ) { if (m{^/dev}) { $cfg .= "${dev}\n"; } else { $cfg .= $_; } } close ( IN ); my ($out, $newf) = mkstemp('/tmp/grub.installdevice.XXXXX'); die unless (print ( $out $cfg)); close ( $out ); $default_backup = mktemp("${default}.old.XXXXX"); copy($default, $default_backup); move($newf, $default); } sub is_grub_drive ($$$) { my ( $prefix, $path, $isdev ) = @_; my $tgt; my ($td, $tp); my ($pd, $pp); my $pattern = qr{\((hd[0-9]+)?,?((?:gpt|msdos)[0-9]+)?\)}; if ($isdev) { chomp( $tgt = qx{grub2-probe --target=drive -d $path} ); } else { chomp( $tgt = qx{grub2-probe --target=drive $path} ); } die "Failed to probe $path for target drive\n" if ($? != 0); ( $tgt =~ $pattern ) && (($td, $tp) = ($1, $2)) || return ; ( $prefix =~ $pattern ) && (($pd, $pp) = ($1, $2)) || return ; return if ($pd && $pd ne $td); return 1 unless ($tp); ($pp eq $tp) ? 1 : 0; } sub embed_part_start ($){ my ($dev) = @_; my @blk; my $ret; chomp (@blk = qx{lsblk --list --ascii --noheadings --output PATH,PTTYPE,PARTTYPE $dev}); die "Failed to get block device information for $dev\n" if ($? != 0); foreach (@blk) { my ($path, $pttype, $parttype) = split /\s+/; if ($pttype eq 'dos') { $ret = 1; last; } elsif ($pttype eq 'gpt' && $parttype eq '21686148-6449-6e6f-744e-656564454649') { if ($path =~ qr{/dev/(.+)}) { if ( -r "/sys/class/block/$1/start" ) { chomp ($ret = qx{cat /sys/class/block/$1/start}); last; } } } } $ret; } sub check_mbr ($) { my ($dev) = @_; my $devh; my $mbr; open( $devh, "< $dev" ) or die "$0: cannot open $dev: $!\n"; sysread( $devh, $mbr, 512 ) == 512 or die "$0: $dev: read error\n"; close( $devh ); my( $magic ) = unpack('H4', $mbr); return if ($magic ne 'eb63'); my( $version ) = unpack('x128H4', $mbr); return if ($version ne '0020'); my( $sector_nr ) = unpack('x92L<', $mbr); return if ($sector_nr ne embed_part_start($dev)); my( $drive_nr ) = unpack('x100H2', $mbr); return if ($drive_nr ne 'ff'); $sector_nr; } sub check_diskboot ($$) { my ($dev, $sector_nr) = @_; my $devh; my $diskboot; my @ret; open($devh, "< $dev" ) or die "$0: cannot open $dev: $!\n"; # print "looks at sector $sector_nr of the same hard drive for core.img\n"; sysseek($devh, $sector_nr*512, 0) or die "$0: $dev: $!\n"; # grub-core/boot/i386/pc/diskboot.S sysread($devh, $diskboot, 512 ) == 512 or die "$0: $dev: read error\n"; close($devh); my( $magic ) = unpack('H8', $diskboot); # print $magic , "\n"; # 5256be1b - upstream diskboot.S # 5256be63 - trustedgrub2 1.4 # 5256be56 - diskboot.S with mjg TPM patches (e.g. in openSUSE Tumbleweed) return if ($magic !~ m/(5256be1b|5256be63|5256be56)/); for (1..3) { my $nr; my $s = 512 - 12 * $_; my( $nr_low, $nr_high, $size ) = unpack("x${s}L 8192) ? 8192 : $size; # Find the last 6 bytes of lzma_decode to find the offset of the lzma_stream: $off = index( unpack( "H".($r<<1), $core ), 'd1e9dffeffff' ); if ($off != -1) { $off >>= 1; $off += 8; $off = (($off + 0b1111) >> 4) << 4; } } sub decomp_lzma ($$) { my ($core, $off) = @_; my $comp_size; my $decomp_size; my $lzma; my $lzmah; my $unlzma; # grub-core/boot/i386/pc/startup_raw.S my $tmpf = "/tmp/lzma_grub.lzma"; ($comp_size, $decomp_size) = unpack ("x8VV", $core); $lzma = pack( "CVVx4", 0x5d, 0x00010000, $decomp_size ); $lzma .= substr( $core, $off, $comp_size ); open($lzmah, "> $tmpf") or die "$0: cannot open $tmpf : $!\n"; binmode $lzmah; print $lzmah $lzma; close($lzmah); $unlzma = qx{lzcat $tmpf}; die if ($? != 0); die "decompressed size mismatch\n" if (length($unlzma) != $decomp_size); ($unlzma, $decomp_size); } sub search_prefix (@) { my ($unlzma, $decomp_size) = @_; my ($mod_base) = unpack("x19V", $unlzma); my ($mod_magic, $mod_off, $mod_sz) = unpack("x$mod_base A4 L< L<", $unlzma); die "module magic mismatch\n" if ( $mod_magic ne "mimg" ); die "module out of bound" if ($mod_base + $mod_sz > $decomp_size); my $mod_start = $mod_base + $mod_off; my $mod_end = $mod_base + $mod_sz; my $embed; my $prefix; while ($mod_start < ($mod_end - 8)) { my ($type, $sz) = unpack("x${mod_start} L< L<", $unlzma); last if ($mod_start + $sz > $mod_end); last if ($sz < 8); if ($type == 2) { ($embed) = unpack(join('', 'x', $mod_start + 8, 'A', $sz - 8), $unlzma); } elsif ($type == 3) { ($prefix) = unpack(join('', 'x', $mod_start + 8, 'A', $sz - 8), $unlzma); } $sz = (($sz + 0b11) >> 2) << 2; $mod_start += $sz; } $prefix; } sub part_to_disk ($) { my ($dev) = @_; my $ret; if ($dev =~ m{/dev/disk/by-uuid/}) { $dev = realpath($dev); } my @regexp = ( qr{(/dev/disk/(?:by-id|by-path)/.+)-part[0-9]+}, qr{(/dev/[a-z]+d[a-z])[0-9]+}, qr{(/dev/nvme[0-9]+n[0-9]+)p[0-9]+} ); foreach (@regexp) { if ($dev =~ $_) { $ret = $1; last; } } $ret; } sub get_prefix ($@) { my ($dev, ($sector_nr, $size)) = @_; my $devh; my $core; my $off; my $prefix; $size <<= 9; $sector_nr <<= 9; open( $devh, "< $dev" ) or die "$0: cannot open $dev: $!\n"; sysseek( $devh, $sector_nr, 0) or die "$0: $dev: $!\n"; sysread( $devh, $core, $size ) == $size or die "$0: $dev: read error\n"; close( $devh ); $off = lzma_start($core, $size); return if ($off == -1); $prefix = search_prefix( decomp_lzma($core, $off) ); } eval { my @uname = uname(); die "machine hardware is not x86_64\n" if ($uname[4] ne 'x86_64'); die "no install device config or no permission to alter it\n" unless ($instdev = default_installdevice()); die "/boot is abstraction\n" if (is_abstraction("/boot")); die "$instdev is NOT partition\n" unless (is_part($instdev)); chomp ( $device = qx{grub2-probe --target=disk /boot} ); die "no disk for /boot\n" unless ( $device ); my $sector_nr = check_mbr($device); die "$device mbr is not used for suse grub embedding\n" unless ($sector_nr); my @core_sectors = check_diskboot($device, $sector_nr); die "core image is not single continuous chunk\n" if (@core_sectors != 2); die "starting sector of startup_raw $core_sectors[0]" . " did not follow diskboot $sector_nr\n" if ($core_sectors[0] != $sector_nr + 1); my $prefix = get_prefix($device, @core_sectors); die "$prefix is not pointing to /boot" unless ($prefix && is_grub_drive ($prefix, '/boot', 0)); my $instdisk = part_to_disk($instdev); die "cannot determine disk device for $instdev" unless ($instdisk); die "$instdisk is not grub disk" unless (is_grub_drive($prefix, $instdisk, 1)); new_installdevice($instdisk); print "The system has been detected using grub in master boot record for booting this updated system with \$prefix=$prefix. However the $default has the install device set to the partition, $instdev. To avoid potential breakage in the application binary interface between grub image and modules, the install device of grub has been changed to use the disk device, $instdisk, to update the master boot record with new grub in order to keep up with the new binary.\n"; print "The backup of the original file is $default_backup\n"; }; print "No fixup required: $@" if ($debug && $@);