···11+## v2.0.0 (2023-04-19)
22+* Add optional argument `?disk_signature` to `Mbr.make` (@Burnleydev1, review by @reynir, #19)
33+* Make the partition type a required argument to `Mbr.Partition.make` and rename it `~partition_type` (@AryanGodara, review by @reynir, #20)
44+* Add tools for inspecting and modifying MBR, and reading/writing data to partitions. The command line tools are not installed as part of the opam package. The tools are `bin/mbr_inspect.exe`, `bin/read_partition.exe`, `bin/resize_partition.exe` and `bin/write_partition.exe`. (@PizieDust, review by @reynir, #22, #23, #24, #26)
55+* Remove dependency on `ppx_cstruct` (@reynir, #27)
66+77+## v1.0 (2022-09-27)
88+* Switch to dune
99+* Remove `Mbr_partition` and `Mbr_lwt`
1010+* Remove old stringly typed interface
1111+* Types are private
1212+* Add helper functions to convert between uint32 MBR values and int64 values as expected in `Mirage_block`
1313+* Update code and slim down on dependencies
1414+* Handle empty partition entries
1515+1616+## v0.3 (2015-06-04)
1717+* Expose a `connect` function for mirage-types > 2.3
1818+* Fix bounds checks
1919+* Add unit tests
2020+* Fix integer overflow
2121+* Add opam file
2222+2323+## v0.2 (2014-08-18)
2424+* add `Mbr_partition: V1_LWT.BLOCK`, for easy access to partitions via
2525+ the standard Mirage block interface.
2626+* use a polymorphic variant result type `` [`Ok of 'a | `Error of 'b]``
+14
LICENSE
···11+Copyright (c) 2014, Citrix Systems Inc
22+33+Permission to use, copy, modify, and/or distribute this software for any
44+purpose with or without fee is hereby granted, provided that the above
55+copyright notice and this permission notice appear in all copies.
66+77+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
88+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
99+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
1010+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
1111+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1212+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1313+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1414+
+37
README.md
···11+ocaml-mbr
22+=========
33+44+A library for manipulating Master Boot Records. The
55+primary purposes of this library are:
66+ 1. to create bootable disk images creating
77+ [mirage](http://www.openmirage.org/) kernels
88+ 2. for mirage kernels to read the partition tables on
99+ attached disks
1010+1111+Usage
1212+-----
1313+Define a single partition as follows:
1414+```
1515+ let disk_length_bytes = Int32.(mul (mul 16l 1024l) 1024l) in
1616+ let disk_length_sectors = Int32.(div disk_length_bytes 512l) in
1717+1818+ let start_sector = 2048l in
1919+ let length_sectors = Int32.sub disk_length_sectors start_sector in
2020+ let partition = Mbr.Partition.make ~active:true ~ty:6 start_sector length_sectors in
2121+ let mbr = Mbr.make [ partition ] in
2222+```
2323+You can write the MBR to sector zero of a block device ```B``` as follows:
2424+```
2525+ B.connect id >>= fun device ->
2626+ let sector = Cstruct.create 512 in
2727+ Mbr.marshal sector mbr;
2828+ B.write device 0L [ sector ] >>= fun () ->
2929+ ...
3030+```
3131+3232+To do items
3333+-----------
3434+3535+* Implement tools to manipulate MBR-formatted disk images
3636+ to construct, inspect or fill partitions that can later
3737+ be used in Mirage unikernels.
···11+open Cmdliner
22+33+let print_mbr_fields print_bootstrap_code mbr =
44+ Printf.printf "MBR fields:\n";
55+ if print_bootstrap_code then
66+ Printf.printf " bootstrap_code: %s\n"
77+ (Cstruct.to_hex_string (Cstruct.of_string mbr.Mbr.bootstrap_code));
88+ Printf.printf " original_physical_drive: %d\n"
99+ mbr.Mbr.original_physical_drive;
1010+ Printf.printf " seconds: %d\n" mbr.Mbr.seconds;
1111+ Printf.printf " minutes: %d\n" mbr.Mbr.minutes;
1212+ Printf.printf " hours: %d\n" mbr.Mbr.hours;
1313+ Printf.printf " disk_signature: %lx\n" mbr.Mbr.disk_signature;
1414+ List.iteri
1515+ (fun i part ->
1616+ let chs_begin = part.Mbr.Partition.first_absolute_sector_chs in
1717+ let chs_end = part.Mbr.Partition.last_absolute_sector_chs in
1818+ Printf.printf " Partition %d:\n" (i + 1);
1919+ Printf.printf " bootable: %b\n" part.Mbr.Partition.active;
2020+ let { Mbr.Geometry.cylinders; Mbr.Geometry.heads; Mbr.Geometry.sectors } =
2121+ chs_begin
2222+ in
2323+ Printf.printf " chs_begin: (cylinders: %d, heads: %d, sectors: %d)\n"
2424+ cylinders heads sectors;
2525+ Printf.printf " ty: %02x\n" part.Mbr.Partition.ty;
2626+ let { Mbr.Geometry.cylinders; Mbr.Geometry.heads; Mbr.Geometry.sectors } =
2727+ chs_end
2828+ in
2929+ Printf.printf " chs_end: (cylinders: %d, heads: %d, sectors: %d)\n"
3030+ cylinders heads sectors;
3131+ Printf.printf " lba_begin: %ld\n"
3232+ part.Mbr.Partition.first_absolute_sector_lba;
3333+ Printf.printf " size_sectors: %ld\n" part.Mbr.Partition.sectors)
3434+ mbr.partitions
3535+3636+let read_mbrs print_bootstrap_code mbrs =
3737+ List.iter
3838+ (fun mbr ->
3939+ let ic = open_in_bin mbr in
4040+ let buf = Bytes.create Mbr.sizeof in
4141+ let () = really_input ic buf 0 Mbr.sizeof in
4242+ close_in ic;
4343+ match Mbr.unmarshal (Cstruct.of_bytes buf) with
4444+ | Ok mbr -> print_mbr_fields print_bootstrap_code mbr
4545+ | Error msg ->
4646+ Printf.printf "Failed to read MBR from %s: %s\n" mbr msg;
4747+ exit 1)
4848+ mbrs
4949+5050+let mbrs = Arg.(non_empty & pos_all file [] & info [] ~docv:"disk_images")
5151+5252+let print_bootstrap_code =
5353+ let doc = "Print the bootstrap code of the disks images." in
5454+ Arg.(value & flag & info [ "b"; "booststrap-code" ] ~doc)
5555+5656+let cmd =
5757+ let doc =
5858+ "Inspect the Master Boot Record (MBR) headers of one or more disk images."
5959+ in
6060+ let info = Cmd.info "mbr_inspect" ~version:"1.0.0" ~doc in
6161+ Cmd.v info Term.(const read_mbrs $ print_bootstrap_code $ mbrs)
6262+6363+let main () = exit (Cmd.eval cmd)
6464+let () = main ()
+88
bin/read_partition.ml
···11+open Cmdliner
22+33+let read_mbr mbr =
44+ let ic = open_in_bin mbr in
55+ let buf = Bytes.create Mbr.sizeof in
66+ let () = really_input ic buf 0 Mbr.sizeof in
77+ close_in ic;
88+ match Mbr.unmarshal (Cstruct.of_bytes buf) with
99+ | Ok mbr -> mbr
1010+ | Error msg ->
1111+ Printf.printf "Failed to read MBR from %s: %s\n" mbr msg;
1212+ exit 1
1313+1414+let get_partition_info mbr partition_num =
1515+ let mbr = read_mbr mbr in
1616+ match partition_num with
1717+ | 1 | 2 | 3 | 4 -> List.nth mbr.Mbr.partitions (partition_num - 1)
1818+ | _ -> failwith "Partition number must be between 1 and 4"
1919+2020+let calculate_partition_info partition =
2121+ let start_sector =
2222+ Int32.to_int partition.Mbr.Partition.first_absolute_sector_lba
2323+ in
2424+ let num_sectors = Int32.to_int partition.Mbr.Partition.sectors in
2525+ let sector_size = 512 in
2626+ (start_sector, num_sectors, sector_size)
2727+2828+let read_partition_data mbr start_sector num_sectors sector_size output =
2929+ let buffer_size = 4096 in
3030+ let buffer = Bytes.create buffer_size in
3131+ let ic = open_in_bin mbr in
3232+ let offset = start_sector * sector_size in
3333+ let () = seek_in ic offset in
3434+ let rec loop remaining_bytes =
3535+ if remaining_bytes > 0 then
3636+ let bytes_to_read = min buffer_size remaining_bytes in
3737+ let () = really_input ic buffer 0 bytes_to_read in
3838+ (* [Bytes.unsafe_to_string buffer] is safe here because [output] will not
3939+ keep the string once returned. *)
4040+ let () = output (Bytes.unsafe_to_string buffer) 0 bytes_to_read in
4141+ loop (remaining_bytes - bytes_to_read)
4242+ else ()
4343+ in
4444+ loop (num_sectors * sector_size);
4545+ close_in ic
4646+4747+let writer output_channel buffer offset length =
4848+ output_substring output_channel buffer offset length
4949+5050+let extract_partition_data mbr partition_num output_file =
5151+ let partition = get_partition_info mbr partition_num in
5252+ let start_sector, num_sectors, sector_size =
5353+ calculate_partition_info partition
5454+ in
5555+ match output_file with
5656+ | None ->
5757+ read_partition_data mbr start_sector num_sectors sector_size
5858+ (writer stdout)
5959+ | Some file_path ->
6060+ let oc =
6161+ open_out_gen [ Open_wronly; Open_creat; Open_trunc ] 0o666 file_path
6262+ in
6363+ let () =
6464+ read_partition_data mbr start_sector num_sectors sector_size (writer oc)
6565+ in
6666+ close_out oc
6767+6868+let mbr =
6969+ let doc = "The disk image containing the partition" in
7070+ Arg.(required & pos 0 (some file) None & info [] ~docv:"disk_image" ~doc)
7171+7272+let partition_number =
7373+ let doc = "The partition number to read" in
7474+ Arg.(required & pos 1 (some int) None & info [] ~docv:"partition_number" ~doc)
7575+7676+let output_to_file =
7777+ let doc = "Output partition contents to a file" in
7878+ Arg.(value & opt (some string) None & info [ "f"; "file" ] ~doc)
7979+8080+let cmd =
8181+ let doc = "Read the contents of a partition" in
8282+ let info = Cmd.info "read_partition" ~version:"1.0.0" ~doc in
8383+ Cmd.v info
8484+ Term.(
8585+ const extract_partition_data $ mbr $ partition_number $ output_to_file)
8686+8787+let main () = exit (Cmd.eval cmd)
8888+let () = main ()
+100
bin/resize_partition.ml
···11+open Cmdliner
22+33+let read_mbr mbr =
44+ let ic = open_in_bin mbr in
55+ let buf = Bytes.create Mbr.sizeof in
66+ let () = really_input ic buf 0 Mbr.sizeof in
77+ close_in ic;
88+ match Mbr.unmarshal (Cstruct.of_bytes buf) with
99+ | Ok mbr -> (mbr, Mbr.sizeof)
1010+ | Error msg ->
1111+ Printf.printf "Failed to read MBR from %s: %s\n" mbr msg;
1212+ exit 1
1313+1414+let get_partition_info mbr partition_number =
1515+ List.nth mbr.Mbr.partitions (partition_number - 1)
1616+1717+let calculate_partition_info partition =
1818+ (* FIXME: Use Int32.unsigned_to_int *)
1919+ let start_sector =
2020+ Int32.to_int partition.Mbr.Partition.first_absolute_sector_lba
2121+ in
2222+ let num_sectors = Int32.to_int partition.Mbr.Partition.sectors in
2323+ let sector_size = 512 in
2424+ Printf.printf "Current partition size: %d bytes\n" (num_sectors * sector_size);
2525+ (start_sector, sector_size)
2626+2727+let make_new_partition partition start_sector sector_size new_size =
2828+ if new_size mod sector_size <> 0 then
2929+ Printf.ksprintf failwith
3030+ "Partition cannot be resized. New size of %d bytes does not align to \
3131+ sectors. New size must be a multiple of %d"
3232+ new_size sector_size
3333+ else
3434+ let new_num_sectors = new_size / sector_size in
3535+ let new_end_sector = start_sector + new_num_sectors in
3636+ Printf.printf "New partition size: %d bytes\n"
3737+ (new_num_sectors * sector_size);
3838+ match
3939+ Mbr.Partition.make ~active:partition.Mbr.Partition.active
4040+ ~partition_type:partition.Mbr.Partition.ty
4141+ partition.Mbr.Partition.first_absolute_sector_lba
4242+ (Int32.of_int new_end_sector)
4343+ with
4444+ | Ok new_partition -> new_partition
4545+ | Error msg -> failwith msg
4646+4747+let replace_partition_in_partition_table mbr partition_number new_partition =
4848+ let update_partition i p =
4949+ if i = partition_number - 1 then new_partition else p
5050+ in
5151+ List.mapi update_partition mbr.Mbr.partitions
5252+5353+(* Mbr.make smart constructor checks for partition overlap, more than 1 active partitions and too many partitions *)
5454+let make_new_mbr mbr new_partition_table =
5555+ match Mbr.make ~disk_signature:mbr.Mbr.disk_signature new_partition_table with
5656+ | Ok new_mbr -> new_mbr
5757+ | Error msg -> failwith msg
5858+5959+let resize_partition mbr partition_number new_size =
6060+ let disk = mbr in
6161+ let mbr = read_mbr mbr |> fst in
6262+ let partition = get_partition_info mbr partition_number in
6363+ let start_sector, sector_size = calculate_partition_info partition in
6464+ let new_partition =
6565+ make_new_partition partition start_sector sector_size new_size
6666+ in
6767+ let new_partition_table =
6868+ replace_partition_in_partition_table mbr partition_number new_partition
6969+ in
7070+ let new_mbr = make_new_mbr mbr new_partition_table in
7171+ let oc = open_out_gen [ Open_wronly; Open_binary ] 0o644 disk in
7272+ seek_out oc 0;
7373+ let buf = Cstruct.create Mbr.sizeof in
7474+ Mbr.marshal buf new_mbr;
7575+ let mbr_bytes = Cstruct.to_bytes buf in
7676+ output oc mbr_bytes 0 Mbr.sizeof;
7777+ close_out_noerr oc
7878+7979+let mbr =
8080+ let doc = "The disk image containing the partition." in
8181+ Arg.(required & pos 0 (some file) None & info [] ~docv:"disk_image" ~doc)
8282+8383+let partition_number =
8484+ let doc = "The partition number to resize. Indexed from 1 to 4." in
8585+ Arg.(required & pos 1 (some int) None & info [] ~docv:"partition_number" ~doc)
8686+8787+let new_size =
8888+ let doc =
8989+ "The new size of the partition in bytes. The size has to be aligned with \
9090+ the sector size, i.e. a multiple of 512."
9191+ in
9292+ Arg.(required & pos 2 (some int) None & info [] ~docv:"new_size" ~doc)
9393+9494+let cmd =
9595+ let doc = "Resize a partition" in
9696+ let info = Cmd.info "resize_partition" ~version:"1.0.0" ~doc in
9797+ Cmd.v info Term.(const resize_partition $ mbr $ partition_number $ new_size)
9898+9999+let main () = exit (Cmd.eval cmd)
100100+let () = main ()
+95
bin/write_partition.ml
···11+open Cmdliner
22+33+let read_mbr mbr =
44+ let ic = open_in_bin mbr in
55+ let buf = Bytes.create Mbr.sizeof in
66+ let () = really_input ic buf 0 Mbr.sizeof in
77+ close_in ic;
88+ match Mbr.unmarshal (Cstruct.of_bytes buf) with
99+ | Ok mbr -> (mbr, Mbr.sizeof)
1010+ | Error msg ->
1111+ Printf.printf "Failed to read MBR from %s: %s\n" mbr msg;
1212+ exit 1
1313+1414+let get_partition_info mbr partition_num =
1515+ let mbr = read_mbr mbr |> fst in
1616+ List.nth mbr.Mbr.partitions (partition_num - 1)
1717+1818+let calculate_partition_info partition =
1919+ (* FIXME: Use Int32.unsigned_to_int *)
2020+ let start_sector =
2121+ Int32.to_int partition.Mbr.Partition.first_absolute_sector_lba
2222+ in
2323+ let num_sectors = Int32.to_int partition.Mbr.Partition.sectors in
2424+ let sector_size = 512 in
2525+ (start_sector, num_sectors, sector_size)
2626+2727+let copy ic oc max_bytes =
2828+ let buf_len = 4096 in
2929+ let buf = Bytes.create buf_len in
3030+ let rec loop i =
3131+ let len = input ic buf 0 buf_len in
3232+ if len > 0 then (
3333+ let len' = min len (max_bytes - i) in
3434+ output oc buf 0 len';
3535+ if i + len > max_bytes then
3636+ failwith "Trying to write more than can fit in partition";
3737+ loop (i + len'))
3838+ else ()
3939+ in
4040+ loop 0
4141+4242+let write_to_partition mbr partition_number input_data =
4343+ let partition = get_partition_info mbr partition_number in
4444+ let start_sector, num_sectors, sector_size =
4545+ calculate_partition_info partition
4646+ in
4747+ if start_sector = 0 then
4848+ Printf.ksprintf failwith
4949+ "Writing to partition %d would overwrite the MBR header" partition_number;
5050+ let ic, data_size =
5151+ match input_data with
5252+ | None -> (stdin, None)
5353+ | Some file_path ->
5454+ let file_info = Unix.stat file_path in
5555+ let data_size = file_info.st_size in
5656+ let ic = open_in_bin file_path in
5757+ (ic, Some data_size)
5858+ in
5959+ let partition_size = num_sectors * sector_size in
6060+ Option.iter
6161+ (fun data_size -> Printf.printf "Total input size: %d\n" data_size)
6262+ data_size;
6363+ Printf.printf "Total Partition size: %d\n" partition_size;
6464+ Option.iter
6565+ (fun data_size ->
6666+ if data_size > partition_size then
6767+ failwith "Input is too large for partition")
6868+ data_size;
6969+ Printf.printf "\nBegin writing to partition:- \n";
7070+ let oc = open_out_gen [ Open_wronly; Open_binary ] 0o644 mbr in
7171+ seek_out oc (start_sector * sector_size);
7272+ copy ic oc partition_size;
7373+ close_out_noerr oc;
7474+ close_in_noerr ic
7575+7676+let mbr =
7777+ let doc = "The disk image containing the partition" in
7878+ Arg.(required & pos 0 (some file) None & info [] ~docv:"disk_image" ~doc)
7979+8080+let partition_number =
8181+ let doc = "The partition number to write to" in
8282+ Arg.(required & pos 1 (some int) None & info [] ~docv:"partition_number" ~doc)
8383+8484+let input_data =
8585+ let doc = "The data to write to the partition." in
8686+ Arg.(value & opt (some string) None & info [ "d"; "data" ] ~doc ~docv:"FILE")
8787+8888+let cmd =
8989+ let doc = "Write data into a partition" in
9090+ let info = Cmd.info "write_partition" ~version:"1.0.0" ~doc in
9191+ Cmd.v info
9292+ Term.(const write_to_partition $ mbr $ partition_number $ input_data)
9393+9494+let main () = exit (Cmd.eval cmd)
9595+let () = main ()
+30
dune-project
···11+(lang dune 3.17)
22+33+(name mbr-format)
44+55+(generate_opam_files true)
66+77+(license ISC)
88+99+(authors
1010+ "David Scott <dave.scott@eu.citrix.com>"
1111+ "Reynir Björnsson <reynir@reynir.dk>")
1212+1313+(maintainers "Thomas Gazagnaire <thomas@gazagnaire.org>")
1414+1515+(source
1616+ (uri https://tangled.org/gazagnaire.org/ocaml-mbr))
1717+1818+(bug_reports "https://tangled.org/gazagnaire.org/ocaml-mbr/issues")
1919+2020+(package
2121+ (name mbr-format)
2222+ (synopsis "A library to manipulate Master Boot Records")
2323+ (description
2424+ "Pure OCaml library for reading and writing Master Boot Records (MBR).
2525+Useful for creating bootable disk images and reading partition tables.")
2626+ (depends
2727+ (ocaml (>= 5.1))
2828+ (bytesrw (>= 0.1))
2929+ (alcotest :with-test)
3030+ (crowbar :with-test)))
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Fuzz tests for MBR parsing.
77+88+ Key properties tested:
99+ 1. Parser crash safety - no crashes on arbitrary 512-byte input
1010+ 2. Roundtrip - marshal(unmarshal(x)) produces valid MBR
1111+ 3. Partition bounds - start + size doesn't overflow
1212+1313+ Security considerations:
1414+ - MBR parsers have historically had integer overflow bugs
1515+ - Malformed partition entries shouldn't cause crashes
1616+ - CHS values can be crafted to cause issues *)
1717+1818+open Crowbar
1919+2020+(** Truncate input to reasonable size. *)
2121+let truncate ?(max_len = 512) buf =
2222+ let len = min max_len (String.length buf) in
2323+ String.sub buf 0 len
2424+2525+(** MBR unmarshal crash safety.
2626+ Parser must not crash on any 512-byte input. *)
2727+let test_unmarshal_crash_safety buf =
2828+ let buf = truncate ~max_len:512 buf in
2929+ (* Pad to 512 bytes if needed *)
3030+ let buf =
3131+ if String.length buf < 512 then
3232+ buf ^ String.make (512 - String.length buf) '\x00'
3333+ else buf
3434+ in
3535+ let _ = Mbr.of_string buf in
3636+ ()
3737+3838+(** MBR roundtrip - valid MBRs should survive marshal/unmarshal. *)
3939+let test_roundtrip () =
4040+ (* Create a simple valid MBR *)
4141+ let p1 =
4242+ match Mbr.Partition.make ~active:false ~partition_type:0x83 2048l 1048576l with
4343+ | Ok p -> p
4444+ | Error _ -> fail "failed to create partition"
4545+ in
4646+ let mbr =
4747+ match Mbr.make [ p1 ] with
4848+ | Ok m -> m
4949+ | Error _ -> fail "failed to create MBR"
5050+ in
5151+ let buf = Mbr.to_string mbr in
5252+ match Mbr.of_string buf with
5353+ | Error _ -> fail "roundtrip failed: unmarshal error"
5454+ | Ok mbr' ->
5555+ let partitions = Mbr.partitions mbr in
5656+ let partitions' = Mbr.partitions mbr' in
5757+ if List.length partitions <> List.length partitions' then
5858+ fail "roundtrip: partition count mismatch"
5959+6060+(** Partition make with arbitrary values - must not crash. *)
6161+let test_partition_make active ptype start size =
6262+ let active = active mod 2 = 0 in
6363+ let ptype = ptype mod 256 in
6464+ let start = Int32.of_int (start mod 0x7FFFFFFF) in
6565+ let size = Int32.of_int (size mod 0x7FFFFFFF) in
6666+ let _ = Mbr.Partition.make ~active ~partition_type:ptype start size in
6767+ ()
6868+6969+(** MBR make with multiple partitions. *)
7070+let test_mbr_make p1_start p1_size p2_start p2_size =
7171+ let p1_start = Int32.of_int ((abs p1_start mod 0x7FFFFFFF) + 1) in
7272+ let p1_size = Int32.of_int ((abs p1_size mod 0x7FFFFF) + 1) in
7373+ let p2_start = Int32.of_int ((abs p2_start mod 0x7FFFFFFF) + 1) in
7474+ let p2_size = Int32.of_int ((abs p2_size mod 0x7FFFFF) + 1) in
7575+ let p1 = Mbr.Partition.make ~active:false ~partition_type:0x83 p1_start p1_size in
7676+ let p2 = Mbr.Partition.make ~active:false ~partition_type:0x83 p2_start p2_size in
7777+ match p1, p2 with
7878+ | Ok p1, Ok p2 ->
7979+ (* MBR.make may fail due to overlap, that's OK *)
8080+ let _ = Mbr.make [ p1; p2 ] in
8181+ ()
8282+ | _ -> ()
8383+8484+let () =
8585+ add_test ~name:"mbr: unmarshal crash safety" [ bytes ] test_unmarshal_crash_safety;
8686+ add_test ~name:"mbr: roundtrip" [] test_roundtrip;
8787+ add_test ~name:"mbr: partition make" [ int; int; int; int ] test_partition_make;
8888+ add_test ~name:"mbr: make with partitions" [ int; int; int; int ] test_mbr_make
···11+(*
22+ * Copyright (C) 2013 Citrix Inc
33+ *
44+ * Permission to use, copy, modify, and distribute this software for any
55+ * purpose with or without fee is hereby granted, provided that the above
66+ * copyright notice and this permission notice appear in all copies.
77+ *
88+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
99+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
1010+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
1111+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
1212+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1313+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1414+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1515+ *)
1616+1717+module Result_syntax = struct
1818+ let ( let* ) = Result.bind
1919+end
2020+2121+open Result_syntax
2222+2323+(* Binary reading helpers - little-endian *)
2424+let get_u8 s off = Char.code (Bytes.get s off)
2525+let get_u16_le s off = get_u8 s off lor (get_u8 s (off + 1) lsl 8)
2626+2727+let get_u32_le s off =
2828+ Int32.logor
2929+ (Int32.of_int (get_u16_le s off))
3030+ (Int32.shift_left (Int32.of_int (get_u16_le s (off + 2))) 16)
3131+3232+(* Binary writing helpers - little-endian *)
3333+let set_u8 s off v = Bytes.set s off (Char.chr (v land 0xff))
3434+3535+let set_u16_le s off v =
3636+ set_u8 s off (v land 0xff);
3737+ set_u8 s (off + 1) ((v lsr 8) land 0xff)
3838+3939+let set_u32_le s off v =
4040+ set_u16_le s off (Int32.to_int (Int32.logand v 0xffffl));
4141+ set_u16_le s (off + 2)
4242+ (Int32.to_int (Int32.logand (Int32.shift_right_logical v 16) 0xffffl))
4343+4444+(* Check all bytes in range are zero *)
4545+let bytes_all_zero buf off len =
4646+ let rec check i = i >= len || (get_u8 buf (off + i) = 0 && check (i + 1)) in
4747+ check 0
4848+4949+module Reader = Bytesrw.Bytes.Reader
5050+module Writer = Bytesrw.Bytes.Writer
5151+module Slice = Bytesrw.Bytes.Slice
5252+5353+(* Read exactly n bytes from reader *)
5454+let read_exactly reader n =
5555+ if n = 0 then Ok Bytes.empty
5656+ else
5757+ let buf = Bytes.create n in
5858+ let rec loop pos remaining =
5959+ if remaining = 0 then Ok buf
6060+ else
6161+ match Reader.read reader with
6262+ | slice when Slice.is_eod slice -> Error "unexpected end of data"
6363+ | slice ->
6464+ let str = Slice.to_string slice in
6565+ let available = String.length str in
6666+ let to_copy = min available remaining in
6767+ Bytes.blit_string str 0 buf pos to_copy;
6868+ if to_copy < available then begin
6969+ let leftover = String.sub str to_copy (available - to_copy) in
7070+ Reader.push_back reader (Slice.of_string leftover)
7171+ end;
7272+ loop (pos + to_copy) (remaining - to_copy)
7373+ in
7474+ loop 0 n
7575+7676+module Geometry = struct
7777+ type t = { cylinders : int; heads : int; sectors : int }
7878+7979+ let kib = 1024L
8080+ let mib = Int64.mul kib 1024L
8181+ let sizeof = 3
8282+8383+ let unmarshal buf off : (t, _) result =
8484+ let* () =
8585+ if Bytes.length buf < off + sizeof then
8686+ Error
8787+ (Printf.sprintf "geometry too small: %d < %d"
8888+ (Bytes.length buf - off)
8989+ sizeof)
9090+ else Ok ()
9191+ in
9292+ let heads = get_u8 buf off in
9393+ let y = get_u8 buf (off + 1) in
9494+ let z = get_u8 buf (off + 2) in
9595+ let sectors = y land 0b0111111 in
9696+ let cylinders = (y lsl 2) lor z in
9797+ Ok { cylinders; heads; sectors }
9898+9999+ let of_lba_size x =
100100+ let sectors = 63 in
101101+ let* heads =
102102+ if x < Int64.(mul 504L mib) then Ok 16
103103+ else if x < Int64.(mul 1008L mib) then Ok 64
104104+ else if x < Int64.(mul 4032L mib) then Ok 128
105105+ else if x < Int64.(add (mul 8032L mib) (mul 512L kib)) then Ok 255
106106+ else Error (Printf.sprintf "sector count exceeds LBA max: %Ld" x)
107107+ in
108108+ let cylinders =
109109+ Int64.(to_int (div (div x (of_int sectors)) (of_int heads)))
110110+ in
111111+ Ok { cylinders; heads; sectors }
112112+113113+ let to_chs g x =
114114+ let open Int64 in
115115+ let cylinders = to_int (div x (mul (of_int g.sectors) (of_int g.heads))) in
116116+ let heads = to_int (rem (div x (of_int g.sectors)) (of_int g.heads)) in
117117+ let sectors = to_int (succ (rem x (of_int g.sectors))) in
118118+ { cylinders; heads; sectors }
119119+end
120120+121121+module Partition = struct
122122+ type t = {
123123+ active : bool;
124124+ first_absolute_sector_chs : Geometry.t;
125125+ ty : int;
126126+ last_absolute_sector_chs : Geometry.t;
127127+ first_absolute_sector_lba : int32;
128128+ sectors : int32;
129129+ }
130130+131131+ let sector_start t =
132132+ Int64.(logand (of_int32 t.first_absolute_sector_lba) 0xFFFF_FFFFL)
133133+134134+ let size_sectors t = Int64.(logand (of_int32 t.sectors) 0xFFFF_FFFFL)
135135+136136+ let make ?(active = false) ~partition_type:(ty : int)
137137+ first_absolute_sector_lba sectors =
138138+ let* () =
139139+ if ty > 0 && ty < 256 then Ok ()
140140+ else Error "Mbr.Partition.make: ty must be between 1 and 255"
141141+ in
142142+ let first_absolute_sector_chs =
143143+ { Geometry.cylinders = 0; heads = 0; sectors = 0 }
144144+ in
145145+ let last_absolute_sector_chs = first_absolute_sector_chs in
146146+ Ok
147147+ {
148148+ active;
149149+ first_absolute_sector_chs;
150150+ ty;
151151+ last_absolute_sector_chs;
152152+ first_absolute_sector_lba;
153153+ sectors;
154154+ }
155155+156156+ let make' ?active ~partition_type:(ty : int) sector_start size_sectors =
157157+ if
158158+ Int64.(
159159+ logand sector_start 0xFFFF_FFFFL = sector_start
160160+ && logand size_sectors 0xFFFF_FFFFL = size_sectors)
161161+ then
162162+ let sector_start = Int64.to_int32 sector_start in
163163+ let size_sectors = Int64.to_int32 size_sectors in
164164+ make ?active ~partition_type:ty sector_start size_sectors
165165+ else Error "partition parameters do not fit in int32"
166166+167167+ let sizeof = 16
168168+ let status_offset = 0
169169+ let first_absolute_sector_chs_offset = 1
170170+ let ty_offset = 4
171171+ let last_absolute_sector_chs_offset = 5
172172+ let first_absolute_sector_lba_offset = 8
173173+ let sectors_offset = 12
174174+175175+ let unmarshal buf off =
176176+ let* () =
177177+ if Bytes.length buf < off + sizeof then
178178+ Error
179179+ (Printf.sprintf "partition entry too small: %d < %d"
180180+ (Bytes.length buf - off)
181181+ sizeof)
182182+ else Ok ()
183183+ in
184184+ let ty = get_u8 buf (off + ty_offset) in
185185+ if ty == 0x00 then
186186+ if bytes_all_zero buf off sizeof then Ok None
187187+ else Error "Non-zero empty partition type"
188188+ else
189189+ let active = get_u8 buf (off + status_offset) = 0x80 in
190190+ let* first_absolute_sector_chs =
191191+ Geometry.unmarshal buf (off + first_absolute_sector_chs_offset)
192192+ in
193193+ let* last_absolute_sector_chs =
194194+ Geometry.unmarshal buf (off + last_absolute_sector_chs_offset)
195195+ in
196196+ let first_absolute_sector_lba =
197197+ get_u32_le buf (off + first_absolute_sector_lba_offset)
198198+ in
199199+ let sectors = get_u32_le buf (off + sectors_offset) in
200200+ Ok
201201+ (Some
202202+ {
203203+ active;
204204+ first_absolute_sector_chs;
205205+ ty;
206206+ last_absolute_sector_chs;
207207+ first_absolute_sector_lba;
208208+ sectors;
209209+ })
210210+211211+ let marshal (buf : bytes) off t =
212212+ set_u8 buf (off + status_offset) (if t.active then 0x80 else 0);
213213+ set_u8 buf (off + ty_offset) t.ty;
214214+ set_u32_le buf (off + first_absolute_sector_lba_offset)
215215+ t.first_absolute_sector_lba;
216216+ set_u32_le buf (off + sectors_offset) t.sectors
217217+end
218218+219219+type t = {
220220+ bootstrap_code : string;
221221+ original_physical_drive : int;
222222+ seconds : int;
223223+ minutes : int;
224224+ hours : int;
225225+ disk_signature : int32;
226226+ partitions : Partition.t list;
227227+}
228228+229229+let partitions t = t.partitions
230230+231231+let make ?(disk_signature = 0l) partitions =
232232+ let* () =
233233+ if List.length partitions <= 4 then Ok () else Error "Too many partitions"
234234+ in
235235+ let num_active =
236236+ List.fold_left
237237+ (fun acc p -> if p.Partition.active then succ acc else acc)
238238+ 0 partitions
239239+ in
240240+ let* () =
241241+ if num_active <= 1 then Ok ()
242242+ else Error "More than one active/boot partitions is not advisable"
243243+ in
244244+ let partitions =
245245+ List.sort
246246+ (fun p1 p2 ->
247247+ Int32.unsigned_compare p1.Partition.first_absolute_sector_lba
248248+ p2.Partition.first_absolute_sector_lba)
249249+ partitions
250250+ in
251251+ let* (_ : int32) =
252252+ List.fold_left
253253+ (fun r p ->
254254+ let* offset = r in
255255+ if
256256+ Int32.unsigned_compare offset p.Partition.first_absolute_sector_lba
257257+ <= 0
258258+ then
259259+ Ok
260260+ (Int32.add p.Partition.first_absolute_sector_lba p.Partition.sectors)
261261+ else Error "Partitions overlap")
262262+ (Ok 1l) partitions
263263+ in
264264+ let bootstrap_code = String.init (218 + 216) (Fun.const '\000') in
265265+ let original_physical_drive = 0 in
266266+ let seconds = 0 in
267267+ let minutes = 0 in
268268+ let hours = 0 in
269269+ Ok
270270+ {
271271+ bootstrap_code;
272272+ original_physical_drive;
273273+ seconds;
274274+ minutes;
275275+ hours;
276276+ disk_signature;
277277+ partitions;
278278+ }
279279+280280+(* MBR layout constants *)
281281+let sizeof = 512
282282+let bootstrap_code1_offset = 0
283283+let bootstrap_code1_len = 218
284284+let original_physical_drive_offset = 220
285285+let seconds_offset = 221
286286+let minutes_offset = 222
287287+let hours_offset = 223
288288+let bootstrap_code2_offset = 224
289289+let bootstrap_code2_len = 216
290290+let disk_signature_offset = 440
291291+let partitions_offset = 446
292292+let signature1_offset = 510
293293+let signature2_offset = 511
294294+295295+let partition_offset n =
296296+ assert (n >= 0 && n < 4);
297297+ partitions_offset + (n * Partition.sizeof)
298298+299299+let default_partition_start = 2048l
300300+301301+(* Internal unmarshal from bytes buffer *)
302302+let unmarshal_bytes (buf : bytes) : (t, string) result =
303303+ let* () =
304304+ if Bytes.length buf < sizeof then
305305+ Error (Printf.sprintf "MBR too small: %d < %d" (Bytes.length buf) sizeof)
306306+ else Ok ()
307307+ in
308308+ let signature1 = get_u8 buf signature1_offset in
309309+ let signature2 = get_u8 buf signature2_offset in
310310+ let* () =
311311+ if signature1 = 0x55 && signature2 = 0xaa then Ok ()
312312+ else
313313+ Error
314314+ (Printf.sprintf "Invalid signature: %02x %02x <> 0x55 0xaa" signature1
315315+ signature2)
316316+ in
317317+ let bootstrap_code =
318318+ Bytes.sub_string buf bootstrap_code1_offset bootstrap_code1_len
319319+ ^ Bytes.sub_string buf bootstrap_code2_offset bootstrap_code2_len
320320+ in
321321+ let original_physical_drive = get_u8 buf original_physical_drive_offset in
322322+ let seconds = get_u8 buf seconds_offset in
323323+ let minutes = get_u8 buf minutes_offset in
324324+ let hours = get_u8 buf hours_offset in
325325+ let disk_signature = get_u32_le buf disk_signature_offset in
326326+ let* p1 = Partition.unmarshal buf (partition_offset 0) in
327327+ let* p2 = Partition.unmarshal buf (partition_offset 1) in
328328+ let* p3 = Partition.unmarshal buf (partition_offset 2) in
329329+ let* p4 = Partition.unmarshal buf (partition_offset 3) in
330330+ let partitions = List.filter_map Fun.id [ p1; p2; p3; p4 ] in
331331+ Ok
332332+ {
333333+ bootstrap_code;
334334+ original_physical_drive;
335335+ seconds;
336336+ minutes;
337337+ hours;
338338+ disk_signature;
339339+ partitions;
340340+ }
341341+342342+(* Internal marshal to bytes buffer *)
343343+let marshal_bytes (buf : bytes) t =
344344+ Bytes.blit_string t.bootstrap_code 0 buf bootstrap_code1_offset
345345+ bootstrap_code1_len;
346346+ Bytes.blit_string t.bootstrap_code bootstrap_code1_len buf
347347+ bootstrap_code2_offset bootstrap_code2_len;
348348+ set_u8 buf original_physical_drive_offset t.original_physical_drive;
349349+ set_u8 buf seconds_offset t.seconds;
350350+ set_u8 buf minutes_offset t.minutes;
351351+ set_u8 buf hours_offset t.hours;
352352+ set_u32_le buf disk_signature_offset t.disk_signature;
353353+ List.iteri
354354+ (fun i p -> Partition.marshal buf (partition_offset i) p)
355355+ t.partitions;
356356+ set_u8 buf signature1_offset 0x55;
357357+ set_u8 buf signature2_offset 0xaa
358358+359359+(* Streaming API *)
360360+361361+let of_string s = unmarshal_bytes (Bytes.of_string s)
362362+363363+let to_string t =
364364+ let buf = Bytes.create sizeof in
365365+ marshal_bytes buf t;
366366+ Bytes.to_string buf
367367+368368+let read reader =
369369+ match read_exactly reader sizeof with
370370+ | Error e -> Error e
371371+ | Ok buf -> unmarshal_bytes buf
372372+373373+let write writer t =
374374+ let s = to_string t in
375375+ Writer.write writer (Slice.of_string s)
+117
lib/mbr.mli
···11+(*
22+ * Copyright (C) 2013 Citrix Inc
33+ *
44+ * Permission to use, copy, modify, and distribute this software for any
55+ * purpose with or without fee is hereby granted, provided that the above
66+ * copyright notice and this permission notice appear in all copies.
77+ *
88+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
99+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
1010+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
1111+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
1212+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1313+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1414+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1515+ *)
1616+1717+(** MBR (Master Boot Record) partition table format. *)
1818+1919+module Geometry : sig
2020+ type t = { cylinders : int; heads : int; sectors : int }
2121+ (** Represents a sector address using the cylinder-heads-sectors addressing
2222+ scheme. *)
2323+2424+ val of_lba_size : int64 -> (t, string) result
2525+ (** For LBA addressable disks of < 8GiB, synthesise a plausible geometry given
2626+ a total number of sectors. *)
2727+2828+ val to_chs : t -> int64 -> t
2929+ (** Given a geometry and an LBA offset, compute the CHS of the offset. *)
3030+end
3131+3232+module Partition : sig
3333+ type t = private {
3434+ active : bool;
3535+ (** true means the partition is active, also known as bootable *)
3636+ first_absolute_sector_chs : Geometry.t;
3737+ (** the CHS address of the first data sector. This is only used by
3838+ BIOSes with pre-LBA disks (< 1996). This will not be marshalled. *)
3939+ ty : int; (** the advertised filesystem type *)
4040+ last_absolute_sector_chs : Geometry.t;
4141+ (** the CHS address of the last data sector. This is only used by BIOSes
4242+ with pre-LBA disks (< 1996). This will not be marshalled. *)
4343+ first_absolute_sector_lba : int32;
4444+ (** the Logical Block Address (LBA) of the first data sector. This is
4545+ the absolute sector offset of the first data sector. *)
4646+ sectors : int32; (** the total number of sectors in the partition *)
4747+ }
4848+ (** A primary partition within the partition table. *)
4949+5050+ val sector_start : t -> int64
5151+ (** [sector_start t] is the int64 representation of
5252+ [t.first_absolute_sector_lba]. *)
5353+5454+ val size_sectors : t -> int64
5555+ (** [size_sectors t] is the int64 representation of [t.sectors]. *)
5656+5757+ val make :
5858+ ?active:bool -> partition_type:int -> int32 -> int32 -> (t, string) result
5959+ (** [make ?active ~partition_type start length] creates a partition starting
6060+ at sector [start] and with length [length] sectors. If the active flag is
6161+ set then the partition will be marked as active/bootable. Partition type
6262+ [ty] determines the advertised filesystem type. [ty] must be between 1 and
6363+ 255. *)
6464+6565+ val make' :
6666+ ?active:bool -> partition_type:int -> int64 -> int64 -> (t, string) result
6767+ (** [make' ?active ~partition_type sector_start size_sectors] is
6868+ [make ?active ~partition_type (Int64.to_int32 sector_start)
6969+ (Int64.to_int32 size_sectors)] when both [sector_start] and [size_sectors]
7070+ fit in int32. Otherwise [Error _]. *)
7171+end
7272+7373+type t = private {
7474+ bootstrap_code : string;
7575+ original_physical_drive : int;
7676+ seconds : int;
7777+ minutes : int;
7878+ hours : int;
7979+ disk_signature : int32;
8080+ partitions : Partition.t list;
8181+}
8282+(** An MBR record. *)
8383+8484+val make : ?disk_signature:int32 -> Partition.t list -> (t, string) result
8585+(** [make ?disk_signature partitions] constructs an MBR given a desired list of
8686+ primary partitions. An [Error _] is returned if:
8787+8888+ - The number of partitions exceeds four,
8989+ - Any of the partitions overlap with each other or the first sector,
9090+ - More than one partition is marked as active (bootable).
9191+9292+ The optional argument [disk_signature] specifies the disk signature to be
9393+ written in the MBR. If [disk_signature] is not provided, the default value
9494+ of [0l] is used. *)
9595+9696+val partitions : t -> Partition.t list
9797+(** [partitions t] returns the list of partitions in the MBR. *)
9898+9999+val sizeof : int
100100+(** [sizeof] is the size of a master boot record in bytes (512 bytes). *)
101101+102102+val default_partition_start : int32
103103+(** Default sector offset for first partition. *)
104104+105105+(** {1 Streaming API} *)
106106+107107+val of_string : string -> (t, string) result
108108+(** [of_string s] parses an MBR from a string. *)
109109+110110+val to_string : t -> string
111111+(** [to_string t] serializes an MBR to a string. *)
112112+113113+val read : Bytesrw.Bytes.Reader.t -> (t, string) result
114114+(** [read reader] reads an MBR from a bytesrw reader. *)
115115+116116+val write : Bytesrw.Bytes.Writer.t -> t -> unit
117117+(** [write writer t] writes an MBR to a bytesrw writer. *)
+36
mbr-format.opam
···11+# This file is generated by dune, edit dune-project instead
22+opam-version: "2.0"
33+synopsis: "A library to manipulate Master Boot Records"
44+description: """
55+Pure OCaml library for reading and writing Master Boot Records (MBR).
66+Useful for creating bootable disk images and reading partition tables."""
77+maintainer: ["Thomas Gazagnaire <thomas@gazagnaire.org>"]
88+authors: [
99+ "David Scott <dave.scott@eu.citrix.com>"
1010+ "Reynir Björnsson <reynir@reynir.dk>"
1111+]
1212+license: "ISC"
1313+bug-reports: "https://tangled.org/gazagnaire.org/ocaml-mbr/issues"
1414+depends: [
1515+ "dune" {>= "3.17"}
1616+ "ocaml" {>= "5.1"}
1717+ "bytesrw" {>= "0.1"}
1818+ "alcotest" {with-test}
1919+ "crowbar" {with-test}
2020+ "odoc" {with-doc}
2121+]
2222+build: [
2323+ ["dune" "subst"] {dev}
2424+ [
2525+ "dune"
2626+ "build"
2727+ "-p"
2828+ name
2929+ "-j"
3030+ jobs
3131+ "@install"
3232+ "@runtest" {with-test}
3333+ "@doc" {with-doc}
3434+ ]
3535+]
3636+dev-repo: "https://tangled.org/gazagnaire.org/ocaml-mbr"