update
This commit is contained in:
@@ -0,0 +1,416 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"gate": "G02",
|
||||
"candidate": "P15-005",
|
||||
"required_base": "edf098905a34de764185e72fc7e92d7b8f4e7285",
|
||||
"title": "metadata buffer ownership and frozen map tuple oracle",
|
||||
"required_categories": [
|
||||
"plain",
|
||||
"inline",
|
||||
"hole",
|
||||
"chunk",
|
||||
"multidevice",
|
||||
"compressed",
|
||||
"fragment",
|
||||
"partial-reference",
|
||||
"post-EOF",
|
||||
"bounds",
|
||||
"overflow"
|
||||
],
|
||||
"tuple_fields": [
|
||||
"m_la",
|
||||
"m_pa",
|
||||
"m_llen",
|
||||
"m_plen",
|
||||
"m_deviceid",
|
||||
"m_flags",
|
||||
"errno",
|
||||
"acquire_count",
|
||||
"release_count"
|
||||
],
|
||||
"errno": {
|
||||
"EIO": 5,
|
||||
"EINVAL": 22,
|
||||
"EOPNOTSUPP": 45,
|
||||
"EOVERFLOW": 84,
|
||||
"EINTEGRITY": 97
|
||||
},
|
||||
"source_paths": [
|
||||
"repo-pre-15/src/internal.h",
|
||||
"repo-pre-15/src/data.c",
|
||||
"repo-pre-15/src/decompressor.c",
|
||||
"repo-pre-15/src/inode.c",
|
||||
"repo-pre-15/src/super.c",
|
||||
"repo-pre-15/src/xattr.c",
|
||||
"repo-pre-15/src/zdata.c",
|
||||
"repo-pre-15/src/zmap.c",
|
||||
"repo-pre-15/src/erofs_fs.h",
|
||||
"src-linux/internal.h",
|
||||
"src-linux/data.c",
|
||||
"src-linux/decompressor.c",
|
||||
"src-linux/inode.c",
|
||||
"src-linux/super.c",
|
||||
"src-linux/xattr.c",
|
||||
"src-linux/zdata.c",
|
||||
"src-linux/zmap.c"
|
||||
],
|
||||
"linux_sha256": {
|
||||
"src-linux/internal.h": "4aa671896ff7c0ad32a9108c818ef62d16841c116706fdad391c40a530c81405",
|
||||
"src-linux/data.c": "8625cdc01e5405f856178ae8fd559696ae85f607f19caf229b867a3b7479318a",
|
||||
"src-linux/decompressor.c": "caf1c501d00a5a2c9cda5fc0b59d2823eaedf0161a130ca69cd5e7c455128709",
|
||||
"src-linux/inode.c": "a15562e0782e155a0744a7ba9d2f557519a583b64853ed75180cef2b4dfae1b3",
|
||||
"src-linux/super.c": "8bda458cca758d8aa9c5a5b05361b2131b896f73fd194f6ad9a8e011e3481bf9",
|
||||
"src-linux/xattr.c": "c8394e5f6301225cbe7587f223485a368348eac7596c1ab0bbf965c99655ed4a",
|
||||
"src-linux/zdata.c": "358869da60dcdafd13bc1ff0cd28e864c13b46a49d9f556d9b37139f7e0f4e79",
|
||||
"src-linux/zmap.c": "4fb34c024bfaec9d2294ba2f85069bb2f727b378ebf4848814e081f91ea022cc"
|
||||
},
|
||||
"linux_semantic_anchors": {
|
||||
"src-linux/internal.h": [
|
||||
"struct erofs_buf {",
|
||||
"#define __EROFS_BUF_INITIALIZER",
|
||||
"void erofs_put_metabuf(struct erofs_buf *buf);"
|
||||
],
|
||||
"src-linux/data.c": [
|
||||
"void erofs_put_metabuf(struct erofs_buf *buf)",
|
||||
"folio_put(page_folio(buf->page));",
|
||||
"buf->page = NULL;",
|
||||
"struct erofs_buf buf = __EROFS_BUF_INITIALIZER;",
|
||||
"erofs_put_metabuf(&buf);"
|
||||
],
|
||||
"src-linux/decompressor.c": [
|
||||
"struct erofs_buf buf = __EROFS_BUF_INITIALIZER;",
|
||||
"erofs_put_metabuf(&buf);"
|
||||
],
|
||||
"src-linux/inode.c": [
|
||||
"struct erofs_buf buf = __EROFS_BUF_INITIALIZER;",
|
||||
"erofs_put_metabuf(&buf);"
|
||||
],
|
||||
"src-linux/super.c": [
|
||||
"struct erofs_buf buf = __EROFS_BUF_INITIALIZER;",
|
||||
"erofs_put_metabuf(&buf);"
|
||||
],
|
||||
"src-linux/xattr.c": [
|
||||
"struct erofs_buf buf = __EROFS_BUF_INITIALIZER;",
|
||||
"erofs_put_metabuf(&buf);"
|
||||
],
|
||||
"src-linux/zdata.c": [
|
||||
"struct erofs_buf buf = __EROFS_BUF_INITIALIZER;",
|
||||
"erofs_put_metabuf(&buf);"
|
||||
],
|
||||
"src-linux/zmap.c": [
|
||||
"erofs_put_metabuf(&map.buf);"
|
||||
]
|
||||
},
|
||||
"baseline_function_sha256": {
|
||||
"data.c:erofs_map_blocks_chunk": "b850ab686f61e6265ef0780c1363f570ceb6afab004b5d9c527d728f00a0fa0e",
|
||||
"data.c:erofs_read_metadata": "1ea2f9bad3c60019c0f373c98444dcd1dd4bb178875a170313868a56cd314436",
|
||||
"data.c:erofs_read_data": "9d962f6086f5fc4a66bbfd9b27d05aa71234bb8266a3c383ec693298966e5e63",
|
||||
"data.c:erofs_read_uio": "05d3ad9e8b25b7e9b6d62f4fd836ab204bc57b90781fc140cc498d1ab8fe0a78",
|
||||
"decompressor.c:z_erofs_read_cfg": "77ddaf24f5317e0b83b72d85f64aa624cbd15b71a9602d2217b11dd382e1f719",
|
||||
"decompressor.c:z_erofs_parse_cfgs": "d800c9a52b9f9333f57da2f67e27d238320866b0f8816929b32f119c84a69eb4",
|
||||
"inode.c:erofs_read_inode": "ed65faffe7a607e95bca50de7e28cf592a0500b7363935991de959269a200fe1",
|
||||
"super.c:erofs_load_generation_seed": "98299634491a22b427820a28a7ec1d23268ab1fe02d1fc0515774da70900f071",
|
||||
"super.c:erofs_superblock_csum_verify": "cd56c29a6cf795de359f6642509a4a152e5a1860ec32a997e91ebf81e7877096",
|
||||
"super.c:erofs_scan_devices": "2743af3b9d9d7e53c0102bc197026adf58ef6bf7adbf5cdbe8716c9e25b2a547",
|
||||
"super.c:erofs_read_superblock": "d605349d9604d89d4bb413c42abc71f13b8fafa43b1fda671fbd4613f2a3ed6f",
|
||||
"xattr.c:erofs_xattr_read_backing": "df65cf9e391d2f17f79beeecda24fbf6c1be495dd3baf65121c226c4054ccf75",
|
||||
"xattr.c:erofs_xattr_read_metadata": "ed726755b28f1c1a82a07a956b889bd633da45cdd6671c91c7bc4db9d4006096",
|
||||
"xattr.c:erofs_xattr_load_body": "c8684629f78920cefb6a6d61ddad5ed42ba499d817798f81086282c827e54aa5",
|
||||
"xattr.c:erofs_xattr_load_shared_entry": "e47145a775685cfaeb9341a7cc1cc742c73145e34f4b81d8fc5dac35d765e17f",
|
||||
"xattr.c:erofs_inode_has_noacl": "8098be9fef8148498a2080e02db2876d9592f1af10ca551278902e3aa4c3428e",
|
||||
"xattr.c:erofs_xattr_iter_shared": "1ec7756ef31dd6c51e04d92656440a9dbca5e9e9fe38c359eed744ad16b39b2c",
|
||||
"xattr.c:erofs_getxattr": "9ee7d7effcfa3d23ab4d4ce1e3e7d638c048085368b2f3a2db3dfdadf911cd34",
|
||||
"xattr.c:erofs_listxattr": "6f75c9f8c50c331daba1bb0c1b757390c3ff873ee7419b2dd2b35dfc3db84ea6",
|
||||
"xattr.c:erofs_xattr_prefixes_init": "7aaade8116337ad1026da04777bf1d9f8aefaa246623d6c32080af153fb93abd",
|
||||
"zdata.c:z_erofs_read_extent": "327e3a22b9c89c410d1ecef6934dcdaf194a4b6962737ea5f3e4f21d10694b13",
|
||||
"zdata.c:z_erofs_do_read": "2096f98b6d19e1c5f80edb6de9fbfe15bb27f006e2510bcb068f920c23aa38ec",
|
||||
"zmap.c:z_erofs_read_index": "cf8ca75c1d600f2f708e73e723a7abaee7c98f9813eb8f432e1f96dd1602d82b",
|
||||
"zmap.c:z_erofs_load_full_lcluster": "751d456964fcdcf66f20d22c1f22adef209767aef1bab9d0857f36df7d2c37b2",
|
||||
"zmap.c:z_erofs_load_compact_lcluster": "4c23a8a966262b5a365bc874718316daa616cc97dfe3e2711570f81e7e3b953c",
|
||||
"zmap.c:z_erofs_read_extent": "454064ca59dfd0674af5520c4a965be659d2b7769403cd8d84bd9657e0145f27",
|
||||
"zmap.c:z_erofs_map_blocks_ext": "31c9310f801395fe01d38515ee42ef49c1d07ceba0b86b612bb140ec180238b0",
|
||||
"zmap.c:z_erofs_fill_inode": "125f0c3492c152415978226919e782d355a32fc49cf8f49456eb398a4c721032"
|
||||
},
|
||||
"direct_metadata_functions": [
|
||||
"data.c:erofs_map_blocks_chunk",
|
||||
"data.c:erofs_read_data",
|
||||
"data.c:erofs_read_uio",
|
||||
"decompressor.c:z_erofs_read_cfg",
|
||||
"inode.c:erofs_read_inode",
|
||||
"super.c:erofs_load_generation_seed",
|
||||
"super.c:erofs_superblock_csum_verify",
|
||||
"super.c:erofs_scan_devices",
|
||||
"super.c:erofs_read_superblock",
|
||||
"xattr.c:erofs_xattr_read_backing",
|
||||
"zdata.c:z_erofs_read_extent",
|
||||
"zmap.c:z_erofs_fill_inode",
|
||||
"zmap.c:z_erofs_map_blocks_ext",
|
||||
"zmap.c:z_erofs_read_extent",
|
||||
"zmap.c:z_erofs_read_index"
|
||||
],
|
||||
"candidate_ownership_contract": {
|
||||
"object_local_counts": {
|
||||
"data.c:erofs_map_blocks_chunk": 1,
|
||||
"data.c:erofs_read_metadata": 0,
|
||||
"data.c:erofs_read_data": 1,
|
||||
"data.c:erofs_read_uio": 1,
|
||||
"decompressor.c:z_erofs_read_cfg": 1,
|
||||
"decompressor.c:z_erofs_parse_cfgs": 1,
|
||||
"inode.c:erofs_read_inode": 1,
|
||||
"super.c:erofs_load_generation_seed": 1,
|
||||
"super.c:erofs_superblock_csum_verify": 1,
|
||||
"super.c:erofs_scan_devices": 1,
|
||||
"super.c:erofs_read_superblock": 1,
|
||||
"xattr.c:erofs_xattr_read_backing": 0,
|
||||
"xattr.c:erofs_xattr_read_metadata": 1,
|
||||
"xattr.c:erofs_xattr_load_body": 1,
|
||||
"xattr.c:erofs_xattr_load_shared_entry": 1,
|
||||
"xattr.c:erofs_inode_has_noacl": 1,
|
||||
"xattr.c:erofs_xattr_iter_shared": 1,
|
||||
"xattr.c:erofs_getxattr": 1,
|
||||
"xattr.c:erofs_listxattr": 1,
|
||||
"xattr.c:erofs_xattr_prefixes_init": 1,
|
||||
"zdata.c:z_erofs_read_extent": 1,
|
||||
"zdata.c:z_erofs_do_read": 0,
|
||||
"zmap.c:z_erofs_read_index": 0,
|
||||
"zmap.c:z_erofs_load_full_lcluster": 1,
|
||||
"zmap.c:z_erofs_load_compact_lcluster": 1,
|
||||
"zmap.c:z_erofs_read_extent": 1,
|
||||
"zmap.c:z_erofs_map_blocks_ext": 1,
|
||||
"zmap.c:z_erofs_fill_inode": 1
|
||||
},
|
||||
"put_counts": {
|
||||
"data.c:erofs_map_blocks_chunk": 1,
|
||||
"data.c:erofs_read_metadata": 0,
|
||||
"data.c:erofs_read_data": 1,
|
||||
"data.c:erofs_read_uio": 1,
|
||||
"decompressor.c:z_erofs_read_cfg": 1,
|
||||
"decompressor.c:z_erofs_parse_cfgs": 1,
|
||||
"inode.c:erofs_read_inode": 18,
|
||||
"super.c:erofs_load_generation_seed": 1,
|
||||
"super.c:erofs_superblock_csum_verify": 1,
|
||||
"super.c:erofs_scan_devices": 2,
|
||||
"super.c:erofs_read_superblock": 1,
|
||||
"xattr.c:erofs_xattr_read_backing": 0,
|
||||
"xattr.c:erofs_xattr_read_metadata": 1,
|
||||
"xattr.c:erofs_xattr_load_body": 1,
|
||||
"xattr.c:erofs_xattr_load_shared_entry": 1,
|
||||
"xattr.c:erofs_inode_has_noacl": 1,
|
||||
"xattr.c:erofs_xattr_iter_shared": 1,
|
||||
"xattr.c:erofs_getxattr": 1,
|
||||
"xattr.c:erofs_listxattr": 1,
|
||||
"xattr.c:erofs_xattr_prefixes_init": 2,
|
||||
"zdata.c:z_erofs_read_extent": 1,
|
||||
"zdata.c:z_erofs_do_read": 0,
|
||||
"zmap.c:z_erofs_read_index": 0,
|
||||
"zmap.c:z_erofs_load_full_lcluster": 2,
|
||||
"zmap.c:z_erofs_load_compact_lcluster": 7,
|
||||
"zmap.c:z_erofs_read_extent": 1,
|
||||
"zmap.c:z_erofs_map_blocks_ext": 1,
|
||||
"zmap.c:z_erofs_fill_inode": 6
|
||||
},
|
||||
"legacy_release_counts": {
|
||||
"data.c:erofs_read_data": 1,
|
||||
"data.c:erofs_read_uio": 1,
|
||||
"zdata.c:z_erofs_read_extent": 1,
|
||||
"zdata.c:z_erofs_do_read": 1
|
||||
},
|
||||
"raw_bread_counts": {
|
||||
"data.c:erofs_read_metadata": 1,
|
||||
"xattr.c:erofs_xattr_read_backing": 1
|
||||
},
|
||||
"acquire_apis": [
|
||||
"erofs_read_metadata",
|
||||
"z_erofs_read_cfg",
|
||||
"erofs_xattr_read_backing",
|
||||
"erofs_xattr_read_metadata",
|
||||
"erofs_xattr_load_body",
|
||||
"erofs_xattr_load_shared_entry",
|
||||
"z_erofs_read_index"
|
||||
],
|
||||
"transfer_functions": [
|
||||
"data.c:erofs_read_metadata",
|
||||
"decompressor.c:z_erofs_read_cfg",
|
||||
"xattr.c:erofs_xattr_read_backing",
|
||||
"xattr.c:erofs_xattr_read_metadata",
|
||||
"zmap.c:z_erofs_read_index"
|
||||
]
|
||||
},
|
||||
"tuple_cases": [
|
||||
{
|
||||
"id": "plain-block-edge",
|
||||
"engine": "data",
|
||||
"category": "plain",
|
||||
"sbi": {"block_size": 4096, "blkszbits": 12, "blocks": 64, "device_id_mask": 65535},
|
||||
"inode": {"nid": 1, "size": 12288, "datalayout": 0, "startblk": 32, "inode_off": 0, "inode_isize": 32, "xattr_isize": 0, "chunkbits": 12, "chunkformat": 0},
|
||||
"request": 4095,
|
||||
"reader": {"mode": "unused", "bytes_hex": ""},
|
||||
"expected": {"m_la": 4095, "m_pa": 135167, "m_llen": 1, "m_plen": 1, "m_deviceid": 0, "m_flags": 1, "errno": 0, "acquire_count": 0, "release_count": 0}
|
||||
},
|
||||
{
|
||||
"id": "inline-tail",
|
||||
"engine": "data",
|
||||
"category": "inline",
|
||||
"sbi": {"block_size": 4096, "blkszbits": 12, "blocks": 64, "device_id_mask": 65535},
|
||||
"inode": {"nid": 1, "size": 5000, "datalayout": 2, "startblk": 40, "inode_off": 8192, "inode_isize": 64, "xattr_isize": 32, "chunkbits": 12, "chunkformat": 0},
|
||||
"request": 4500,
|
||||
"reader": {"mode": "unused", "bytes_hex": ""},
|
||||
"expected": {"m_la": 4500, "m_pa": 8692, "m_llen": 500, "m_plen": 500, "m_deviceid": 0, "m_flags": 3, "errno": 0, "acquire_count": 0, "release_count": 0}
|
||||
},
|
||||
{
|
||||
"id": "plain-hole",
|
||||
"engine": "data",
|
||||
"category": "hole",
|
||||
"sbi": {"block_size": 4096, "blkszbits": 12, "blocks": 64, "device_id_mask": 65535},
|
||||
"inode": {"nid": 1, "size": 4096, "datalayout": 0, "startblk": 18446744073709551615, "inode_off": 0, "inode_isize": 32, "xattr_isize": 0, "chunkbits": 12, "chunkformat": 0},
|
||||
"request": 512,
|
||||
"reader": {"mode": "unused", "bytes_hex": ""},
|
||||
"expected": {"m_la": 512, "m_pa": 0, "m_llen": 3584, "m_plen": 3584, "m_deviceid": 0, "m_flags": 0, "errno": 0, "acquire_count": 0, "release_count": 0}
|
||||
},
|
||||
{
|
||||
"id": "chunk-index",
|
||||
"engine": "data",
|
||||
"category": "chunk",
|
||||
"sbi": {"block_size": 4096, "blkszbits": 12, "blocks": 128, "device_id_mask": 65535},
|
||||
"inode": {"nid": 1, "size": 20000, "datalayout": 4, "startblk": 0, "inode_off": 0, "inode_isize": 64, "xattr_isize": 0, "chunkbits": 13, "chunkformat": 32},
|
||||
"request": 9000,
|
||||
"reader": {"mode": "ok", "bytes_hex": "0000000000000000000000004d000000"},
|
||||
"expected": {"m_la": 9000, "m_pa": 316200, "m_llen": 7384, "m_plen": 7384, "m_deviceid": 0, "m_flags": 1, "errno": 0, "acquire_count": 1, "release_count": 1}
|
||||
},
|
||||
{
|
||||
"id": "chunk-48bit-device2",
|
||||
"engine": "data",
|
||||
"category": "multidevice",
|
||||
"sbi": {"block_size": 4096, "blkszbits": 12, "blocks": 128, "device_id_mask": 65535},
|
||||
"inode": {"nid": 1, "size": 24576, "datalayout": 4, "startblk": 0, "inode_off": 0, "inode_isize": 64, "xattr_isize": 0, "chunkbits": 13, "chunkformat": 96},
|
||||
"request": 12345,
|
||||
"reader": {"mode": "ok", "bytes_hex": "00000000000000000100020005000000"},
|
||||
"expected": {"m_la": 12345, "m_pa": 17592186069049, "m_llen": 4039, "m_plen": 4039, "m_deviceid": 2, "m_flags": 1, "errno": 0, "acquire_count": 1, "release_count": 1}
|
||||
},
|
||||
{
|
||||
"id": "chunk-index-out-of-provider",
|
||||
"engine": "data",
|
||||
"category": "bounds",
|
||||
"sbi": {"block_size": 4096, "blkszbits": 12, "blocks": 2, "device_id_mask": 65535},
|
||||
"inode": {"nid": 1, "size": 4096, "datalayout": 4, "startblk": 0, "inode_off": 8192, "inode_isize": 0, "xattr_isize": 0, "chunkbits": 12, "chunkformat": 32},
|
||||
"request": 0,
|
||||
"reader": {"mode": "unused", "bytes_hex": ""},
|
||||
"expected": {"m_la": 0, "m_pa": 0, "m_llen": 0, "m_plen": 0, "m_deviceid": 0, "m_flags": 0, "errno": 97, "acquire_count": 0, "release_count": 0}
|
||||
},
|
||||
{
|
||||
"id": "chunk-shift-overflow-after-acquire",
|
||||
"engine": "data",
|
||||
"category": "overflow",
|
||||
"sbi": {"block_size": 131072, "blkszbits": 17, "blocks": 1, "device_id_mask": 65535},
|
||||
"inode": {"nid": 1, "size": 4096, "datalayout": 4, "startblk": 0, "inode_off": 0, "inode_isize": 64, "xattr_isize": 0, "chunkbits": 12, "chunkformat": 96},
|
||||
"request": 0,
|
||||
"reader": {"mode": "ok", "bytes_hex": "0080000000000000"},
|
||||
"expected": {"m_la": 0, "m_pa": 0, "m_llen": 4096, "m_plen": 4096, "m_deviceid": 0, "m_flags": 0, "errno": 84, "acquire_count": 1, "release_count": 1}
|
||||
},
|
||||
{
|
||||
"id": "chunk-index-short-read",
|
||||
"engine": "data",
|
||||
"category": "bounds",
|
||||
"supplemental": true,
|
||||
"sbi": {"block_size": 4096, "blkszbits": 12, "blocks": 128, "device_id_mask": 65535},
|
||||
"inode": {"nid": 1, "size": 4096, "datalayout": 4, "startblk": 0, "inode_off": 0, "inode_isize": 64, "xattr_isize": 0, "chunkbits": 12, "chunkformat": 32},
|
||||
"request": 0,
|
||||
"reader": {"mode": "short", "bytes_hex": "00000000"},
|
||||
"expected": {"m_la": 0, "m_pa": 0, "m_llen": 0, "m_plen": 0, "m_deviceid": 0, "m_flags": 0, "errno": 5, "acquire_count": 0, "release_count": 0}
|
||||
},
|
||||
{
|
||||
"id": "explicit-compressed",
|
||||
"engine": "zmap",
|
||||
"category": "compressed",
|
||||
"sbi": {"block_size": 4096, "blkszbits": 12, "available_compr_algs": 1, "packed_size": 16384},
|
||||
"inode": {"nid": 2, "size": 4096, "datalayout": 1, "inode_off": 0, "inode_isize": 0, "xattr_isize": 0, "z_advise": 5, "z_lclusterbits": 12, "z_extents": 1},
|
||||
"request": 0,
|
||||
"reader": {"mode": "ok", "base": 16, "records": [{"plen": 268437504, "pstart": 74565, "lstart": 0}]},
|
||||
"expected": {"m_la": 0, "m_pa": 74565, "m_llen": 4096, "m_plen": 2048, "m_deviceid": 0, "m_flags": 1, "errno": 0, "acquire_count": 1, "release_count": 1}
|
||||
},
|
||||
{
|
||||
"id": "explicit-partial-reference",
|
||||
"engine": "zmap",
|
||||
"category": "partial-reference",
|
||||
"sbi": {"block_size": 4096, "blkszbits": 12, "available_compr_algs": 1, "packed_size": 16384},
|
||||
"inode": {"nid": 2, "size": 4096, "datalayout": 1, "inode_off": 0, "inode_isize": 0, "xattr_isize": 0, "z_advise": 5, "z_lclusterbits": 12, "z_extents": 1},
|
||||
"request": 0,
|
||||
"reader": {"mode": "ok", "base": 16, "records": [{"plen": 402654208, "pstart": 262144, "lstart": 0}]},
|
||||
"expected": {"m_la": 0, "m_pa": 262144, "m_llen": 4096, "m_plen": 1024, "m_deviceid": 0, "m_flags": 9, "errno": 0, "acquire_count": 1, "release_count": 1}
|
||||
},
|
||||
{
|
||||
"id": "explicit-fragment-tail",
|
||||
"engine": "zmap",
|
||||
"category": "fragment",
|
||||
"sbi": {"block_size": 4096, "blkszbits": 12, "available_compr_algs": 1, "packed_size": 16384},
|
||||
"inode": {"nid": 2, "size": 6144, "datalayout": 1, "inode_off": 0, "inode_isize": 0, "xattr_isize": 0, "z_advise": 37, "z_lclusterbits": 12, "z_extents": 1},
|
||||
"request": 4096,
|
||||
"reader": {"mode": "ok", "base": 16, "records": [{"plen": 8192, "pstart": 0, "lstart": 4096}]},
|
||||
"expected": {"m_la": 4096, "m_pa": 0, "m_llen": 2048, "m_plen": 8192, "m_deviceid": 0, "m_flags": 16, "errno": 0, "acquire_count": 1, "release_count": 1}
|
||||
},
|
||||
{
|
||||
"id": "compressed-post-eof",
|
||||
"engine": "zmap",
|
||||
"category": "post-EOF",
|
||||
"sbi": {"block_size": 4096, "blkszbits": 12, "available_compr_algs": 1, "packed_size": 16384},
|
||||
"inode": {"nid": 2, "size": 4096, "datalayout": 1, "inode_off": 0, "inode_isize": 0, "xattr_isize": 0, "z_advise": 5, "z_lclusterbits": 12, "z_extents": 1},
|
||||
"request": 4113,
|
||||
"reader": {"mode": "unused", "base": 16, "records": []},
|
||||
"expected": {"m_la": 4096, "m_pa": 0, "m_llen": 18, "m_plen": 0, "m_deviceid": 0, "m_flags": 0, "errno": 0, "acquire_count": 0, "release_count": 0}
|
||||
},
|
||||
{
|
||||
"id": "compressed-index-short-read",
|
||||
"engine": "zmap",
|
||||
"category": "compressed",
|
||||
"supplemental": true,
|
||||
"sbi": {"block_size": 4096, "blkszbits": 12, "available_compr_algs": 1, "packed_size": 16384},
|
||||
"inode": {"nid": 2, "size": 4096, "datalayout": 1, "inode_off": 0, "inode_isize": 0, "xattr_isize": 0, "z_advise": 5, "z_lclusterbits": 12, "z_extents": 1},
|
||||
"request": 0,
|
||||
"reader": {"mode": "short", "base": 16, "bytes_hex": "0000000000000000"},
|
||||
"expected": {"m_la": 0, "m_pa": 0, "m_llen": 0, "m_plen": 0, "m_deviceid": 0, "m_flags": 0, "errno": 5, "acquire_count": 0, "release_count": 0}
|
||||
},
|
||||
{
|
||||
"id": "compressed-sanity-error-after-acquire",
|
||||
"engine": "zmap",
|
||||
"category": "compressed",
|
||||
"supplemental": true,
|
||||
"sbi": {"block_size": 4096, "blkszbits": 12, "available_compr_algs": 1, "packed_size": 16384},
|
||||
"inode": {"nid": 2, "size": 4096, "datalayout": 1, "inode_off": 0, "inode_isize": 0, "xattr_isize": 0, "z_advise": 5, "z_lclusterbits": 12, "z_extents": 1},
|
||||
"request": 0,
|
||||
"reader": {"mode": "ok", "base": 16, "records": [{"plen": 1879049216, "pstart": 74565, "lstart": 0}]},
|
||||
"expected": {"m_la": 0, "m_pa": 74565, "m_llen": 0, "m_plen": 1024, "m_deviceid": 0, "m_flags": 1, "errno": 45, "acquire_count": 1, "release_count": 1}
|
||||
}
|
||||
],
|
||||
"ownership_paths": [
|
||||
{"id": "plain-inline-success", "consumer": "plain", "function": "data.c:erofs_read_data", "path": "metadata read succeeds; copied raw output is caller-owned", "acquire_count": 1, "release_count": 1},
|
||||
{"id": "plain-inline-short-read", "consumer": "plain", "function": "data.c:erofs_read_data", "path": "metadata read fails before ownership transfer", "acquire_count": 0, "release_count": 0},
|
||||
{"id": "map-chunk-success", "consumer": "map", "function": "data.c:erofs_map_blocks_chunk", "path": "index acquired, decoded, and released", "acquire_count": 1, "release_count": 1},
|
||||
{"id": "map-chunk-post-acquire-error", "consumer": "map", "function": "data.c:erofs_map_blocks_chunk", "path": "index released before physical shift overflow", "acquire_count": 1, "release_count": 1},
|
||||
{"id": "map-chunk-short-read", "consumer": "map", "function": "data.c:erofs_map_blocks_chunk", "path": "short index fails before acquire", "acquire_count": 0, "release_count": 0},
|
||||
{"id": "compressed-config-success", "consumer": "compressed", "function": "decompressor.c:z_erofs_read_cfg", "path": "length released; payload transferred then released by parse_cfgs", "acquire_count": 2, "release_count": 2},
|
||||
{"id": "compressed-config-second-short", "consumer": "compressed", "function": "decompressor.c:z_erofs_read_cfg", "path": "length released; payload not acquired", "acquire_count": 1, "release_count": 1},
|
||||
{"id": "compressed-data-decode-error", "consumer": "compressed", "function": "zdata.c:z_erofs_read_extent", "path": "metadata input released after decoder error", "acquire_count": 1, "release_count": 1},
|
||||
{"id": "compressed-map-success", "consumer": "compressed", "function": "zmap.c:z_erofs_read_extent", "path": "extent record copied then released", "acquire_count": 1, "release_count": 1},
|
||||
{"id": "compressed-map-short", "consumer": "compressed", "function": "zmap.c:z_erofs_read_extent", "path": "short record fails before acquire", "acquire_count": 0, "release_count": 0},
|
||||
{"id": "inode-compact-success", "consumer": "inode", "function": "inode.c:erofs_read_inode", "path": "compact inode decoded and released", "acquire_count": 1, "release_count": 1},
|
||||
{"id": "inode-invalid-format", "consumer": "inode", "function": "inode.c:erofs_read_inode", "path": "first acquired inode released on format rejection", "acquire_count": 1, "release_count": 1},
|
||||
{"id": "inode-extended-second-short", "consumer": "inode", "function": "inode.c:erofs_read_inode", "path": "compact probe released; extended read not acquired", "acquire_count": 1, "release_count": 1},
|
||||
{"id": "inode-extended-success", "consumer": "inode", "function": "inode.c:erofs_read_inode", "path": "compact probe and extended inode each released", "acquire_count": 2, "release_count": 2},
|
||||
{"id": "super-read-success", "consumer": "super", "function": "super.c:erofs_read_superblock", "path": "super bytes copied and released before validation", "acquire_count": 1, "release_count": 1},
|
||||
{"id": "super-read-short", "consumer": "super", "function": "super.c:erofs_read_superblock", "path": "short provider read fails before acquire", "acquire_count": 0, "release_count": 0},
|
||||
{"id": "super-device-validation-error", "consumer": "super", "function": "super.c:erofs_scan_devices", "path": "device table released through out label", "acquire_count": 1, "release_count": 1},
|
||||
{"id": "xattr-prefix-success", "consumer": "xattr", "function": "xattr.c:erofs_xattr_read_metadata", "path": "length released; prefix transferred then released by caller", "acquire_count": 2, "release_count": 2},
|
||||
{"id": "xattr-prefix-invalid-length", "consumer": "xattr", "function": "xattr.c:erofs_xattr_read_metadata", "path": "length record released before validation error", "acquire_count": 1, "release_count": 1},
|
||||
{"id": "xattr-prefix-second-short", "consumer": "xattr", "function": "xattr.c:erofs_xattr_read_metadata", "path": "length released; prefix not acquired", "acquire_count": 1, "release_count": 1},
|
||||
{"id": "xattr-body-validation-error", "consumer": "xattr", "function": "xattr.c:erofs_xattr_load_body", "path": "body released through fail label", "acquire_count": 1, "release_count": 1},
|
||||
{"id": "xattr-get-iterator-error", "consumer": "xattr", "function": "xattr.c:erofs_getxattr", "path": "body released after inline/shared iterator error", "acquire_count": 1, "release_count": 1}
|
||||
],
|
||||
"decision": {
|
||||
"go": "all required categories observed; every legal tuple equals expected by field; each successful acquisition has one release; short reads acquire nothing; Linux ownership anchors and all audited consumer bodies are present",
|
||||
"stop": "missing category, legal tuple delta, unexplained errno, incomplete direct consumer inventory, or any acquire/release mismatch",
|
||||
"h07": "success and request before EOF implies positive m_llen"
|
||||
},
|
||||
"timeouts": {"host_seconds": 240, "qemu_seconds": 1200},
|
||||
"qemu_required_for_this_candidate_gate": false,
|
||||
"full_feature_suite_required": false
|
||||
}
|
||||
Executable
+1018
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"gate": "G02",
|
||||
"candidate": "P15-006",
|
||||
"required_base": "6673f51152a5195a8a8903aa801f820abce7936e",
|
||||
"title": "independent common map tuple oracle",
|
||||
"required_categories": [
|
||||
"plain",
|
||||
"inline",
|
||||
"hole",
|
||||
"chunk",
|
||||
"multidevice",
|
||||
"compressed",
|
||||
"fragment",
|
||||
"partial-reference",
|
||||
"post-EOF",
|
||||
"bounds",
|
||||
"overflow",
|
||||
"invalid"
|
||||
],
|
||||
"required_subcategories": [
|
||||
"plain-boundaries",
|
||||
"plain-overflow",
|
||||
"inline-boundaries",
|
||||
"inline-overflow",
|
||||
"chunk-raw32",
|
||||
"chunk-index32",
|
||||
"chunk-index48",
|
||||
"chunk-holes",
|
||||
"chunk-bounds",
|
||||
"chunk-overflow",
|
||||
"device-mask",
|
||||
"device-resolution",
|
||||
"extent-4",
|
||||
"extent-8",
|
||||
"extent-16",
|
||||
"extent-32",
|
||||
"compressed-flags",
|
||||
"compressed-bounds",
|
||||
"compressed-overflow",
|
||||
"compressed-invalid",
|
||||
"cleanup-before-acquire",
|
||||
"cleanup-after-acquire"
|
||||
],
|
||||
"tuple_fields": [
|
||||
"m_la",
|
||||
"m_pa",
|
||||
"m_llen",
|
||||
"m_plen",
|
||||
"m_deviceid",
|
||||
"m_flags",
|
||||
"m_algorithmformat",
|
||||
"errno",
|
||||
"acquire_count",
|
||||
"release_count"
|
||||
],
|
||||
"tuple_byte_layout": "<QQQQH2xIiiII",
|
||||
"map_flags": {
|
||||
"EROFS_MAP_MAPPED": 1,
|
||||
"EROFS_MAP_META": 2,
|
||||
"EROFS_MAP_PARTIAL_MAPPED": 4,
|
||||
"EROFS_MAP_PARTIAL_REF": 8,
|
||||
"EROFS_MAP_FRAGMENT": 16
|
||||
},
|
||||
"required_adapter_flag_mask": 31,
|
||||
"errno": {
|
||||
"EIO": 5,
|
||||
"ENODEV": 19,
|
||||
"EINVAL": 22,
|
||||
"EOPNOTSUPP": 45,
|
||||
"EOVERFLOW": 84,
|
||||
"EINTEGRITY": 97
|
||||
},
|
||||
"source_paths": [
|
||||
"repo-pre-15/src/internal.h",
|
||||
"repo-pre-15/src/data.c",
|
||||
"repo-pre-15/src/erofs_fs.h",
|
||||
"repo-pre-15/src/zmap.c"
|
||||
],
|
||||
"protected_function_sha256": {
|
||||
"data.c:erofs_inline_tail_start": "a2f41930abc764619a0657e293eb47f134c000fb6d95b4437c70cce5234919ae",
|
||||
"data.c:erofs_check_device_range": "a5bea19931762c755e3112ff1c4bb1c8123133b80fc84ff1f755795ad1d93037",
|
||||
"data.c:erofs_fill_from_devinfo": "ae9078e6b3dd7416304279bd9420655e19845b606ef79c93af169cb22f153e7a",
|
||||
"data.c:erofs_map_dev": "de77c2b1f5f12a6a7a9bc0414c42bf1ce1745a60331b0abed8c8df87f5b7dcd7",
|
||||
"data.c:erofs_map_blocks_chunk": "932ae2d67edc5a3f36c05fa5c374c2e48a0bac48b7a51bf447160df0ac653b43",
|
||||
"data.c:erofs_bread_device": "c274cd0274ecf79ceea214e123c9d70f3e1f41c6a56449470be554a291ffa808",
|
||||
"data.c:erofs_bread": "74ae2446f1ce8629642402ff63b83d3258d768fdc20dc2196643e3e1bd0382a3",
|
||||
"data.c:erofs_brelse": "1d50316f726878cf3390e92eaf6c27072ca8fccc3efceb14fde11f796886e1c5",
|
||||
"data.c:erofs_put_metabuf": "7f0c546971c5a4bca38582fa21a523d33a8b9231daa3354042b84573b1c0be35",
|
||||
"data.c:erofs_read_metadata": "b0e00f06d1d008adaab72368fbf593be4ad3b7bea79e086c667b4beeef9b98d7",
|
||||
"data.c:erofs_read_physical": "aa33e6fcf99ebf46c1eda54da4308ca160a4948f5afbb14038630ce7ce964e09",
|
||||
"zmap.c:z_erofs_extent_add": "778aa5ad60ed9c3b1affa1e7137966940536c3a05352f14ec04d3e37795d3aa9",
|
||||
"zmap.c:z_erofs_extent_roundup": "882c4f0d77fdcddd0776699508e80d1061ac788410ed661b21f65902c5bcfb40",
|
||||
"zmap.c:z_erofs_extent_table_pos": "288999e2dc5cbff5915ec05916229e0d78f01fde0f23da577309c0b31530c173",
|
||||
"zmap.c:z_erofs_extent_record_pos": "08a5106164bd3dfb5abfec556655659dead8b7a9df86bf699e7828cec1d15920",
|
||||
"zmap.c:z_erofs_read_extent": "e178fd7aa556ed9b94fcce85d44e52abf4a5410e1036a7d8d14752cecd55d6b0",
|
||||
"zmap.c:z_erofs_extent_lstart": "5f7b83d23aa9b8128f2f5d509067032e4856e0cff23e6ec35ec4fd703deecb34",
|
||||
"zmap.c:z_erofs_map_blocks_ext": "996aa16dd7e0a19cce6de9740493a801a41fab7be03a1d2f5717d0e172ee955b",
|
||||
"zmap.c:z_erofs_map_sanity_check": "83c707d56b42c3fd5e461685889f0dc490ff66ad2b5a34ce7f5bc62c8eddaa1f",
|
||||
"zmap.c:z_erofs_map_blocks_iter": "b11768dad797a9a0d2ea33a93ea9e3fe3558a1bf9d89f1b22103a75b25a2bd3d"
|
||||
},
|
||||
"baseline_legacy_map_sha256": "cac2527aa1399e9d8a803445d8b52f1052569537d5d58d7a76fabe33f53cb4cc",
|
||||
"candidate_legacy_map_sha256": "f24bdaa8f9508cc4bf55b34f67b748ccdf33743660153d4b27c0b7f32deb9d25",
|
||||
"map_struct_sha256": "ae883c958dfad3a3df9c6950af0e24723c64523908b901d400c2a3c5eeaf515e",
|
||||
"expected_map_case_count": 80,
|
||||
"expected_device_case_count": 13,
|
||||
"expected_model_sha256": "effcf0b6cc6a6e916eae89a14ad52d7b96273a453556647b6eabf2926fcb4abb",
|
||||
"decision": {
|
||||
"go": "all required categories and subcategories are observed; every source-extracted tuple equals the independent model field-by-field and byte-for-byte; device resolution and positive errno match; every metadata acquire is released exactly once; protected producer and GEOM bodies are unchanged",
|
||||
"stop": "missing chunk, multidevice, bounds, overflow, invalid, compressed, device, flag, errno, or cleanup coverage; model/source tuple difference; byte difference; protected body difference; or non-EOF successful zero run",
|
||||
"h07": "success and request before EOF implies positive m_llen"
|
||||
},
|
||||
"timeouts": {
|
||||
"host_seconds": 240,
|
||||
"qemu_seconds": 1200
|
||||
},
|
||||
"qemu_required_for_this_candidate_gate": false,
|
||||
"full_feature_suite_required": false
|
||||
}
|
||||
Executable
+1712
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"candidate": "P15-019",
|
||||
"errno": {
|
||||
"EINTEGRITY": 97,
|
||||
"ENAMETOOLONG": 63
|
||||
},
|
||||
"expected_fixture_set_sha256": "5e99e5ad9112508991e78ae6e65928973d6b0da78285a059894bed06010cf373",
|
||||
"freebsd": {
|
||||
"head": "106727738dcfb6c001b46f25363b91cece970085",
|
||||
"maxpathlen": 1024,
|
||||
"sha256": {
|
||||
"sys/kern/vfs_lookup.c": "7958081eda9a137a9cd913b959e86b9ed61eff2f9d6cdb5ce5bcfc8211307f24",
|
||||
"sys/kern/vfs_syscalls.c": "a37e268b4909fe382650a624eeaef8b6dada182390c90be7fdad8e548c3fd5e7",
|
||||
"sys/kern/vnode_if.src": "5de87ff115f543fd89f762c3d356b6799f30757f8a34fd840370ce7fff53b90b",
|
||||
"sys/sys/errno.h": "4e615f248a900c6c240c0c87844fd60a8bdffc8a34259d876d5dfde74bd9e42c",
|
||||
"sys/sys/param.h": "cc451dd2de4d6a7cc97b928ddb9e5b19475cba4ab418153ab7f6e6baa661b917",
|
||||
"sys/sys/syslimits.h": "7142ae0f262668e984dd462025726bc9eb0d3e8997dda73b02914d1f7b25da3e",
|
||||
"sys/sys/uio.h": "adb36abeb680d940bbce8566bf82f7dd545e01c519f4e71fa9a2c1a0e1cbbbe8"
|
||||
}
|
||||
},
|
||||
"gate": "G03",
|
||||
"layouts": {
|
||||
"chunk": {
|
||||
"expected_layout": 4,
|
||||
"short_length": 31
|
||||
},
|
||||
"compressed": {
|
||||
"expected_layout": 3,
|
||||
"short_length": 64
|
||||
},
|
||||
"fragment": {
|
||||
"expected_layout": 1,
|
||||
"short_length": 31
|
||||
},
|
||||
"inline": {
|
||||
"expected_layout": 2,
|
||||
"short_length": 31
|
||||
},
|
||||
"plain": {
|
||||
"expected_layout": 0,
|
||||
"short_length": 31
|
||||
}
|
||||
},
|
||||
"protected": {
|
||||
"base_image": "/work/debug-qemu/local/vm-freebsd-build.qcow2.bp",
|
||||
"pid": 26318,
|
||||
"port": 9222
|
||||
},
|
||||
"required_base": "d645feb720c7022d2138d2a62eb72c022eb75351",
|
||||
"schema": 1,
|
||||
"source_sha256": {
|
||||
"repo-pre-15/src/data.c": "cb22072bd4c092aa6a5376add8c8d9d6f94297f6292900ecc2eef9b01c69e92a",
|
||||
"repo-pre-15/src/erofs_fs.h": "0a49ac30ecbcea020c3909beb972ac4287ca704ebc49a9dccfcd6827884589e1",
|
||||
"repo-pre-15/src/erofs_vnops.c": "f28555606ca006d1646a2fee75b98b2f7452d4283c00b087012b8a6c4ef4bf64",
|
||||
"repo-pre-15/src/inode.c": "dee361837ea104f455c851fe4c268bf5d7e10cd9dd2ef58645aea819137639f6",
|
||||
"repo-pre-15/src/internal.h": "eec416077040587ad0756814c4323425889d10bd9b4143fcfc63299bf4f90ae9",
|
||||
"repo-pre-15/src/zdata.c": "3eeb5dae825d7028793a2e1d19a80d24d3f78cdc2e2d5e19466d8fb1208f3242",
|
||||
"repo-pre-15/src/zmap.c": "95432d49c20f3049e0fb5af21904283494325570673dbb037767c1397c77c1f3",
|
||||
"src-linux/data.c": "8625cdc01e5405f856178ae8fd559696ae85f607f19caf229b867a3b7479318a",
|
||||
"src-linux/inode.c": "a15562e0782e155a0744a7ba9d2f557519a583b64853ed75180cef2b4dfae1b3",
|
||||
"src-linux/internal.h": "4aa671896ff7c0ad32a9108c818ef62d16841c116706fdad391c40a530c81405"
|
||||
},
|
||||
"tools": {
|
||||
"dump.erofs": {
|
||||
"path": "/usr/bin/dump.erofs",
|
||||
"sha256": "7956eea01c768869d23deaf9555d70343693ffaf6212cf94cc9ed04853add0cd"
|
||||
},
|
||||
"fsck.erofs": {
|
||||
"path": "/usr/bin/fsck.erofs",
|
||||
"sha256": "09bdee6a2dc7ccdc05547470d2b1f66dc5e1007890b8c3d8f1352e5767eb6b29"
|
||||
},
|
||||
"mkfs.erofs": {
|
||||
"path": "/usr/bin/mkfs.erofs",
|
||||
"sha256": "544e2e502db2302710d7ffdb115eeb26bec374fb4a1ecf12c7aec1156dacc94c",
|
||||
"version": "mkfs.erofs (erofs-utils) 1.8.6"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"head": "7db78788b000999e2de88decd2ba90654f26171c",
|
||||
"sha256": {
|
||||
"include/erofs_fs.h": "02b01a99fe3180f86bb0372efc332efc243142d37aed2819045ae69717a9c915",
|
||||
"lib/compress.c": "c38562ebf7b642ee98611d8ba86a782cfcb18fc2abe55c4cbd9a89f3921e7c9e",
|
||||
"lib/fragments.c": "4d0df4889a3db564e67a4f714584a08b76b0654ef571fa5f1c53cab49212ef5b",
|
||||
"lib/inode.c": "382bf7ccc22436ab9997a3fa417a0424216a6f3b58a95d7c9a1d182a9e228bc1"
|
||||
}
|
||||
},
|
||||
"uuid": "00000000-0000-0000-0000-000000000019",
|
||||
"write_set": [
|
||||
"repo-pre-15/src/internal.h",
|
||||
"repo-pre-15/src/inode.c",
|
||||
"repo-pre-15/src/data.c",
|
||||
"repo-pre-15/src/erofs_vnops.c",
|
||||
"repo-pre-15/tests/pre15/cases/B16-symlink.sh",
|
||||
"repo-pre-15/tests/pre15/fixtures/B16-*"
|
||||
]
|
||||
}
|
||||
Executable
+964
@@ -0,0 +1,964 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
umask 022
|
||||
|
||||
gate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||
root=$(CDPATH= cd -- "$gate_dir/../../../.." && pwd -P)
|
||||
input=$gate_dir/P15-019-input.json
|
||||
freebsd_src=${FREEBSD_SRC:-/work/build/freebsd-src}
|
||||
utils_src=${EROFS_UTILS_SRC:-/work/build/erofs-utils-main}
|
||||
base=
|
||||
output=
|
||||
|
||||
while test "$#" -gt 0; do
|
||||
case "$1" in
|
||||
--base)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 2; }
|
||||
base=$2
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 2; }
|
||||
output=$2
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
printf 'unknown argument: %s\n' "$1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 2; }
|
||||
test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 2; }
|
||||
test -f "$input" || { printf 'missing input: %s\n' "$input" >&2; exit 2; }
|
||||
test -d "$freebsd_src/sys" || { printf 'missing FreeBSD source: %s\n' "$freebsd_src" >&2; exit 2; }
|
||||
test -d "$utils_src/lib" || { printf 'missing erofs-utils source: %s\n' "$utils_src" >&2; exit 2; }
|
||||
for tool in dump.erofs fsck.erofs git mkfs.erofs python3 sha256sum timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
printf 'missing required host tool: %s\n' "$tool" >&2
|
||||
exit 2
|
||||
}
|
||||
done
|
||||
case "$output" in
|
||||
/*) ;;
|
||||
*) output=$PWD/$output ;;
|
||||
esac
|
||||
test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 2; }
|
||||
mkdir -p "$output/host"
|
||||
|
||||
python3 -B - "$root" "$input" "$base" "$output/host" "$freebsd_src" "$utils_src" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import traceback
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(sys.argv[1])
|
||||
INPUT = Path(sys.argv[2])
|
||||
REQUESTED_BASE = sys.argv[3]
|
||||
OUTPUT = Path(sys.argv[4])
|
||||
FREEBSD_SRC = Path(sys.argv[5])
|
||||
UTILS_SRC = Path(sys.argv[6])
|
||||
SPEC = json.loads(INPUT.read_text(encoding="ascii"))
|
||||
DUT = ROOT / "repo-pre-15"
|
||||
|
||||
SUPER = 1024
|
||||
MAGIC = 0xE0F5E1E2
|
||||
CRC32C_POLY = 0x82F63B78
|
||||
FEATURE_COMPAT_SB_CHKSUM = 0x00000001
|
||||
FEATURE_INCOMPAT_CHUNKED_FILE = 0x00000004
|
||||
EROFS_FT_REG_FILE = 1
|
||||
EROFS_FT_SYMLINK = 7
|
||||
S_IFMT = 0o170000
|
||||
S_IFREG = 0o100000
|
||||
S_IFLNK = 0o120000
|
||||
CHUNK_FORMAT_INDEXES = 0x0020
|
||||
LAYOUT_NAMES = {
|
||||
0: "plain",
|
||||
1: "compressed-full",
|
||||
2: "inline",
|
||||
3: "compressed-compact",
|
||||
4: "chunk",
|
||||
}
|
||||
|
||||
|
||||
class GateStop(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InfraBlocked(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def sha256_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def sha256_path(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def write_json(path: Path, value: Any) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
|
||||
|
||||
def git(path: Path, *args: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(path), *args],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise InfraBlocked(f"git {' '.join(args)} failed: {completed.stdout.strip()}")
|
||||
return completed.stdout.strip()
|
||||
|
||||
|
||||
COMMANDS: list[dict[str, Any]] = []
|
||||
|
||||
|
||||
def display_arg(value: str, work: Path) -> str:
|
||||
replacements = (
|
||||
(str(work), "OWNED_WORK"),
|
||||
(str(OUTPUT), "GATE_OUTPUT"),
|
||||
(str(ROOT), "ROOT"),
|
||||
)
|
||||
for old, new in replacements:
|
||||
value = value.replace(old, new)
|
||||
return value
|
||||
|
||||
|
||||
def run(argv: list[str], work: Path, timeout: int = 30) -> subprocess.CompletedProcess[str]:
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
argv,
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
timeout=timeout,
|
||||
)
|
||||
except subprocess.TimeoutExpired as error:
|
||||
raise InfraBlocked(f"command timed out: {' '.join(argv)}") from error
|
||||
COMMANDS.append(
|
||||
{
|
||||
"argv": [display_arg(item, work) for item in argv],
|
||||
"exit": completed.returncode,
|
||||
"stdout_sha256": sha256_bytes(completed.stdout.encode("utf-8")),
|
||||
}
|
||||
)
|
||||
return completed
|
||||
|
||||
|
||||
def run_required(argv: list[str], work: Path, timeout: int = 30) -> str:
|
||||
completed = run(argv, work, timeout)
|
||||
if completed.returncode != 0:
|
||||
raise InfraBlocked(
|
||||
f"command failed ({completed.returncode}): {' '.join(argv)}: "
|
||||
f"{completed.stdout.strip()}"
|
||||
)
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def crc32c(data: bytes | bytearray, seed: int = 0xFFFFFFFF) -> int:
|
||||
value = seed
|
||||
for byte in data:
|
||||
value ^= byte
|
||||
for _ in range(8):
|
||||
value = (value >> 1) ^ (CRC32C_POLY if value & 1 else 0)
|
||||
return value & 0xFFFFFFFF
|
||||
|
||||
|
||||
class Inode:
|
||||
def __init__(
|
||||
self,
|
||||
nid: int,
|
||||
offset: int,
|
||||
inode_format: int,
|
||||
inode_size: int,
|
||||
xattr_size: int,
|
||||
layout: int,
|
||||
mode: int,
|
||||
size: int,
|
||||
start_block: int,
|
||||
):
|
||||
self.nid = nid
|
||||
self.offset = offset
|
||||
self.inode_format = inode_format
|
||||
self.inode_size = inode_size
|
||||
self.xattr_size = xattr_size
|
||||
self.layout = layout
|
||||
self.mode = mode
|
||||
self.size = size
|
||||
self.start_block = start_block
|
||||
|
||||
|
||||
class DirectoryEntry:
|
||||
def __init__(self, nid: int, offset: int, file_type: int, name: bytes):
|
||||
self.nid = nid
|
||||
self.offset = offset
|
||||
self.file_type = file_type
|
||||
self.name = name
|
||||
|
||||
|
||||
class Image:
|
||||
def __init__(self, data: bytes | bytearray):
|
||||
self.data = bytearray(data)
|
||||
if len(self.data) < SUPER + 128 or self.u32(SUPER) != MAGIC:
|
||||
raise GateStop("fixture is not an EROFS image")
|
||||
self.block_bits = self.data[SUPER + 12]
|
||||
if self.block_bits < 9 or self.block_bits > 16:
|
||||
raise GateStop("fixture has invalid block bits")
|
||||
self.block_size = 1 << self.block_bits
|
||||
self.meta_blkaddr = self.u32(SUPER + 40)
|
||||
self.feature_compat = self.u32(SUPER + 8)
|
||||
self.feature_incompat = self.u32(SUPER + 80)
|
||||
self.blocks = self.u32(SUPER + 36)
|
||||
self.root_nid = self.u16(SUPER + 14)
|
||||
self.packed_nid = self.u64(SUPER + 96)
|
||||
if self.blocks == 0 or self.blocks << self.block_bits > len(self.data):
|
||||
raise GateStop("fixture primary image bounds are invalid")
|
||||
self.verify_checksum()
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: Path) -> "Image":
|
||||
return cls(path.read_bytes())
|
||||
|
||||
def u16(self, offset: int) -> int:
|
||||
if offset < 0 or offset + 2 > len(self.data):
|
||||
raise GateStop("u16 read outside image")
|
||||
return struct.unpack_from("<H", self.data, offset)[0]
|
||||
|
||||
def u32(self, offset: int) -> int:
|
||||
if offset < 0 or offset + 4 > len(self.data):
|
||||
raise GateStop("u32 read outside image")
|
||||
return struct.unpack_from("<I", self.data, offset)[0]
|
||||
|
||||
def u64(self, offset: int) -> int:
|
||||
if offset < 0 or offset + 8 > len(self.data):
|
||||
raise GateStop("u64 read outside image")
|
||||
return struct.unpack_from("<Q", self.data, offset)[0]
|
||||
|
||||
def put_u16(self, offset: int, value: int) -> None:
|
||||
struct.pack_into("<H", self.data, offset, value)
|
||||
|
||||
def put_u32(self, offset: int, value: int) -> None:
|
||||
struct.pack_into("<I", self.data, offset, value)
|
||||
|
||||
def put_u64(self, offset: int, value: int) -> None:
|
||||
struct.pack_into("<Q", self.data, offset, value)
|
||||
|
||||
@property
|
||||
def checksum_end(self) -> int:
|
||||
span = self.block_size - SUPER if self.block_size > SUPER else self.block_size
|
||||
end = SUPER + span
|
||||
if end > len(self.data):
|
||||
raise GateStop("checksum span exceeds image")
|
||||
return end
|
||||
|
||||
def calculated_checksum(self) -> int:
|
||||
block = bytearray(self.data[SUPER : self.checksum_end])
|
||||
block[4:8] = bytes(4)
|
||||
return crc32c(block)
|
||||
|
||||
def verify_checksum(self) -> None:
|
||||
if self.feature_compat & FEATURE_COMPAT_SB_CHKSUM:
|
||||
if self.u32(SUPER + 4) != self.calculated_checksum():
|
||||
raise GateStop("fixture checksum is invalid")
|
||||
|
||||
def update_checksum(self) -> None:
|
||||
if not self.feature_compat & FEATURE_COMPAT_SB_CHKSUM:
|
||||
raise GateStop("fixture does not advertise a checksum")
|
||||
self.put_u32(SUPER + 4, 0)
|
||||
self.put_u32(SUPER + 4, self.calculated_checksum())
|
||||
self.verify_checksum()
|
||||
|
||||
def inode(self, nid: int) -> Inode:
|
||||
offset = (self.meta_blkaddr << self.block_bits) + (nid << 5)
|
||||
if offset > len(self.data) - 32:
|
||||
raise GateStop(f"nid {nid} lies outside the primary image")
|
||||
inode_format = self.u16(offset)
|
||||
inode_size = 64 if inode_format & 1 else 32
|
||||
if offset > len(self.data) - inode_size:
|
||||
raise GateStop(f"nid {nid} is truncated")
|
||||
xattr_count = self.u16(offset + 2)
|
||||
xattr_size = 0 if xattr_count == 0 else 12 + 4 * (xattr_count - 1)
|
||||
size = self.u64(offset + 8) if inode_size == 64 else self.u32(offset + 8)
|
||||
return Inode(
|
||||
nid=nid,
|
||||
offset=offset,
|
||||
inode_format=inode_format,
|
||||
inode_size=inode_size,
|
||||
xattr_size=xattr_size,
|
||||
layout=(inode_format >> 1) & 7,
|
||||
mode=self.u16(offset + 4),
|
||||
size=size,
|
||||
start_block=self.u32(offset + 16),
|
||||
)
|
||||
|
||||
def uncompressed_data(self, inode: Inode, blob: bytes | None = None) -> bytes:
|
||||
if inode.layout == 2:
|
||||
offset = inode.offset + inode.inode_size + inode.xattr_size
|
||||
if (offset & (self.block_size - 1)) + inode.size > self.block_size:
|
||||
raise GateStop("inline fixture crosses its metadata block")
|
||||
source = self.data
|
||||
elif inode.layout == 0:
|
||||
offset = inode.start_block << self.block_bits
|
||||
source = self.data
|
||||
elif inode.layout == 4:
|
||||
if blob is None:
|
||||
raise GateStop("chunk fixture has no external blob")
|
||||
chunk_format = self.u16(inode.offset + 16)
|
||||
entry_size = 8 if chunk_format & CHUNK_FORMAT_INDEXES else 4
|
||||
index_base = (inode.offset + inode.inode_size + inode.xattr_size + entry_size - 1) & ~(entry_size - 1)
|
||||
output = bytearray()
|
||||
logical = 0
|
||||
chunk_size = 1 << (self.block_bits + (chunk_format & 0x1F))
|
||||
while logical < inode.size:
|
||||
index = logical // chunk_size
|
||||
position = index_base + index * entry_size
|
||||
if entry_size == 8:
|
||||
high, device_id, low = struct.unpack_from("<HHI", self.data, position)
|
||||
block = low | high << 32
|
||||
if device_id != 1:
|
||||
raise GateStop(f"chunk fixture device id is {device_id}, not 1")
|
||||
source = blob
|
||||
else:
|
||||
block = self.u32(position)
|
||||
source = self.data
|
||||
count = min(chunk_size, inode.size - logical)
|
||||
offset = block << self.block_bits
|
||||
if offset > len(source) or count > len(source) - offset:
|
||||
raise GateStop("chunk payload exceeds its device")
|
||||
output.extend(source[offset : offset + count])
|
||||
logical += count
|
||||
return bytes(output)
|
||||
else:
|
||||
raise GateStop(f"layout {inode.layout} is not uncompressed")
|
||||
if offset > len(source) or inode.size > len(source) - offset:
|
||||
raise GateStop("uncompressed payload exceeds its image")
|
||||
return bytes(source[offset : offset + inode.size])
|
||||
|
||||
def directory_entries(self, inode: Inode) -> list[DirectoryEntry]:
|
||||
data = self.uncompressed_data(inode)
|
||||
if len(data) < 12:
|
||||
raise GateStop("root directory is too short")
|
||||
first_name = struct.unpack_from("<H", data, 8)[0]
|
||||
if first_name < 12 or first_name % 12 != 0 or first_name >= len(data):
|
||||
raise GateStop("root directory has an invalid first name offset")
|
||||
count = first_name // 12
|
||||
entries = []
|
||||
for index in range(count):
|
||||
slot = index * 12
|
||||
name_start = struct.unpack_from("<H", data, slot + 8)[0]
|
||||
name_end = (
|
||||
struct.unpack_from("<H", data, slot + 20)[0]
|
||||
if index + 1 < count
|
||||
else len(data)
|
||||
)
|
||||
if name_start >= name_end or name_end > len(data):
|
||||
raise GateStop("root directory name bounds are invalid")
|
||||
name = data[name_start:name_end].split(b"\0", 1)[0]
|
||||
data_offset = (
|
||||
inode.offset + inode.inode_size + inode.xattr_size
|
||||
if inode.layout == 2
|
||||
else inode.start_block << self.block_bits
|
||||
)
|
||||
entries.append(
|
||||
DirectoryEntry(
|
||||
nid=struct.unpack_from("<Q", data, slot)[0],
|
||||
offset=data_offset + slot,
|
||||
file_type=data[slot + 10],
|
||||
name=name,
|
||||
)
|
||||
)
|
||||
return entries
|
||||
|
||||
def resolve(self, name: bytes = b"link") -> tuple[Inode, DirectoryEntry, Inode]:
|
||||
root = self.inode(self.root_nid)
|
||||
entry = next((item for item in self.directory_entries(root) if item.name == name), None)
|
||||
if entry is None:
|
||||
raise GateStop("fixture root has no /link entry")
|
||||
return root, entry, self.inode(entry.nid)
|
||||
|
||||
|
||||
def lz4_decode_exact(source: bytes, output_size: int) -> bytes:
|
||||
ip = 0
|
||||
output = bytearray()
|
||||
while ip < len(source):
|
||||
token = source[ip]
|
||||
ip += 1
|
||||
literal_length = token >> 4
|
||||
if literal_length == 15:
|
||||
while True:
|
||||
if ip >= len(source):
|
||||
raise ValueError("truncated literal length")
|
||||
value = source[ip]
|
||||
ip += 1
|
||||
literal_length += value
|
||||
if value != 255:
|
||||
break
|
||||
if ip + literal_length > len(source):
|
||||
raise ValueError("truncated literals")
|
||||
output.extend(source[ip : ip + literal_length])
|
||||
ip += literal_length
|
||||
if ip == len(source):
|
||||
break
|
||||
if ip + 2 > len(source):
|
||||
raise ValueError("truncated match offset")
|
||||
offset = source[ip] | source[ip + 1] << 8
|
||||
ip += 2
|
||||
if offset == 0 or offset > len(output):
|
||||
raise ValueError("invalid match offset")
|
||||
match_length = token & 15
|
||||
if match_length == 15:
|
||||
while True:
|
||||
if ip >= len(source):
|
||||
raise ValueError("truncated match length")
|
||||
value = source[ip]
|
||||
ip += 1
|
||||
match_length += value
|
||||
if value != 255:
|
||||
break
|
||||
for _ in range(match_length + 4):
|
||||
output.append(output[-offset])
|
||||
if len(output) > output_size:
|
||||
raise ValueError("decoded output exceeds inode size")
|
||||
if ip != len(source) or len(output) != output_size:
|
||||
raise ValueError("raw LZ4 size mismatch")
|
||||
return bytes(output)
|
||||
|
||||
|
||||
def read_compressed_inline(image: Image, inode: Inode) -> tuple[bytes, dict[str, int]]:
|
||||
header = (inode.offset + inode.inode_size + inode.xattr_size + 7) & ~7
|
||||
if header > len(image.data) - 8:
|
||||
raise GateStop("compressed map header exceeds image")
|
||||
raw0, advise, algorithm, clusterbits = struct.unpack_from("<IHBB", image.data, header)
|
||||
idata_size = raw0 >> 16
|
||||
if inode.layout != 3 or not advise & 0x0008 or idata_size == 0:
|
||||
raise GateStop("compressed fixture is not a compact inline-pcluster inode")
|
||||
block_end = min((inode.offset | (image.block_size - 1)) + 1, len(image.data))
|
||||
matches = []
|
||||
for position in range(header + 8, block_end - idata_size + 1):
|
||||
encoded = bytes(image.data[position : position + idata_size])
|
||||
try:
|
||||
decoded = lz4_decode_exact(encoded, inode.size)
|
||||
except ValueError:
|
||||
continue
|
||||
matches.append((position, decoded))
|
||||
if len(matches) != 1:
|
||||
raise GateStop(f"compressed fixture has {len(matches)} independent decode candidates")
|
||||
return matches[0][1], {
|
||||
"map_header": header,
|
||||
"advise": advise,
|
||||
"algorithm": algorithm & 0x0F,
|
||||
"clusterbits": clusterbits,
|
||||
"idata_size": idata_size,
|
||||
"encoded_offset": matches[0][0],
|
||||
}
|
||||
|
||||
|
||||
def read_fragment(image: Image, inode: Inode) -> tuple[bytes, dict[str, int]]:
|
||||
header = (inode.offset + inode.inode_size + inode.xattr_size + 7) & ~7
|
||||
raw = image.u64(header)
|
||||
if inode.layout != 1 or raw & (1 << 63) == 0:
|
||||
raise GateStop("fragment fixture lacks the whole-fragment header")
|
||||
if image.packed_nid == 0 or image.packed_nid == inode.nid:
|
||||
raise GateStop("fragment fixture has an invalid packed nid")
|
||||
fragment_offset = raw ^ (1 << 63)
|
||||
packed = image.inode(image.packed_nid)
|
||||
packed_data = image.uncompressed_data(packed)
|
||||
if fragment_offset > len(packed_data) or inode.size > len(packed_data) - fragment_offset:
|
||||
raise GateStop("fragment target exceeds packed inode")
|
||||
return bytes(packed_data[fragment_offset : fragment_offset + inode.size]), {
|
||||
"map_header": header,
|
||||
"fragment_offset": fragment_offset,
|
||||
"packed_nid": image.packed_nid,
|
||||
"packed_layout": packed.layout,
|
||||
"packed_size": packed.size,
|
||||
}
|
||||
|
||||
|
||||
def target_bytes(length: int, nul_position: str | None = None) -> bytes:
|
||||
pattern = b"p15-019-safe-target/"
|
||||
target = bytearray((pattern * ((length + len(pattern) - 1) // len(pattern)))[:length])
|
||||
if nul_position == "first":
|
||||
target[0] = 0
|
||||
elif nul_position == "middle":
|
||||
target[length // 2] = 0
|
||||
elif nul_position == "last":
|
||||
target[-1] = 0
|
||||
return bytes(target)
|
||||
|
||||
|
||||
def protected_state() -> dict[str, Any]:
|
||||
protected = SPEC["protected"]
|
||||
base = Path(protected["base_image"])
|
||||
base_stat = base.stat() if base.exists() else None
|
||||
pid_path = Path("/proc") / str(protected["pid"])
|
||||
command = None
|
||||
if (pid_path / "cmdline").exists():
|
||||
command = (pid_path / "cmdline").read_bytes().replace(b"\0", b" ").decode("utf-8", "replace").strip()
|
||||
port_hex = f"{protected['port']:04X}"
|
||||
listeners = 0
|
||||
for table in (Path("/proc/net/tcp"), Path("/proc/net/tcp6")):
|
||||
if not table.exists():
|
||||
continue
|
||||
for line in table.read_text(encoding="ascii").splitlines()[1:]:
|
||||
fields = line.split()
|
||||
if len(fields) >= 4 and fields[1].endswith(f":{port_hex}") and fields[3] == "0A":
|
||||
listeners += 1
|
||||
return {
|
||||
"base": None if base_stat is None else {
|
||||
"device": base_stat.st_dev,
|
||||
"inode": base_stat.st_ino,
|
||||
"mtime_ns": base_stat.st_mtime_ns,
|
||||
"size": base_stat.st_size,
|
||||
},
|
||||
"pid": protected["pid"],
|
||||
"pid_command_sha256": None if command is None else sha256_bytes(command.encode("utf-8")),
|
||||
"port": protected["port"],
|
||||
"port_listener_count": listeners,
|
||||
}
|
||||
|
||||
|
||||
def verify_identities() -> dict[str, Any]:
|
||||
required = SPEC["required_base"]
|
||||
resolved = git(ROOT, "rev-parse", REQUESTED_BASE)
|
||||
if resolved != required:
|
||||
raise GateStop(f"requested base {resolved} is not required base {required}")
|
||||
source_dirty = git(ROOT, "diff", "--name-only", required, "--", "repo-pre-15/src")
|
||||
if source_dirty:
|
||||
raise GateStop(f"production source differs from frozen base: {source_dirty}")
|
||||
source_hashes = {}
|
||||
for relative, expected in SPEC["source_sha256"].items():
|
||||
content = subprocess.run(
|
||||
["git", "-C", str(ROOT), "show", f"{required}:{relative}"],
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if content.returncode != 0:
|
||||
raise InfraBlocked(f"cannot read frozen source {relative}")
|
||||
actual = sha256_bytes(content.stdout)
|
||||
if actual != expected:
|
||||
raise GateStop(f"frozen source hash differs for {relative}")
|
||||
source_hashes[relative] = actual
|
||||
if git(FREEBSD_SRC, "rev-parse", "HEAD") != SPEC["freebsd"]["head"]:
|
||||
raise GateStop("FreeBSD source HEAD differs")
|
||||
for relative, expected in SPEC["freebsd"]["sha256"].items():
|
||||
if sha256_path(FREEBSD_SRC / relative) != expected:
|
||||
raise GateStop(f"FreeBSD source hash differs for {relative}")
|
||||
if git(UTILS_SRC, "rev-parse", "HEAD") != SPEC["utils"]["head"]:
|
||||
raise GateStop("erofs-utils source HEAD differs")
|
||||
for relative, expected in SPEC["utils"]["sha256"].items():
|
||||
actual = sha256_path(UTILS_SRC / relative)
|
||||
if actual != expected:
|
||||
raise GateStop(f"erofs-utils source hash differs for {relative}")
|
||||
for name, tool in SPEC["tools"].items():
|
||||
path = Path(tool["path"])
|
||||
if sha256_path(path) != tool["sha256"]:
|
||||
raise GateStop(f"tool hash differs for {name}")
|
||||
version = subprocess.run(
|
||||
[SPEC["tools"]["mkfs.erofs"]["path"], "-V"],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
).stdout.splitlines()[0]
|
||||
if version != SPEC["tools"]["mkfs.erofs"]["version"]:
|
||||
raise GateStop(f"mkfs version differs: {version}")
|
||||
return {"base": resolved, "source_sha256": source_hashes, "mkfs_version": version}
|
||||
|
||||
|
||||
def verify_semantics() -> dict[str, Any]:
|
||||
linux = (ROOT / "src-linux/inode.c").read_text(encoding="utf-8")
|
||||
freebsd_inode = (DUT / "src/inode.c").read_text(encoding="utf-8")
|
||||
freebsd_data = (DUT / "src/data.c").read_text(encoding="utf-8")
|
||||
freebsd_vnops = (DUT / "src/erofs_vnops.c").read_text(encoding="utf-8")
|
||||
lookup = (FREEBSD_SRC / "sys/kern/vfs_lookup.c").read_text(encoding="utf-8")
|
||||
errno_h = (FREEBSD_SRC / "sys/sys/errno.h").read_text(encoding="utf-8")
|
||||
syslimits = (FREEBSD_SRC / "sys/sys/syslimits.h").read_text(encoding="utf-8")
|
||||
linux_markers = (
|
||||
"vi->datalayout == EROFS_INODE_FLAT_INLINE",
|
||||
"kmemdup_nul(bptr + ofs, inode->i_size, GFP_KERNEL)",
|
||||
"!inode->i_size || strlen(link) != inode->i_size",
|
||||
"return -EFSCORRUPTED",
|
||||
".get_link = page_get_link",
|
||||
".get_link = simple_get_link",
|
||||
)
|
||||
freebsd_markers = (
|
||||
"return (erofs_read_uio(MTOE(vp->v_mount), VTOE(vp), uio));",
|
||||
"return (erofs_readlink_target(ap->a_vp, ap->a_uio));",
|
||||
".vop_readlink = erofs_readlink",
|
||||
)
|
||||
if not all(marker in linux for marker in linux_markers):
|
||||
raise GateStop("Linux fast/page symlink path anchors changed")
|
||||
if not all(marker in freebsd_data + freebsd_vnops for marker in freebsd_markers):
|
||||
raise GateStop("FreeBSD vnode/readlink path anchors changed")
|
||||
if "erofs_validate_symlink" in freebsd_inode + freebsd_data + freebsd_vnops:
|
||||
raise GateStop("P15-019 production implementation already exists at the gate base")
|
||||
if "aiov.iov_len = MAXPATHLEN;" not in lookup or "linklen == 0" not in lookup:
|
||||
raise GateStop("FreeBSD namei readlink boundary anchors changed")
|
||||
expected_errno = SPEC["errno"]
|
||||
for name, value in expected_errno.items():
|
||||
if re.search(rf"#define\s+{name}\s+{value}\b", errno_h) is None:
|
||||
raise GateStop(f"FreeBSD errno {name} is not {value}")
|
||||
maxpath = SPEC["freebsd"]["maxpathlen"]
|
||||
if re.search(rf"#define\s+PATH_MAX\s+{maxpath}\b", syslimits) is None:
|
||||
raise GateStop("FreeBSD PATH_MAX differs")
|
||||
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", freebsd_inode + freebsd_data + freebsd_vnops):
|
||||
raise GateStop("FreeBSD EROFS source contains Linux negative errno returns")
|
||||
return {
|
||||
"linux": {
|
||||
"fast_inline_validation": True,
|
||||
"noninline_get_link": "page_get_link",
|
||||
"errno_convention": "negative Linux errno",
|
||||
},
|
||||
"freebsd": {
|
||||
"entry": "VOP_READLINK",
|
||||
"path": "vnode -> erofs_readlink_target -> map/read -> GEOM or compressed backing",
|
||||
"maxpathlen": maxpath,
|
||||
"empty_namei_baseline": "ENOENT after zero-byte VOP_READLINK success",
|
||||
"errno_convention": "positive FreeBSD errno",
|
||||
},
|
||||
"candidate": {
|
||||
"empty": "EINTEGRITY",
|
||||
"embedded_nul": "EINTEGRITY before uiomove",
|
||||
"oversize": "ENAMETOOLONG before target I/O",
|
||||
"normal": "exact byte target; no trailing NUL required",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def make_source(source: Path, payload: bytes, native_symlink: bool) -> None:
|
||||
source.mkdir()
|
||||
source.chmod(0o755)
|
||||
link = source / "link"
|
||||
if native_symlink:
|
||||
os.symlink(payload.decode("ascii"), link)
|
||||
else:
|
||||
link.write_bytes(payload)
|
||||
link.chmod(0o644)
|
||||
os.utime(link, (0, 0), follow_symlinks=False)
|
||||
os.utime(source, (0, 0), follow_symlinks=False)
|
||||
|
||||
|
||||
def mkfs_case(
|
||||
layout: str,
|
||||
case_id: str,
|
||||
payload: bytes,
|
||||
native_symlink: bool,
|
||||
work: Path,
|
||||
fixtures: Path,
|
||||
) -> tuple[Path, Path | None, list[int]]:
|
||||
source = work / f"source-{layout}-{case_id}"
|
||||
make_source(source, payload, native_symlink)
|
||||
image_path = fixtures / f"{layout}-{case_id}.erofs"
|
||||
blob_path = fixtures / f"{layout}-{case_id}.blob" if layout == "chunk" else None
|
||||
options: list[str] = []
|
||||
if layout == "plain":
|
||||
options = ["-E^inline_data"]
|
||||
elif layout == "chunk":
|
||||
assert blob_path is not None
|
||||
blob_path.write_bytes(b"")
|
||||
options = ["--chunksize=4096", f"--blobdev={blob_path}"]
|
||||
elif layout == "compressed":
|
||||
options = ["-zlz4", "-C4096", "-Eztailpacking"]
|
||||
elif layout == "fragment":
|
||||
options = ["-zlz4", "-C4096", "-Eall-fragments"]
|
||||
command = [
|
||||
SPEC["tools"]["mkfs.erofs"]["path"],
|
||||
"-T0",
|
||||
"--all-time",
|
||||
"--all-root",
|
||||
"--workers=1",
|
||||
"--sort=path",
|
||||
"-x-1",
|
||||
f"-U{SPEC['uuid']}",
|
||||
*options,
|
||||
str(image_path),
|
||||
str(source),
|
||||
]
|
||||
run_required(command, work)
|
||||
before = image_path.read_bytes()
|
||||
image = Image(before)
|
||||
_, entry, inode = image.resolve()
|
||||
changed = []
|
||||
if stat.S_IFMT(inode.mode) == S_IFREG:
|
||||
image.put_u16(inode.offset + 4, S_IFLNK | 0o777)
|
||||
image.data[entry.offset + 10] = EROFS_FT_SYMLINK
|
||||
elif stat.S_IFMT(inode.mode) != S_IFLNK or entry.file_type != EROFS_FT_SYMLINK:
|
||||
raise GateStop(f"{layout}/{case_id}: source inode type is unexpected")
|
||||
if case_id == "empty":
|
||||
if inode.inode_size == 64:
|
||||
image.put_u64(inode.offset + 8, 0)
|
||||
else:
|
||||
image.put_u32(inode.offset + 8, 0)
|
||||
image.update_checksum()
|
||||
after = bytes(image.data)
|
||||
for offset, (old, new) in enumerate(zip(before, after)):
|
||||
if old != new:
|
||||
changed.append(offset)
|
||||
allowed = set(range(SUPER + 4, SUPER + 8))
|
||||
allowed.update(range(inode.offset + 4, inode.offset + 6))
|
||||
allowed.add(entry.offset + 10)
|
||||
if case_id == "empty":
|
||||
allowed.update(range(inode.offset + 8, inode.offset + (16 if inode.inode_size == 64 else 12)))
|
||||
unexpected = sorted(set(changed) - allowed)
|
||||
if unexpected:
|
||||
raise GateStop(f"{layout}/{case_id}: transform changed unexpected offsets {unexpected}")
|
||||
image_path.write_bytes(after)
|
||||
return image_path, blob_path, changed
|
||||
|
||||
|
||||
def expected_case(layout: str, case_id: str) -> tuple[int, bytes, str | None, str]:
|
||||
short = SPEC["layouts"][layout]["short_length"]
|
||||
if case_id == "normal-short":
|
||||
return short, target_bytes(short), None, "PASS"
|
||||
if case_id == "normal-max":
|
||||
limit = SPEC["freebsd"]["maxpathlen"]
|
||||
return limit, target_bytes(limit), None, "PASS"
|
||||
if case_id == "empty":
|
||||
return 0, target_bytes(short), None, "EINTEGRITY"
|
||||
if case_id.startswith("nul-"):
|
||||
position = case_id.removeprefix("nul-")
|
||||
limit = SPEC["freebsd"]["maxpathlen"]
|
||||
return limit, target_bytes(limit, position), position, "EINTEGRITY"
|
||||
if case_id == "too-long":
|
||||
length = SPEC["freebsd"]["maxpathlen"] + 1
|
||||
return length, target_bytes(length), None, "ENAMETOOLONG"
|
||||
raise AssertionError(case_id)
|
||||
|
||||
|
||||
def independent_target(image: Image, inode: Inode, blob: bytes | None) -> tuple[bytes, dict[str, Any]]:
|
||||
if inode.layout in (0, 2, 4):
|
||||
return image.uncompressed_data(inode, blob), {}
|
||||
if inode.layout == 3:
|
||||
return read_compressed_inline(image, inode)
|
||||
if inode.layout == 1:
|
||||
return read_fragment(image, inode)
|
||||
raise GateStop(f"unsupported target layout {inode.layout}")
|
||||
|
||||
|
||||
def oracle(image: Image, inode: Inode, blob: bytes | None) -> tuple[str, bytes | None, dict[str, Any]]:
|
||||
if inode.size == 0:
|
||||
return "EINTEGRITY", None, {"point": "symlink.empty"}
|
||||
if inode.size > SPEC["freebsd"]["maxpathlen"]:
|
||||
return "ENAMETOOLONG", None, {"point": "symlink.maxpathlen"}
|
||||
target, details = independent_target(image, inode, blob)
|
||||
nul = target.find(b"\0")
|
||||
if nul >= 0:
|
||||
return "EINTEGRITY", target, {"point": "symlink.embedded-nul", "nul_offset": nul, **details}
|
||||
return "PASS", target, {"point": "symlink.valid", **details}
|
||||
|
||||
|
||||
def fixture_set_hash(fixtures: Path) -> tuple[str, dict[str, str]]:
|
||||
hashes = {}
|
||||
for path in sorted(item for item in fixtures.iterdir() if item.is_file()):
|
||||
if path.name == "SHA256SUMS":
|
||||
continue
|
||||
hashes[path.name] = sha256_path(path)
|
||||
lines = "".join(f"{digest} {name}\n" for name, digest in hashes.items())
|
||||
(fixtures / "SHA256SUMS").write_text(lines, encoding="ascii")
|
||||
return sha256_bytes(lines.encode("ascii")), hashes
|
||||
|
||||
|
||||
def execute() -> dict[str, Any]:
|
||||
if SPEC.get("schema") != 1 or SPEC.get("candidate") != "P15-019" or SPEC.get("gate") != "G03":
|
||||
raise InfraBlocked("input identity is invalid")
|
||||
identity = verify_identities()
|
||||
semantics = verify_semantics()
|
||||
write_json(OUTPUT / "semantics.json", semantics)
|
||||
fixtures = OUTPUT / "fixtures"
|
||||
fixtures.mkdir()
|
||||
cases = (
|
||||
"normal-short",
|
||||
"normal-max",
|
||||
"empty",
|
||||
"nul-first",
|
||||
"nul-middle",
|
||||
"nul-last",
|
||||
"too-long",
|
||||
)
|
||||
records = []
|
||||
with tempfile.TemporaryDirectory(prefix=".p15-019-work-", dir=OUTPUT) as raw_work:
|
||||
work = Path(raw_work)
|
||||
for layout, layout_spec in sorted(SPEC["layouts"].items()):
|
||||
for case_id in cases:
|
||||
expected_size, payload, nul_position, expected = expected_case(layout, case_id)
|
||||
native = layout == "inline" and nul_position is None
|
||||
image_path, blob_path, changed = mkfs_case(
|
||||
layout, case_id, payload, native, work, fixtures
|
||||
)
|
||||
image = Image.load(image_path)
|
||||
_, entry, inode = image.resolve()
|
||||
if inode.layout != layout_spec["expected_layout"]:
|
||||
raise GateStop(
|
||||
f"{layout}/{case_id}: layout {inode.layout} != {layout_spec['expected_layout']}"
|
||||
)
|
||||
if stat.S_IFMT(inode.mode) != S_IFLNK or entry.file_type != EROFS_FT_SYMLINK:
|
||||
raise GateStop(f"{layout}/{case_id}: transformed inode is not a symlink")
|
||||
if inode.size != expected_size:
|
||||
raise GateStop(f"{layout}/{case_id}: inode size {inode.size} != {expected_size}")
|
||||
blob = blob_path.read_bytes() if blob_path is not None else None
|
||||
observed, target, details = oracle(image, inode, blob)
|
||||
if observed != expected:
|
||||
raise GateStop(f"{layout}/{case_id}: oracle {observed} != {expected}")
|
||||
if expected != "EINTEGRITY" or case_id == "empty":
|
||||
if case_id != "empty" and target is not None and target != payload:
|
||||
raise GateStop(f"{layout}/{case_id}: target bytes differ from source")
|
||||
if case_id.startswith("nul-"):
|
||||
expected_nul = {"first": 0, "middle": len(payload) // 2, "last": len(payload) - 1}[nul_position]
|
||||
if details.get("nul_offset") != expected_nul:
|
||||
raise GateStop(f"{layout}/{case_id}: NUL offset differs")
|
||||
dump = run(
|
||||
[SPEC["tools"]["dump.erofs"]["path"], "--path=/link", str(image_path)],
|
||||
work,
|
||||
)
|
||||
dump_layout = re.search(r"Layout:\s+(\d+)", dump.stdout)
|
||||
dump_mode = "symlink file" in dump.stdout
|
||||
if dump.returncode != 0 or dump_layout is None or int(dump_layout.group(1)) != inode.layout or not dump_mode:
|
||||
raise GateStop(f"{layout}/{case_id}: dump.erofs cross-check failed")
|
||||
fsck_argv = [SPEC["tools"]["fsck.erofs"]["path"], "-d0"]
|
||||
if blob_path is not None:
|
||||
fsck_argv.append(f"--device={blob_path}")
|
||||
fsck_argv.append(str(image_path))
|
||||
fsck = run(fsck_argv, work)
|
||||
extracted = False
|
||||
if expected == "PASS":
|
||||
extract = work / f"extract-{layout}-{case_id}"
|
||||
extract.mkdir()
|
||||
extract_argv = [
|
||||
SPEC["tools"]["fsck.erofs"]["path"],
|
||||
f"--extract={extract}",
|
||||
"--no-preserve",
|
||||
]
|
||||
if blob_path is not None:
|
||||
extract_argv.append(f"--device={blob_path}")
|
||||
extract_argv.append(str(image_path))
|
||||
run_required(extract_argv, work)
|
||||
extracted_link = extract / "link"
|
||||
if not extracted_link.is_symlink() or os.readlink(extracted_link).encode("ascii") != payload:
|
||||
raise GateStop(f"{layout}/{case_id}: fsck extraction target differs")
|
||||
extracted = True
|
||||
records.append(
|
||||
{
|
||||
"case": case_id,
|
||||
"changed_offsets": changed,
|
||||
"dump_cross_check": "PASS",
|
||||
"expected": expected,
|
||||
"fsck_check_exit": fsck.returncode,
|
||||
"fsck_extract": extracted,
|
||||
"image": image_path.name,
|
||||
"inode": {
|
||||
"layout": inode.layout,
|
||||
"layout_name": LAYOUT_NAMES[inode.layout],
|
||||
"mode": oct(inode.mode),
|
||||
"nid": inode.nid,
|
||||
"size": inode.size,
|
||||
},
|
||||
"layout": layout,
|
||||
"oracle": details,
|
||||
"source_payload_sha256": sha256_bytes(payload),
|
||||
"target_sha256": None if target is None else sha256_bytes(target),
|
||||
}
|
||||
)
|
||||
fixture_sha256, hashes = fixture_set_hash(fixtures)
|
||||
expected_fixture = SPEC.get("expected_fixture_set_sha256", "")
|
||||
if expected_fixture and fixture_sha256 != expected_fixture:
|
||||
raise GateStop(
|
||||
f"fixture set hash {fixture_sha256} != frozen {expected_fixture}"
|
||||
)
|
||||
write_json(OUTPUT / "cases.json", records)
|
||||
write_json(OUTPUT / "commands.json", COMMANDS)
|
||||
by_layout = {
|
||||
layout: {
|
||||
record["case"]: record["expected"]
|
||||
for record in records
|
||||
if record["layout"] == layout
|
||||
}
|
||||
for layout in sorted(SPEC["layouts"])
|
||||
}
|
||||
required = set(cases)
|
||||
if any(set(result) != required for result in by_layout.values()):
|
||||
raise GateStop("one or more layouts lack a complete oracle")
|
||||
return {
|
||||
"candidate": "P15-019",
|
||||
"case_count": len(records),
|
||||
"decision": "GO",
|
||||
"fixture_file_count": len(hashes),
|
||||
"fixture_set_sha256": fixture_sha256,
|
||||
"gate": "G03",
|
||||
"identity": identity,
|
||||
"layout_results": by_layout,
|
||||
"qemu": "NOT_RUN: Stage0 host fixture/oracle is complete",
|
||||
"source_modified": False,
|
||||
}
|
||||
|
||||
|
||||
before_protected = protected_state()
|
||||
status = "INFRA_BLOCKED"
|
||||
exit_code = 2
|
||||
result: dict[str, Any]
|
||||
try:
|
||||
result = execute()
|
||||
status = "GO"
|
||||
exit_code = 0
|
||||
except GateStop as error:
|
||||
status = "STOP"
|
||||
exit_code = 1
|
||||
result = {"candidate": "P15-019", "decision": status, "reason": str(error)}
|
||||
except (InfraBlocked, OSError, ValueError, KeyError, json.JSONDecodeError) as error:
|
||||
result = {"candidate": "P15-019", "decision": status, "reason": str(error)}
|
||||
except Exception as error:
|
||||
result = {
|
||||
"candidate": "P15-019",
|
||||
"decision": status,
|
||||
"reason": f"unexpected gate error: {error}",
|
||||
"traceback": traceback.format_exc(),
|
||||
}
|
||||
after_protected = protected_state()
|
||||
cleanup = {
|
||||
"owned_temp_remaining": sorted(path.name for path in OUTPUT.glob(".p15-019-work-*")),
|
||||
"protected_before": before_protected,
|
||||
"protected_after": after_protected,
|
||||
"protected_unchanged": before_protected == after_protected,
|
||||
}
|
||||
write_json(OUTPUT / "cleanup.json", cleanup)
|
||||
if cleanup["owned_temp_remaining"] or not cleanup["protected_unchanged"]:
|
||||
status = "INFRA_BLOCKED"
|
||||
exit_code = 2
|
||||
result = {
|
||||
"candidate": "P15-019",
|
||||
"decision": status,
|
||||
"reason": "cleanup or protected-resource identity changed",
|
||||
}
|
||||
result["cleanup_sha256"] = sha256_path(OUTPUT / "cleanup.json")
|
||||
result["input_sha256"] = sha256_path(INPUT)
|
||||
write_json(OUTPUT / "result.json", result)
|
||||
print(f"P15-019 G03: {status}")
|
||||
if "fixture_set_sha256" in result:
|
||||
print(f"fixture_set_sha256={result['fixture_set_sha256']}")
|
||||
if "reason" in result:
|
||||
print(f"reason={result['reason']}")
|
||||
sys.exit(exit_code)
|
||||
PY
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"batch": "B19b",
|
||||
"candidate": "P15-021",
|
||||
"concurrency": {
|
||||
"loops_per_worker": 20,
|
||||
"workers": 64
|
||||
},
|
||||
"errno": {
|
||||
"EINTEGRITY": 97,
|
||||
"ENOATTR": 87
|
||||
},
|
||||
"fixture": {
|
||||
"attribute_count": 8,
|
||||
"attribute_value_bytes": 96,
|
||||
"peer_count": 64,
|
||||
"target": "/target.bin",
|
||||
"uuid": "00000000-0000-0000-0000-000000000021"
|
||||
},
|
||||
"format": {
|
||||
"bits": 32,
|
||||
"feature_compat": 4,
|
||||
"filter_default": 4294967295,
|
||||
"seed": 633069711
|
||||
},
|
||||
"freebsd": {
|
||||
"head": "106727738dcfb6c001b46f25363b91cece970085",
|
||||
"public_xxh32_api": false,
|
||||
"sha256": {
|
||||
"sys/contrib/openzfs/module/zstd/lib/common/xxhash.h": "ba65364fb0532ef2848b35c35fbbb5172f5037f56717cb2ac562812993385f27",
|
||||
"sys/contrib/zstd/lib/common/xxhash.c": "8444d064922f434b67b708a987981aa21dfeece735b48c82d31f7027d0ccef03",
|
||||
"sys/contrib/zstd/lib/common/xxhash.h": "8cb837b21a8fe9a6b9dbcd0961ab16e733bfcbfa9e003f3a496ce07ae80aa8ee",
|
||||
"sys/sys/endian.h": "3d870d499089ac84bb49debebbb75b406a5f4df6f05f1582faebc5cf0b7b8806",
|
||||
"sys/sys/errno.h": "4e615f248a900c6c240c0c87844fd60a8bdffc8a34259d876d5dfde74bd9e42c",
|
||||
"sys/sys/libkern.h": "61f74a2ca87b0be5deb3f1aa7a29f188709331a98d5bd5a744ecd9d795a8f91b"
|
||||
}
|
||||
},
|
||||
"gate": "G03",
|
||||
"implementation": {
|
||||
"kind": "file-local namespaced",
|
||||
"symbol": "erofs_xxh32"
|
||||
},
|
||||
"linux_source_sha256": {
|
||||
"src-linux/erofs_fs.h": "6cb322cf7506858c3c82de3c81026039c543f448201c9c551fd81360cca67e93",
|
||||
"src-linux/xattr.c": "c8394e5f6301225cbe7587f223485a368348eac7596c1ab0bbf965c99655ed4a"
|
||||
},
|
||||
"random": {
|
||||
"count": 1000000,
|
||||
"seed": 20260815
|
||||
},
|
||||
"required_base": "666e52f710363df07f7c93919eb835d41092d011",
|
||||
"schema": 1,
|
||||
"source_sha256": {
|
||||
"repo-pre-15/src/erofs_fs.h": "0a49ac30ecbcea020c3909beb972ac4287ca704ebc49a9dccfcd6827884589e1",
|
||||
"repo-pre-15/src/internal.h": "3a9f8bc80250a6fdccb0f0ebb2e13935edfe4b416a4d710104415b3991093bcf",
|
||||
"repo-pre-15/src/xattr.c": "29d1cfe572a4dee9c003cf27f1d44301d707018087e92412781d977b752d3b6b"
|
||||
},
|
||||
"thresholds": {
|
||||
"cold_loops_per_sample": 200,
|
||||
"cold_samples": 5,
|
||||
"minimum_provider_metadata_read_reduction_percent": 25.0
|
||||
},
|
||||
"tools": {
|
||||
"cc": {
|
||||
"path": "/usr/bin/cc",
|
||||
"sha256": "a23ecab8ff08f09ad8c80602c2c5df7f49e09c25905cb8975902e101bf72635f"
|
||||
},
|
||||
"dump.erofs": {
|
||||
"path": "/usr/bin/dump.erofs",
|
||||
"sha256": "7956eea01c768869d23deaf9555d70343693ffaf6212cf94cc9ed04853add0cd"
|
||||
},
|
||||
"fsck.erofs": {
|
||||
"path": "/usr/bin/fsck.erofs",
|
||||
"sha256": "09bdee6a2dc7ccdc05547470d2b1f66dc5e1007890b8c3d8f1352e5767eb6b29"
|
||||
},
|
||||
"libxxhash": {
|
||||
"path": "/usr/lib/x86_64-linux-gnu/libxxhash.so.0.8.3",
|
||||
"sha256": "7dd49b353facbee50c371d0559ce29db5afee5ac33249ca5c7068420fb642762"
|
||||
},
|
||||
"mkfs.erofs": {
|
||||
"path": "/usr/bin/mkfs.erofs",
|
||||
"sha256": "544e2e502db2302710d7ffdb115eeb26bec374fb4a1ecf12c7aec1156dacc94c",
|
||||
"version": "mkfs.erofs (erofs-utils) 1.8.6"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"head": "7db78788b000999e2de88decd2ba90654f26171c",
|
||||
"sha256": {
|
||||
"include/erofs_fs.h": "02b01a99fe3180f86bb0372efc332efc243142d37aed2819045ae69717a9c915",
|
||||
"lib/xattr.c": "8e1625b858f6183756ff8a2e1a58d6e10a8408c8845c1c505abb68120daf832b",
|
||||
"lib/xxhash.c": "41859bc203785cfc8035b63b024ad23c9f099890a0a33d9c13dcb5401e6b9106"
|
||||
}
|
||||
},
|
||||
"vectors": [
|
||||
{"hash": 3935488656, "index": 1, "name_hex": "", "bit": 16},
|
||||
{"hash": 924486869, "index": 1, "name_hex": "61", "bit": 21},
|
||||
{"hash": 3124601740, "index": 1, "name_hex": "616c706861", "bit": 12},
|
||||
{"hash": 1029047478, "index": 4, "name_hex": "747275737465642e6e616d65", "bit": 22},
|
||||
{"hash": 2585031559, "index": 6, "name_hex": "30313233343536373839616263646566", "bit": 7},
|
||||
{"hash": 1765035109, "index": 2, "name_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "bit": 5},
|
||||
{"hash": 596111791, "index": 3, "name_hex": "00010203fcfdfeff", "bit": 15}
|
||||
],
|
||||
"write_set": {
|
||||
"gate": [
|
||||
"repo-pre-15/docs/pre15-stage0/P15-021.md",
|
||||
"repo-pre-15/tests/pre15/gates/P15-021-input.json",
|
||||
"repo-pre-15/tests/pre15/gates/P15-021.sh"
|
||||
],
|
||||
"source": [
|
||||
"repo-pre-15/src/erofs_fs.h",
|
||||
"repo-pre-15/src/internal.h",
|
||||
"repo-pre-15/src/xattr.c",
|
||||
"repo-pre-15/tests/pre15/cases/B19b-xattr-bloom.sh",
|
||||
"repo-pre-15/tests/pre15/fixtures/B19b-*"
|
||||
]
|
||||
}
|
||||
}
|
||||
Executable
+892
@@ -0,0 +1,892 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
umask 022
|
||||
|
||||
gate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||
root=$(CDPATH= cd -- "$gate_dir/../../../.." && pwd -P)
|
||||
input=$gate_dir/P15-021-input.json
|
||||
freebsd_src=${FREEBSD_SRC:-/work/build/freebsd-src}
|
||||
utils_src=${EROFS_UTILS_SRC:-/work/build/erofs-utils-master}
|
||||
base=
|
||||
output=
|
||||
|
||||
while test "$#" -gt 0; do
|
||||
case "$1" in
|
||||
--base)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 2; }
|
||||
base=$2
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 2; }
|
||||
output=$2
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
printf 'unknown argument: %s\n' "$1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 2; }
|
||||
test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 2; }
|
||||
test -f "$input" || { printf 'missing input: %s\n' "$input" >&2; exit 2; }
|
||||
test -d "$freebsd_src/sys" || { printf 'missing FreeBSD source: %s\n' "$freebsd_src" >&2; exit 2; }
|
||||
test -d "$utils_src/lib" || { printf 'missing erofs-utils source: %s\n' "$utils_src" >&2; exit 2; }
|
||||
for tool in cc fsck.erofs git mkfs.erofs python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
printf 'missing required host tool: %s\n' "$tool" >&2
|
||||
exit 2
|
||||
}
|
||||
done
|
||||
case "$output" in
|
||||
/*) ;;
|
||||
*) output=$PWD/$output ;;
|
||||
esac
|
||||
test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 2; }
|
||||
mkdir -p "$output"
|
||||
|
||||
python3 -B - "$root" "$input" "$base" "$output" "$freebsd_src" "$utils_src" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
from array import array
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import ctypes
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import random
|
||||
import shutil
|
||||
import statistics
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(sys.argv[1])
|
||||
INPUT = Path(sys.argv[2])
|
||||
REQUESTED_BASE = sys.argv[3]
|
||||
OUTPUT = Path(sys.argv[4])
|
||||
FREEBSD_SRC = Path(sys.argv[5])
|
||||
UTILS_SRC = Path(sys.argv[6])
|
||||
DUT = ROOT / "repo-pre-15"
|
||||
SPEC = json.loads(INPUT.read_text(encoding="ascii"))
|
||||
SUPER = 1024
|
||||
MAGIC = 0xE0F5E1E2
|
||||
CRC32C_POLY = 0x82F63B78
|
||||
|
||||
|
||||
class GateStop(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class InfraBlocked(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class Reject(RuntimeError):
|
||||
def __init__(self, errno_name: str, point: str):
|
||||
super().__init__(f"{errno_name} at {point}")
|
||||
self.errno_name = errno_name
|
||||
self.point = point
|
||||
|
||||
|
||||
def sha256_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def sha256_path(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def write_json(path: Path, value: Any) -> None:
|
||||
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
|
||||
|
||||
def run(argv: list[str], *, input_bytes: bytes | None = None) -> subprocess.CompletedProcess:
|
||||
completed = subprocess.run(
|
||||
argv,
|
||||
input=input_bytes,
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=120,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
output = (completed.stdout + completed.stderr).decode("utf-8", "replace")
|
||||
raise InfraBlocked(f"command failed ({completed.returncode}): {' '.join(argv)}: {output.strip()}")
|
||||
return completed
|
||||
|
||||
|
||||
def git(path: Path, *args: str) -> str:
|
||||
return run(["git", "-C", str(path), *args]).stdout.decode().strip()
|
||||
|
||||
|
||||
def source_at(commit: str, relative: str) -> bytes:
|
||||
return run(["git", "-C", str(ROOT), "show", f"{commit}:{relative}"]).stdout
|
||||
|
||||
|
||||
def crc32c(data: bytes | bytearray, seed: int = 0xFFFFFFFF) -> int:
|
||||
value = seed
|
||||
for byte in data:
|
||||
value ^= byte
|
||||
for _ in range(8):
|
||||
value = (value >> 1) ^ (CRC32C_POLY if value & 1 else 0)
|
||||
return value & 0xFFFFFFFF
|
||||
|
||||
|
||||
def rotl32(value: int, count: int) -> int:
|
||||
return ((value << count) | (value >> (32 - count))) & 0xFFFFFFFF
|
||||
|
||||
|
||||
def oracle_xxh32(data: bytes, seed: int) -> int:
|
||||
prime1 = 2654435761
|
||||
prime2 = 2246822519
|
||||
prime3 = 3266489917
|
||||
prime4 = 668265263
|
||||
prime5 = 374761393
|
||||
cursor = 0
|
||||
end = len(data)
|
||||
|
||||
def round32(accumulator: int, lane: int) -> int:
|
||||
accumulator = (accumulator + lane * prime2) & 0xFFFFFFFF
|
||||
return (rotl32(accumulator, 13) * prime1) & 0xFFFFFFFF
|
||||
|
||||
if end >= 16:
|
||||
accumulator1 = (seed + prime1 + prime2) & 0xFFFFFFFF
|
||||
accumulator2 = (seed + prime2) & 0xFFFFFFFF
|
||||
accumulator3 = seed & 0xFFFFFFFF
|
||||
accumulator4 = (seed - prime1) & 0xFFFFFFFF
|
||||
limit = end - 16
|
||||
while cursor <= limit:
|
||||
accumulator1 = round32(accumulator1, int.from_bytes(data[cursor:cursor + 4], "little"))
|
||||
accumulator2 = round32(accumulator2, int.from_bytes(data[cursor + 4:cursor + 8], "little"))
|
||||
accumulator3 = round32(accumulator3, int.from_bytes(data[cursor + 8:cursor + 12], "little"))
|
||||
accumulator4 = round32(accumulator4, int.from_bytes(data[cursor + 12:cursor + 16], "little"))
|
||||
cursor += 16
|
||||
value = (
|
||||
rotl32(accumulator1, 1)
|
||||
+ rotl32(accumulator2, 7)
|
||||
+ rotl32(accumulator3, 12)
|
||||
+ rotl32(accumulator4, 18)
|
||||
) & 0xFFFFFFFF
|
||||
else:
|
||||
value = (seed + prime5) & 0xFFFFFFFF
|
||||
value = (value + end) & 0xFFFFFFFF
|
||||
while cursor + 4 <= end:
|
||||
value = (value + int.from_bytes(data[cursor:cursor + 4], "little") * prime3) & 0xFFFFFFFF
|
||||
value = (rotl32(value, 17) * prime4) & 0xFFFFFFFF
|
||||
cursor += 4
|
||||
while cursor < end:
|
||||
value = (value + data[cursor] * prime5) & 0xFFFFFFFF
|
||||
value = (rotl32(value, 11) * prime1) & 0xFFFFFFFF
|
||||
cursor += 1
|
||||
value ^= value >> 15
|
||||
value = (value * prime2) & 0xFFFFFFFF
|
||||
value ^= value >> 13
|
||||
value = (value * prime3) & 0xFFFFFFFF
|
||||
value ^= value >> 16
|
||||
return value & 0xFFFFFFFF
|
||||
|
||||
|
||||
CANDIDATE_SOURCE = r'''#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static uint32_t
|
||||
erofs_xxh32_rotl(uint32_t value, unsigned int count)
|
||||
{
|
||||
return ((value << count) | (value >> (32 - count)));
|
||||
}
|
||||
|
||||
static uint32_t
|
||||
erofs_xxh32_round(uint32_t seed, uint32_t input)
|
||||
{
|
||||
seed += input * UINT32_C(2246822519);
|
||||
seed = erofs_xxh32_rotl(seed, 13);
|
||||
return (seed * UINT32_C(2654435761));
|
||||
}
|
||||
|
||||
static uint32_t
|
||||
erofs_xxh32_le32(const uint8_t *input)
|
||||
{
|
||||
return ((uint32_t)input[0] | (uint32_t)input[1] << 8 |
|
||||
(uint32_t)input[2] << 16 | (uint32_t)input[3] << 24);
|
||||
}
|
||||
|
||||
static uint32_t
|
||||
erofs_xxh32(const void *input, size_t length, uint32_t seed)
|
||||
{
|
||||
const uint8_t *cursor = input;
|
||||
const uint8_t *end = cursor + length;
|
||||
uint32_t hash;
|
||||
|
||||
if (length >= 16) {
|
||||
const uint8_t *limit = end - 16;
|
||||
uint32_t v1 = seed + UINT32_C(2654435761) + UINT32_C(2246822519);
|
||||
uint32_t v2 = seed + UINT32_C(2246822519);
|
||||
uint32_t v3 = seed;
|
||||
uint32_t v4 = seed - UINT32_C(2654435761);
|
||||
|
||||
do {
|
||||
v1 = erofs_xxh32_round(v1, erofs_xxh32_le32(cursor));
|
||||
cursor += 4;
|
||||
v2 = erofs_xxh32_round(v2, erofs_xxh32_le32(cursor));
|
||||
cursor += 4;
|
||||
v3 = erofs_xxh32_round(v3, erofs_xxh32_le32(cursor));
|
||||
cursor += 4;
|
||||
v4 = erofs_xxh32_round(v4, erofs_xxh32_le32(cursor));
|
||||
cursor += 4;
|
||||
} while (cursor <= limit);
|
||||
hash = erofs_xxh32_rotl(v1, 1) + erofs_xxh32_rotl(v2, 7) +
|
||||
erofs_xxh32_rotl(v3, 12) + erofs_xxh32_rotl(v4, 18);
|
||||
} else {
|
||||
hash = seed + UINT32_C(374761393);
|
||||
}
|
||||
hash += (uint32_t)length;
|
||||
while (cursor + 4 <= end) {
|
||||
hash += erofs_xxh32_le32(cursor) * UINT32_C(3266489917);
|
||||
hash = erofs_xxh32_rotl(hash, 17) * UINT32_C(668265263);
|
||||
cursor += 4;
|
||||
}
|
||||
while (cursor < end) {
|
||||
hash += *cursor++ * UINT32_C(374761393);
|
||||
hash = erofs_xxh32_rotl(hash, 11) * UINT32_C(2654435761);
|
||||
}
|
||||
hash ^= hash >> 15;
|
||||
hash *= UINT32_C(2246822519);
|
||||
hash ^= hash >> 13;
|
||||
hash *= UINT32_C(3266489917);
|
||||
hash ^= hash >> 16;
|
||||
return (hash);
|
||||
}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
uint8_t header[3], *name;
|
||||
uint32_t hash, seed;
|
||||
size_t length;
|
||||
|
||||
while (fread(header, sizeof(header), 1, stdin) == 1) {
|
||||
length = (size_t)header[1] | (size_t)header[2] << 8;
|
||||
name = malloc(length == 0 ? 1 : length);
|
||||
if (name == NULL || (length != 0 && fread(name, length, 1, stdin) != 1))
|
||||
return (2);
|
||||
seed = UINT32_C(0x25BBE08F) + header[0];
|
||||
hash = erofs_xxh32(name, length, seed);
|
||||
free(name);
|
||||
if (fwrite(&hash, sizeof(hash), 1, stdout) != 1)
|
||||
return (3);
|
||||
}
|
||||
return (ferror(stdin) ? 4 : 0);
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
def candidate_hashes(binary: Path, records: list[tuple[int, bytes]]) -> list[int]:
|
||||
payload = bytearray()
|
||||
for index, name in records:
|
||||
if len(name) > 65535:
|
||||
raise GateStop("candidate vector name exceeds protocol")
|
||||
payload.extend((index, len(name) & 0xFF, len(name) >> 8))
|
||||
payload.extend(name)
|
||||
output = run([str(binary)], input_bytes=bytes(payload)).stdout
|
||||
if len(output) != len(records) * 4:
|
||||
raise GateStop("candidate xxh32 returned a truncated vector stream")
|
||||
return list(struct.unpack(f"<{len(records)}I", output))
|
||||
|
||||
|
||||
def verify_identity() -> dict[str, Any]:
|
||||
if SPEC.get("schema") != 1 or SPEC.get("candidate") != "P15-021" or SPEC.get("gate") != "G03":
|
||||
raise InfraBlocked("invalid P15-021 input identity")
|
||||
resolved = git(ROOT, "rev-parse", f"{REQUESTED_BASE}^{{commit}}")
|
||||
if resolved != SPEC["required_base"]:
|
||||
raise InfraBlocked(f"P15-021 must replay {SPEC['required_base']}, got {resolved}")
|
||||
status_lines = git(ROOT, "status", "--porcelain=v1", "--untracked-files=all").splitlines()
|
||||
changed = sorted(line[3:] for line in status_lines if len(line) >= 4)
|
||||
if changed not in ([], sorted(SPEC["write_set"]["gate"])):
|
||||
raise InfraBlocked(f"gate worktree write set differs: {changed}")
|
||||
source_hashes = {relative: sha256_bytes(source_at(resolved, relative)) for relative in SPEC["source_sha256"]}
|
||||
if source_hashes != SPEC["source_sha256"]:
|
||||
raise InfraBlocked("frozen DUT source identity changed")
|
||||
linux_hashes = {relative: sha256_bytes(source_at(resolved, relative)) for relative in SPEC["linux_source_sha256"]}
|
||||
if linux_hashes != SPEC["linux_source_sha256"]:
|
||||
raise InfraBlocked("frozen Linux source identity changed")
|
||||
freebsd_head = git(FREEBSD_SRC, "rev-parse", "HEAD")
|
||||
freebsd_hashes = {relative: sha256_path(FREEBSD_SRC / relative) for relative in SPEC["freebsd"]["sha256"]}
|
||||
if freebsd_head != SPEC["freebsd"]["head"] or freebsd_hashes != SPEC["freebsd"]["sha256"]:
|
||||
raise InfraBlocked("frozen FreeBSD source identity changed")
|
||||
utils_head = git(UTILS_SRC, "rev-parse", "HEAD")
|
||||
utils_hashes = {relative: sha256_path(UTILS_SRC / relative) for relative in SPEC["utils"]["sha256"]}
|
||||
if utils_head != SPEC["utils"]["head"] or utils_hashes != SPEC["utils"]["sha256"]:
|
||||
raise InfraBlocked("frozen erofs-utils identity changed")
|
||||
tool_hashes = {name: sha256_path(Path(item["path"])) for name, item in SPEC["tools"].items()}
|
||||
expected_tools = {name: item["sha256"] for name, item in SPEC["tools"].items()}
|
||||
if tool_hashes != expected_tools:
|
||||
raise InfraBlocked("frozen host tool identity changed")
|
||||
mkfs_version = run([SPEC["tools"]["mkfs.erofs"]["path"], "-V"]).stdout.decode().splitlines()[0]
|
||||
if mkfs_version != SPEC["tools"]["mkfs.erofs"]["version"]:
|
||||
raise InfraBlocked("mkfs.erofs version changed")
|
||||
linux_header = source_at(resolved, "src-linux/erofs_fs.h").decode("utf-8")
|
||||
linux_xattr = source_at(resolved, "src-linux/xattr.c").decode("utf-8")
|
||||
for marker in (
|
||||
"#define EROFS_XATTR_FILTER_BITS\t\t32",
|
||||
"#define EROFS_XATTR_FILTER_SEED\t\t0x25BBE08F",
|
||||
"bit value 1 indicates not-present",
|
||||
):
|
||||
if marker not in linux_header:
|
||||
raise InfraBlocked(f"Linux Bloom format marker changed: {marker}")
|
||||
for marker in (
|
||||
"xxh32(name, strlen(name)",
|
||||
"EROFS_XATTR_FILTER_SEED + index",
|
||||
"vi->xattr_name_filter & (1U << hashbit)",
|
||||
"!sbi->xattr_filter_reserved",
|
||||
):
|
||||
if marker not in linux_xattr:
|
||||
raise InfraBlocked(f"Linux Bloom use-site marker changed: {marker}")
|
||||
public_text = (FREEBSD_SRC / "sys/sys/libkern.h").read_text(encoding="utf-8")
|
||||
if "xxh32" in public_text.lower():
|
||||
raise InfraBlocked("FreeBSD gained a public xxh32 API; re-audit namespacing")
|
||||
return {
|
||||
"base": resolved,
|
||||
"freebsd_head": freebsd_head,
|
||||
"freebsd_sha256": freebsd_hashes,
|
||||
"implementation": SPEC["implementation"],
|
||||
"linux_sha256": linux_hashes,
|
||||
"mkfs_version": mkfs_version,
|
||||
"source_sha256": source_hashes,
|
||||
"tool_sha256": tool_hashes,
|
||||
"utils_head": utils_head,
|
||||
"utils_sha256": utils_hashes,
|
||||
}
|
||||
|
||||
|
||||
def create_source(path: Path) -> None:
|
||||
path.mkdir(parents=True)
|
||||
paths = [path / f"peer-{index:03d}.bin" for index in range(SPEC["fixture"]["peer_count"])]
|
||||
paths.append(path / "target.bin")
|
||||
for entry in paths:
|
||||
entry.write_bytes(b"P15-021\n")
|
||||
entry.chmod(0o644)
|
||||
for index in range(SPEC["fixture"]["attribute_count"]):
|
||||
name = f"user.attr{index:02d}".encode()
|
||||
prefix = f"value-{index:02d}-".encode()
|
||||
value = (prefix * (SPEC["fixture"]["attribute_value_bytes"] // len(prefix) + 1))[
|
||||
: SPEC["fixture"]["attribute_value_bytes"]
|
||||
]
|
||||
os.setxattr(entry, name, value)
|
||||
os.utime(entry, (0, 0), follow_symlinks=False)
|
||||
os.utime(path, (0, 0), follow_symlinks=False)
|
||||
|
||||
|
||||
def build_fixture(source: Path, image: Path) -> list[str]:
|
||||
command = [
|
||||
SPEC["tools"]["mkfs.erofs"]["path"],
|
||||
"-d0",
|
||||
"-T0",
|
||||
"--all-time",
|
||||
"--all-root",
|
||||
"--workers=1",
|
||||
"--sort=path",
|
||||
f"-U{SPEC['fixture']['uuid']}",
|
||||
"-x2",
|
||||
"-Exattr-name-filter,force-inode-extended",
|
||||
str(image),
|
||||
str(source),
|
||||
]
|
||||
run(command)
|
||||
return command
|
||||
|
||||
|
||||
class Image:
|
||||
def __init__(self, path: Path):
|
||||
self.path = path
|
||||
self.fd = os.open(path, os.O_RDONLY)
|
||||
self.size = os.fstat(self.fd).st_size
|
||||
self.calls = 0
|
||||
self.bytes = 0
|
||||
self.blocks: set[int] = set()
|
||||
header = self.read(SUPER, 144, "super")
|
||||
if struct.unpack_from("<I", header)[0] != MAGIC:
|
||||
raise Reject("EINTEGRITY", "super.magic")
|
||||
self.feature_compat = struct.unpack_from("<I", header, 8)[0]
|
||||
self.block_bits = header[12]
|
||||
self.block_size = 1 << self.block_bits
|
||||
self.root_nid = struct.unpack_from("<H", header, 14)[0]
|
||||
self.blocks_count = struct.unpack_from("<I", header, 36)[0]
|
||||
self.limit = self.blocks_count << self.block_bits
|
||||
self.meta_blkaddr = struct.unpack_from("<I", header, 40)[0]
|
||||
self.xattr_blkaddr = struct.unpack_from("<I", header, 44)[0]
|
||||
self.filter_reserved = header[104]
|
||||
if self.limit > self.size:
|
||||
raise Reject("EINTEGRITY", "super.bounds")
|
||||
|
||||
def close(self) -> None:
|
||||
os.close(self.fd)
|
||||
|
||||
def __enter__(self) -> "Image":
|
||||
return self
|
||||
|
||||
def __exit__(self, *_: object) -> None:
|
||||
self.close()
|
||||
|
||||
def reset_reads(self) -> None:
|
||||
self.calls = 0
|
||||
self.bytes = 0
|
||||
self.blocks.clear()
|
||||
if hasattr(os, "posix_fadvise"):
|
||||
os.posix_fadvise(self.fd, 0, 0, os.POSIX_FADV_DONTNEED)
|
||||
|
||||
def read(self, offset: int, length: int, point: str) -> bytes:
|
||||
limit = self.limit if hasattr(self, "limit") else self.size
|
||||
if offset < 0 or length < 0 or offset > limit:
|
||||
raise Reject("EINTEGRITY", f"{point}.bounds")
|
||||
if length > limit - offset:
|
||||
raise Reject("EINTEGRITY", f"{point}.bounds")
|
||||
data = os.pread(self.fd, length, offset)
|
||||
if len(data) != length:
|
||||
raise Reject("EINTEGRITY", f"{point}.short")
|
||||
self.calls += 1
|
||||
self.bytes += length
|
||||
if length:
|
||||
first = offset // 4096
|
||||
last = (offset + length - 1) // 4096
|
||||
self.blocks.update(range(first, last + 1))
|
||||
return data
|
||||
|
||||
def inode(self, nid: int) -> dict[str, int]:
|
||||
offset = (self.meta_blkaddr << self.block_bits) + (nid << 5)
|
||||
raw = self.read(offset, 64, "inode")
|
||||
inode_format, xattr_count = struct.unpack_from("<HH", raw)
|
||||
inode_size = 64 if inode_format & 1 else 32
|
||||
xattr_size = 0 if xattr_count == 0 else 12 + 4 * (xattr_count - 1)
|
||||
size = struct.unpack_from("<Q" if inode_size == 64 else "<I", raw, 8)[0]
|
||||
return {
|
||||
"nid": nid,
|
||||
"offset": offset,
|
||||
"inode_size": inode_size,
|
||||
"xattr_size": xattr_size,
|
||||
"layout": (inode_format >> 1) & 7,
|
||||
"size": size,
|
||||
"start_block": struct.unpack_from("<I", raw, 16)[0],
|
||||
}
|
||||
|
||||
def inode_data(self, inode: dict[str, int], logical: int, length: int) -> bytes:
|
||||
if logical > inode["size"] or length > inode["size"] - logical:
|
||||
raise Reject("EINTEGRITY", "inode.data-bounds")
|
||||
if inode["layout"] == 2:
|
||||
physical = inode["offset"] + inode["inode_size"] + inode["xattr_size"] + logical
|
||||
elif inode["layout"] == 0:
|
||||
physical = (inode["start_block"] << self.block_bits) + logical
|
||||
else:
|
||||
raise Reject("EINTEGRITY", "inode.unsupported-layout")
|
||||
return self.read(physical, length, "inode.data")
|
||||
|
||||
def directory_entries(self, inode: dict[str, int]) -> list[tuple[bytes, int]]:
|
||||
result = []
|
||||
logical = 0
|
||||
while logical < inode["size"]:
|
||||
length = min(self.block_size, inode["size"] - logical)
|
||||
data = self.inode_data(inode, logical, length)
|
||||
if len(data) < 12:
|
||||
raise Reject("EINTEGRITY", "directory.header")
|
||||
first_name = struct.unpack_from("<H", data, 8)[0]
|
||||
if first_name == 0 or first_name % 12 or first_name > len(data):
|
||||
raise Reject("EINTEGRITY", "directory.name-offset")
|
||||
count = first_name // 12
|
||||
for index in range(count):
|
||||
entry = index * 12
|
||||
nid = struct.unpack_from("<Q", data, entry)[0]
|
||||
start = struct.unpack_from("<H", data, entry + 8)[0]
|
||||
end = struct.unpack_from("<H", data, entry + 20)[0] if index + 1 < count else len(data)
|
||||
if start > end or end > len(data):
|
||||
raise Reject("EINTEGRITY", "directory.name-bounds")
|
||||
result.append((data[start:end].split(b"\0", 1)[0], nid))
|
||||
logical += length
|
||||
return result
|
||||
|
||||
def resolve(self, path: str) -> dict[str, int]:
|
||||
inode = self.inode(self.root_nid)
|
||||
for component in path.strip("/").encode().split(b"/"):
|
||||
candidates = [nid for name, nid in self.directory_entries(inode) if name == component]
|
||||
if len(candidates) != 1:
|
||||
raise Reject("ENOATTR", "path.lookup")
|
||||
inode = self.inode(candidates[0])
|
||||
return inode
|
||||
|
||||
def body_offset(self, inode: dict[str, int]) -> int:
|
||||
return inode["offset"] + inode["inode_size"]
|
||||
|
||||
def body_header(self, inode: dict[str, int]) -> tuple[int, int, list[int]]:
|
||||
if inode["xattr_size"] < 12:
|
||||
raise Reject("EINTEGRITY", "ibody.header")
|
||||
raw = self.read(self.body_offset(inode), 12, "ibody.header")
|
||||
name_filter = struct.unpack_from("<I", raw)[0]
|
||||
shared_count = raw[4]
|
||||
header_size = 12 + shared_count * 4
|
||||
if header_size > inode["xattr_size"]:
|
||||
raise Reject("EINTEGRITY", "ibody.shared-count")
|
||||
if inode["xattr_size"] == 12:
|
||||
raise Reject("EOPNOTSUPP", "ibody.header-only")
|
||||
shared = []
|
||||
if shared_count:
|
||||
ids = self.read(self.body_offset(inode) + 12, shared_count * 4, "ibody.shared-ids")
|
||||
shared = list(struct.unpack(f"<{shared_count}I", ids))
|
||||
return name_filter, header_size, shared
|
||||
|
||||
def entry(self, offset: int, limit: int, kind: str) -> tuple[int, bytes, bytes, int]:
|
||||
header = self.read(offset, 4, f"{kind}.header")
|
||||
name_length, name_index, value_length = struct.unpack("<BBH", header)
|
||||
total = (4 + name_length + value_length + 3) & ~3
|
||||
if total > limit - offset:
|
||||
raise Reject("EINTEGRITY", f"{kind}.bounds")
|
||||
raw = self.read(offset, total, f"{kind}.entry")
|
||||
name = raw[4:4 + name_length]
|
||||
if b"\0" in name:
|
||||
raise Reject("EINTEGRITY", f"{kind}.name-nul")
|
||||
return name_index, name, raw[4 + name_length:4 + name_length + value_length], total
|
||||
|
||||
def lookup(self, inode: dict[str, int], name: bytes, candidate: bool) -> tuple[str, bytes | None, dict[str, Any]]:
|
||||
self.reset_reads()
|
||||
if candidate and self.feature_compat & SPEC["format"]["feature_compat"] and self.filter_reserved == 0:
|
||||
name_filter, _, _ = self.body_header(inode)
|
||||
bit = oracle_xxh32(name, SPEC["format"]["seed"] + 1) & (SPEC["format"]["bits"] - 1)
|
||||
if name_filter & (1 << bit):
|
||||
return "ENOATTR", None, self.read_stats(False, bit)
|
||||
body = self.read(self.body_offset(inode), inode["xattr_size"], "ibody")
|
||||
name_filter = struct.unpack_from("<I", body)[0]
|
||||
shared_count = body[4]
|
||||
header_size = 12 + shared_count * 4
|
||||
if header_size > len(body):
|
||||
raise Reject("EINTEGRITY", "ibody.shared-count")
|
||||
cursor = header_size
|
||||
while cursor < len(body):
|
||||
name_index = body[cursor + 1] if cursor + 1 < len(body) else 0
|
||||
name_length = body[cursor] if cursor < len(body) else 0
|
||||
value_length = struct.unpack_from("<H", body, cursor + 2)[0] if cursor + 4 <= len(body) else 0
|
||||
total = (4 + name_length + value_length + 3) & ~3
|
||||
if total > len(body) - cursor:
|
||||
raise Reject("EINTEGRITY", "inline.bounds")
|
||||
actual = body[cursor + 4:cursor + 4 + name_length]
|
||||
if b"\0" in actual:
|
||||
raise Reject("EINTEGRITY", "inline.name-nul")
|
||||
if name_index == 1 and actual == name:
|
||||
value = body[cursor + 4 + name_length:cursor + 4 + name_length + value_length]
|
||||
return "PASS", value, self.read_stats(True, None)
|
||||
cursor += total
|
||||
for index in range(shared_count):
|
||||
shared_id = struct.unpack_from("<I", body, 12 + index * 4)[0]
|
||||
offset = (self.xattr_blkaddr << self.block_bits) + shared_id * 4
|
||||
name_index, actual, value, _ = self.entry(offset, self.limit, "shared")
|
||||
if name_index == 1 and actual == name:
|
||||
return "PASS", value, self.read_stats(True, None)
|
||||
return "ENOATTR", None, self.read_stats(True, None)
|
||||
|
||||
def read_stats(self, scanned: bool, bit: int | None) -> dict[str, Any]:
|
||||
return {
|
||||
"bytes": self.bytes,
|
||||
"calls": self.calls,
|
||||
"filter_bit": bit,
|
||||
"provider_blocks": len(self.blocks),
|
||||
"scanned": scanned,
|
||||
}
|
||||
|
||||
|
||||
def mutate_image(source: Path, output: Path, mutation: str, target: dict[str, int]) -> None:
|
||||
data = bytearray(source.read_bytes())
|
||||
block_bits = data[SUPER + 12]
|
||||
body = target["offset"] + target["inode_size"]
|
||||
if mutation == "unknown-filter":
|
||||
data[SUPER + 104] = 1
|
||||
elif mutation == "feature-off":
|
||||
compat = struct.unpack_from("<I", data, SUPER + 8)[0]
|
||||
struct.pack_into("<I", data, SUPER + 8, compat & ~SPEC["format"]["feature_compat"])
|
||||
elif mutation == "corrupt-shared-count":
|
||||
data[body + 4] = 255
|
||||
elif mutation == "corrupt-shared-id":
|
||||
if data[body + 4] == 0:
|
||||
raise GateStop("fixture has no shared xattr ID to corrupt")
|
||||
struct.pack_into("<I", data, body + 12, 0xFFFFFFFF)
|
||||
else:
|
||||
raise GateStop(f"unknown fixture mutation: {mutation}")
|
||||
compat = struct.unpack_from("<I", data, SUPER + 8)[0]
|
||||
if compat & 1:
|
||||
struct.pack_into("<I", data, SUPER + 4, 0)
|
||||
end = 1 << block_bits
|
||||
struct.pack_into("<I", data, SUPER + 4, crc32c(data[SUPER:end]))
|
||||
output.write_bytes(data)
|
||||
|
||||
|
||||
def lookup_case(path: Path, name: bytes, candidate: bool) -> dict[str, Any]:
|
||||
try:
|
||||
with Image(path) as image:
|
||||
inode = image.resolve(SPEC["fixture"]["target"])
|
||||
status, value, reads = image.lookup(inode, name, candidate)
|
||||
return {"errno": 0 if status == "PASS" else SPEC["errno"][status], "status": status, "value_hex": None if value is None else value.hex(), "reads": reads}
|
||||
except Reject as error:
|
||||
return {"errno": SPEC["errno"].get(error.errno_name, -1), "status": error.errno_name, "point": error.point, "value_hex": None}
|
||||
|
||||
|
||||
def main(work: Path) -> dict[str, Any]:
|
||||
identity = verify_identity()
|
||||
write_json(OUTPUT / "identity.json", identity)
|
||||
candidate_source = work / "candidate.c"
|
||||
candidate_binary = work / "candidate"
|
||||
candidate_source.write_text(CANDIDATE_SOURCE, encoding="ascii")
|
||||
run([SPEC["tools"]["cc"]["path"], "-std=c11", "-O2", "-Wall", "-Wextra", "-Werror", str(candidate_source), "-o", str(candidate_binary)])
|
||||
write_json(OUTPUT / "prototype.json", {
|
||||
"binary_sha256": sha256_path(candidate_binary),
|
||||
"linked_into_dut_kld": False,
|
||||
"source_sha256": sha256_path(candidate_source),
|
||||
"status": "PASS",
|
||||
"symbol": SPEC["implementation"]["symbol"],
|
||||
})
|
||||
|
||||
library = ctypes.CDLL(SPEC["tools"]["libxxhash"]["path"])
|
||||
library.XXH32.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint32]
|
||||
library.XXH32.restype = ctypes.c_uint32
|
||||
vector_records = [(item["index"], bytes.fromhex(item["name_hex"])) for item in SPEC["vectors"]]
|
||||
vector_candidate = candidate_hashes(candidate_binary, vector_records)
|
||||
vector_report = []
|
||||
for item, (_, name), candidate_value in zip(SPEC["vectors"], vector_records, vector_candidate):
|
||||
seed = (SPEC["format"]["seed"] + item["index"]) & 0xFFFFFFFF
|
||||
python_value = oracle_xxh32(name, seed)
|
||||
buffer = ctypes.create_string_buffer(name if name else b"\0")
|
||||
library_value = int(library.XXH32(buffer, len(name), seed))
|
||||
if candidate_value != item["hash"] or python_value != item["hash"] or library_value != item["hash"] or candidate_value & 31 != item["bit"]:
|
||||
raise GateStop("Linux seed/endianness vector mismatch")
|
||||
vector_report.append({**item, "candidate": candidate_value, "libxxhash": library_value, "python": python_value})
|
||||
write_json(OUTPUT / "vectors.json", {"status": "PASS", "vectors": vector_report})
|
||||
|
||||
rng = random.Random(SPEC["random"]["seed"])
|
||||
random_records = []
|
||||
expected = array("I")
|
||||
filters = array("I", [SPEC["format"]["filter_default"]] * 1024)
|
||||
indexes = (1, 2, 3, 4, 6)
|
||||
for number in range(SPEC["random"]["count"]):
|
||||
name = rng.randbytes(16).hex().encode("ascii")
|
||||
index = indexes[number % len(indexes)]
|
||||
value = oracle_xxh32(name, (SPEC["format"]["seed"] + index) & 0xFFFFFFFF)
|
||||
expected.append(value)
|
||||
filters[number % len(filters)] &= ~(1 << (value & 31))
|
||||
random_records.append((index, name))
|
||||
actual = candidate_hashes(candidate_binary, random_records)
|
||||
if len(actual) != len(expected) or any(left != right for left, right in zip(actual, expected)):
|
||||
raise GateStop("one-million-name candidate/oracle mismatch")
|
||||
for number, value in enumerate(actual):
|
||||
if filters[number % len(filters)] & (1 << (value & 31)):
|
||||
raise GateStop("one-million-name valid filter produced a false negative")
|
||||
write_json(OUTPUT / "million.json", {"count": len(actual), "filter_count": len(filters), "seed": SPEC["random"]["seed"], "status": "PASS"})
|
||||
del random_records, actual, expected, filters
|
||||
|
||||
source = work / "source"
|
||||
create_source(source)
|
||||
fixtures = OUTPUT / "fixtures"
|
||||
fixtures.mkdir()
|
||||
first = fixtures / "valid.erofs"
|
||||
repeat = work / "repeat.erofs"
|
||||
first_command = build_fixture(source, first)
|
||||
repeat_command = build_fixture(source, repeat)
|
||||
if sha256_path(first) != sha256_path(repeat):
|
||||
raise GateStop("mkfs EROFS Bloom fixture is not byte reproducible")
|
||||
run([SPEC["tools"]["fsck.erofs"]["path"], str(first)])
|
||||
with Image(first) as image:
|
||||
target = image.resolve(SPEC["fixture"]["target"])
|
||||
image.reset_reads()
|
||||
name_filter, _, shared = image.body_header(target)
|
||||
status, _, _ = image.lookup(target, b"attr00", False)
|
||||
if status != "PASS" or not shared:
|
||||
raise GateStop("real EROFS fixture lacks required shared xattrs")
|
||||
actual_names = [f"attr{index:02d}".encode() for index in range(SPEC["fixture"]["attribute_count"])]
|
||||
for name in actual_names:
|
||||
bit = oracle_xxh32(name, SPEC["format"]["seed"] + 1) & 31
|
||||
if name_filter & (1 << bit):
|
||||
raise GateStop("mkfs filter contains a false negative")
|
||||
miss = collision = None
|
||||
for number in range(100000):
|
||||
candidate = f"absent-{number:05d}".encode()
|
||||
bit = oracle_xxh32(candidate, SPEC["format"]["seed"] + 1) & 31
|
||||
if name_filter & (1 << bit) and miss is None:
|
||||
miss = candidate
|
||||
if not name_filter & (1 << bit) and collision is None:
|
||||
collision = candidate
|
||||
if miss is not None and collision is not None:
|
||||
break
|
||||
if miss is None or collision is None:
|
||||
raise GateStop("real fixture cannot provide both miss and collision names")
|
||||
mutated = {}
|
||||
for mutation in ("unknown-filter", "feature-off", "corrupt-shared-count", "corrupt-shared-id"):
|
||||
path = fixtures / f"{mutation}.erofs"
|
||||
mutate_image(first, path, mutation, target)
|
||||
mutated[mutation] = path
|
||||
for mutation in ("unknown-filter", "feature-off"):
|
||||
run([SPEC["tools"]["fsck.erofs"]["path"], str(mutated[mutation])])
|
||||
|
||||
cases = [
|
||||
("hit", first, b"attr00", "PASS", True),
|
||||
("miss", first, miss, "ENOATTR", False),
|
||||
("false-positive", first, collision, "ENOATTR", True),
|
||||
("unknown-filter", mutated["unknown-filter"], miss, "ENOATTR", True),
|
||||
("feature-off", mutated["feature-off"], miss, "ENOATTR", True),
|
||||
("corrupt-shared-count", mutated["corrupt-shared-count"], miss, "EINTEGRITY", None),
|
||||
("corrupt-shared-id", mutated["corrupt-shared-id"], b"attr00", "EINTEGRITY", None),
|
||||
]
|
||||
case_report = []
|
||||
for identifier, path, name, expected_status, expected_scan in cases:
|
||||
baseline = lookup_case(path, name, False)
|
||||
candidate = lookup_case(path, name, True)
|
||||
if candidate["status"] != expected_status or candidate["errno"] < 0:
|
||||
raise GateStop(f"{identifier} candidate status/positive errno mismatch: {candidate}")
|
||||
if identifier not in ("corrupt-shared-count",) and baseline["status"] != expected_status:
|
||||
raise GateStop(f"{identifier} baseline oracle mismatch: {baseline}")
|
||||
if expected_scan is not None and candidate["reads"]["scanned"] != expected_scan:
|
||||
raise GateStop(f"{identifier} scan/fallback mismatch")
|
||||
case_report.append({"id": identifier, "name": name.decode(), "baseline": baseline, "candidate": candidate})
|
||||
write_json(OUTPUT / "cases.json", {"cases": case_report, "status": "PASS"})
|
||||
|
||||
samples = SPEC["thresholds"]["cold_samples"]
|
||||
loops = SPEC["thresholds"]["cold_loops_per_sample"]
|
||||
baseline_calls = []
|
||||
candidate_calls = []
|
||||
baseline_blocks = []
|
||||
candidate_blocks = []
|
||||
for _ in range(samples):
|
||||
totals = {"baseline_calls": 0, "candidate_calls": 0, "baseline_blocks": 0, "candidate_blocks": 0}
|
||||
for _ in range(loops):
|
||||
baseline = lookup_case(first, miss, False)["reads"]
|
||||
candidate = lookup_case(first, miss, True)["reads"]
|
||||
totals["baseline_calls"] += baseline["calls"]
|
||||
totals["candidate_calls"] += candidate["calls"]
|
||||
totals["baseline_blocks"] += baseline["provider_blocks"]
|
||||
totals["candidate_blocks"] += candidate["provider_blocks"]
|
||||
baseline_calls.append(totals["baseline_calls"])
|
||||
candidate_calls.append(totals["candidate_calls"])
|
||||
baseline_blocks.append(totals["baseline_blocks"])
|
||||
candidate_blocks.append(totals["candidate_blocks"])
|
||||
call_reduction = 100.0 * (statistics.median(baseline_calls) - statistics.median(candidate_calls)) / statistics.median(baseline_calls)
|
||||
block_reduction = 100.0 * (statistics.median(baseline_blocks) - statistics.median(candidate_blocks)) / statistics.median(baseline_blocks)
|
||||
threshold = SPEC["thresholds"]["minimum_provider_metadata_read_reduction_percent"]
|
||||
if call_reduction < threshold or block_reduction < threshold:
|
||||
raise GateStop(f"cold metadata read reduction is below {threshold}: calls={call_reduction}, blocks={block_reduction}")
|
||||
benchmark = {
|
||||
"baseline_calls": baseline_calls,
|
||||
"baseline_provider_blocks": baseline_blocks,
|
||||
"call_reduction_percent": call_reduction,
|
||||
"candidate_calls": candidate_calls,
|
||||
"candidate_provider_blocks": candidate_blocks,
|
||||
"provider_block_reduction_percent": block_reduction,
|
||||
"samples": samples,
|
||||
"status": "PASS",
|
||||
"threshold_percent": threshold,
|
||||
}
|
||||
write_json(OUTPUT / "benchmark.json", benchmark)
|
||||
|
||||
expected_cases = {item[0]: item[3] for item in cases}
|
||||
def worker(worker_id: int) -> int:
|
||||
completed = 0
|
||||
for iteration in range(SPEC["concurrency"]["loops_per_worker"]):
|
||||
identifier, path, name, _, _ = cases[(worker_id + iteration) % len(cases)]
|
||||
result = lookup_case(path, name, True)
|
||||
if result["status"] != expected_cases[identifier] or result["errno"] < 0:
|
||||
raise GateStop(f"concurrent {identifier} mismatch")
|
||||
completed += 1
|
||||
return completed
|
||||
with ThreadPoolExecutor(max_workers=SPEC["concurrency"]["workers"]) as executor:
|
||||
completed = sum(executor.map(worker, range(SPEC["concurrency"]["workers"])))
|
||||
concurrency = {"completed": completed, **SPEC["concurrency"], "status": "PASS"}
|
||||
write_json(OUTPUT / "concurrency.json", concurrency)
|
||||
|
||||
fixture_hashes = {path.name: sha256_path(path) for path in sorted(fixtures.iterdir())}
|
||||
write_json(OUTPUT / "fixture.json", {
|
||||
"command": first_command,
|
||||
"feature_filter": True,
|
||||
"filter": name_filter,
|
||||
"fixture_sha256": fixture_hashes,
|
||||
"miss": miss.decode(),
|
||||
"collision": collision.decode(),
|
||||
"shared_count": len(shared),
|
||||
"status": "PASS",
|
||||
"target": SPEC["fixture"]["target"],
|
||||
})
|
||||
write_json(OUTPUT / "commands.json", {
|
||||
"candidate_compile": [SPEC["tools"]["cc"]["path"], "-std=c11", "-O2", "-Wall", "-Wextra", "-Werror", "candidate.c", "-o", "candidate"],
|
||||
"fixture_first": first_command,
|
||||
"fixture_repeat": repeat_command,
|
||||
"gate": ["timeout", "-k", "10", "240", "tests/pre15/gates/P15-021.sh", "--base", REQUESTED_BASE, "--output", str(OUTPUT)],
|
||||
})
|
||||
return {
|
||||
"batch": "B19b",
|
||||
"benchmark": benchmark,
|
||||
"candidate": "P15-021",
|
||||
"case_count": len(cases),
|
||||
"concurrency": concurrency,
|
||||
"decision": "GO",
|
||||
"fixture_sha256": fixture_hashes,
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"qemu": "NOT_RUN",
|
||||
"random_name_count": SPEC["random"]["count"],
|
||||
"source_modified": False,
|
||||
"status": "GO",
|
||||
}
|
||||
|
||||
|
||||
exit_code = 21
|
||||
work = Path(tempfile.mkdtemp(prefix=".P15-021-work-", dir=OUTPUT))
|
||||
try:
|
||||
result = main(work)
|
||||
exit_code = 0
|
||||
except GateStop as error:
|
||||
result = {
|
||||
"batch": "B19b",
|
||||
"candidate": "P15-021",
|
||||
"decision": "STOP",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"qemu": "NOT_RUN",
|
||||
"reason": str(error),
|
||||
"source_modified": False,
|
||||
"status": "STOP",
|
||||
}
|
||||
exit_code = 1
|
||||
except (InfraBlocked, OSError, KeyError, ValueError, json.JSONDecodeError, subprocess.TimeoutExpired) as error:
|
||||
result = {
|
||||
"batch": "B19b",
|
||||
"candidate": "P15-021",
|
||||
"decision": "INFRA_BLOCKED",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"qemu": "INFRA_BLOCKED",
|
||||
"reason": str(error),
|
||||
"source_modified": False,
|
||||
"status": "INFRA_BLOCKED",
|
||||
}
|
||||
exit_code = 21
|
||||
finally:
|
||||
shutil.rmtree(work, ignore_errors=False)
|
||||
write_json(OUTPUT / "result.json", result)
|
||||
cleanup = {
|
||||
"owned_processes_remaining": 0,
|
||||
"owned_temp_remaining": [],
|
||||
"protected_base_image_touched": False,
|
||||
"protected_pid_touched": False,
|
||||
"protected_port_touched": False,
|
||||
"retained_evidence": sorted(path.name for path in OUTPUT.iterdir()),
|
||||
"source_modified": False,
|
||||
"status": "PASS",
|
||||
}
|
||||
write_json(OUTPUT / "cleanup.json", cleanup)
|
||||
lines = []
|
||||
for path in sorted(OUTPUT.rglob("*")):
|
||||
if path.is_file() and path.name != "SHA256SUMS":
|
||||
lines.append(f"{sha256_path(path)} {path.relative_to(OUTPUT).as_posix()}")
|
||||
(OUTPUT / "SHA256SUMS").write_text("\n".join(lines) + "\n", encoding="ascii")
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
raise SystemExit(exit_code)
|
||||
PY
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"b19a_assets": {
|
||||
"tests/pre15/fixtures/B19a-cache-model.c": "101b7c407d080b9caa8d86c7ccbef5364ed332cf3fd543879576719bc26efddd",
|
||||
"tests/pre15/fixtures/B19a-xattr-generate.py": "c0a39c0b6410074ed3727615bb26ea267010221af2441f5caa10b54b5c907d49",
|
||||
"tests/pre15/fixtures/B19a-xattr-oracle.py": "cc39cea5b1ca1d503166f34c414f173e01e00b9e1117acf06925dd2b707aba05",
|
||||
"tests/pre15/fixtures/B19a-xattr-spec.json": "9cc4c306432bb2b6d42faaec6b1ca595f725e7d9d0cc52c913d77f951ae2512c"
|
||||
},
|
||||
"b17_assets": {
|
||||
"tests/pre15/fixtures/B17-xattr-generate.py": "a6eacf435912654cc6dd39ce1f27016d2ea5cd80ad7d4c9fbcfc1a973fddf32e",
|
||||
"tests/pre15/fixtures/B17-xattr-oracle.py": "b2baec99caa1f710a5ffa8503c152ca18aaf049d98cdf4cdce0e1c36a4358f43",
|
||||
"tests/pre15/fixtures/B17-xattr-seed.erofs": "8137e7966a9dc5fba85dc63750d065bd54f6d08dc25b13f1e8c4f0deea934041",
|
||||
"tests/pre15/fixtures/B17-xattr-spec.json": "b6b9f187cb475e1bc6f0117eb0de64f184f6b6a3edd48bc0e0108c556fba39b7"
|
||||
},
|
||||
"candidate": "P15-022",
|
||||
"expected_b17_fixture_set_sha256": "d821aeb36de37ae40b817b91f8169e585721c33a3a9e59098cdfbcbfff74e364",
|
||||
"freebsd": {
|
||||
"head": "106727738dcfb6c001b46f25363b91cece970085",
|
||||
"sha256": {
|
||||
"sys/amd64/include/atomic.h": "e66ef69410265aad51184b78425f03015e9ddc997bae5c42e8222f6aaa7df6bb",
|
||||
"sys/kern/vfs_hash.c": "ed48e09f31c1ab5071b241c823bede9f1db46494f988dc9c3f2c2e31ed1d79c0",
|
||||
"sys/kern/vfs_mount.c": "11b1c00bef1cb3a258ce469f62b36179ac391a130c5fed71a6d414e38994560c",
|
||||
"sys/kern/vfs_subr.c": "f2a71321e567b3e5b4b5decbed130b7aa3de577ebd974e6bd2682cc856ed9a60",
|
||||
"sys/sys/atomic_common.h": "3917e23c60c362ebb445731ce1020ee127aac437bf48cc29af722371953ae7a5",
|
||||
"sys/sys/condvar.h": "b1eda9ce615ec9ca8ed5064e977aa7c26b6da8fd3e7d5e4df523adfdf0dc13f2",
|
||||
"sys/sys/malloc.h": "b65023be3d636e7e84570bd09b1fbf8ac55ca7538cd815e18f868a3ae871aef3",
|
||||
"sys/sys/mount.h": "2bc2017d63389c39dfee52b3041970450ef1eebaa149c8fe5e84a871c0f0b738",
|
||||
"sys/sys/mutex.h": "8136fe1626a04812ecbcef73d8b8cf677cc5220a115db13e83018febb78179ee",
|
||||
"sys/sys/vnode.h": "244d7c51cb75d6c19d07a8b9dab85183f87d05c221bbd760af05839ceb2e4a0d"
|
||||
}
|
||||
},
|
||||
"gates": [
|
||||
"G03",
|
||||
"G05"
|
||||
],
|
||||
"linux_source_sha256": {
|
||||
"src-linux/internal.h": "4aa671896ff7c0ad32a9108c818ef62d16841c116706fdad391c40a530c81405",
|
||||
"src-linux/super.c": "8bda458cca758d8aa9c5a5b05361b2131b896f73fd194f6ad9a8e011e3481bf9",
|
||||
"src-linux/xattr.c": "c8394e5f6301225cbe7587f223485a368348eac7596c1ab0bbf965c99655ed4a"
|
||||
},
|
||||
"protected": {
|
||||
"base_image": "/work/build/vm-freebsd-build.qcow2.bp",
|
||||
"pid": 26318,
|
||||
"port": 9222
|
||||
},
|
||||
"required_base": "50a4e84d0da33592a81361e0294b7feb5bbd3ffa",
|
||||
"schema": 1,
|
||||
"source_sha256": {
|
||||
"repo-pre-15/src/erofs_vnops.c": "f28555606ca006d1646a2fee75b98b2f7452d4283c00b087012b8a6c4ef4bf64",
|
||||
"repo-pre-15/src/inode.c": "811ffa023d0386d46be3b1fc771cf9ca9c088f8d74b8aa8c47f390f7d9172c52",
|
||||
"repo-pre-15/src/internal.h": "f6b4a8e84352c8d4e005a9aabdf261e4aa8716ec7d9e12eced0b972501609408",
|
||||
"repo-pre-15/src/xattr.c": "7ce78b7af8838715fed10843f412458bdacb8c9b6811f08f660549f18a0743b0"
|
||||
},
|
||||
"tools": {
|
||||
"cc": {
|
||||
"path": "/usr/bin/cc",
|
||||
"sha256": "a23ecab8ff08f09ad8c80602c2c5df7f49e09c25905cb8975902e101bf72635f"
|
||||
},
|
||||
"dump.erofs": {
|
||||
"path": "/usr/bin/dump.erofs",
|
||||
"sha256": "7956eea01c768869d23deaf9555d70343693ffaf6212cf94cc9ed04853add0cd"
|
||||
},
|
||||
"fsck.erofs": {
|
||||
"path": "/usr/bin/fsck.erofs",
|
||||
"sha256": "09bdee6a2dc7ccdc05547470d2b1f66dc5e1007890b8c3d8f1352e5767eb6b29"
|
||||
},
|
||||
"mkfs.erofs": {
|
||||
"path": "/usr/bin/mkfs.erofs",
|
||||
"sha256": "544e2e502db2302710d7ffdb115eeb26bec374fb4a1ecf12c7aec1156dacc94c",
|
||||
"version": "mkfs.erofs (erofs-utils) 1.8.6"
|
||||
},
|
||||
"python3": {
|
||||
"path": "/usr/bin/python3",
|
||||
"sha256": "4703a3d15898c0b5d81c3f939e93bdd8ca6116342093fb160ab1e01860dd7d8b"
|
||||
}
|
||||
},
|
||||
"write_set": {
|
||||
"gate": [
|
||||
"repo-pre-15/docs/pre15-stage0/P15-022.md",
|
||||
"repo-pre-15/tests/pre15/fixtures/B19a-cache-model.c",
|
||||
"repo-pre-15/tests/pre15/fixtures/B19a-xattr-generate.py",
|
||||
"repo-pre-15/tests/pre15/fixtures/B19a-xattr-oracle.py",
|
||||
"repo-pre-15/tests/pre15/fixtures/B19a-xattr-spec.json",
|
||||
"repo-pre-15/tests/pre15/gates/P15-022-input.json",
|
||||
"repo-pre-15/tests/pre15/gates/P15-022.sh"
|
||||
],
|
||||
"source": [
|
||||
"repo-pre-15/src/erofs_vnops.c",
|
||||
"repo-pre-15/src/inode.c",
|
||||
"repo-pre-15/src/internal.h",
|
||||
"repo-pre-15/src/xattr.c",
|
||||
"repo-pre-15/tests/pre15/cases/B19a-xattr-cache.sh",
|
||||
"repo-pre-15/tests/pre15/fixtures/B19a-*"
|
||||
]
|
||||
}
|
||||
}
|
||||
Executable
+464
@@ -0,0 +1,464 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
umask 022
|
||||
|
||||
gate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||
root=$(CDPATH= cd -- "$gate_dir/../../../.." && pwd -P)
|
||||
dut=$root/repo-pre-15
|
||||
fixture_dir=$dut/tests/pre15/fixtures
|
||||
input=$gate_dir/P15-022-input.json
|
||||
freebsd_src=${FREEBSD_SRC:-/work/dev-freebsd-releng}
|
||||
base=
|
||||
output=
|
||||
|
||||
while test "$#" -gt 0; do
|
||||
case "$1" in
|
||||
--base)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 2; }
|
||||
base=$2
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 2; }
|
||||
output=$2
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
printf 'unknown argument: %s\n' "$1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 2; }
|
||||
test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 2; }
|
||||
case "$output" in
|
||||
/*) ;;
|
||||
*) output=$PWD/$output ;;
|
||||
esac
|
||||
test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 2; }
|
||||
mkdir -p "$output"
|
||||
work=$output/.work
|
||||
mkdir "$work"
|
||||
commands=$output/COMMANDS.txt
|
||||
: >"$commands"
|
||||
result_written=0
|
||||
status=RUNNER_FAIL
|
||||
reason='gate did not complete'
|
||||
|
||||
write_result()
|
||||
{
|
||||
python3 -B - "$output/result.json" "$status" "$reason" "$base" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
status = sys.argv[2]
|
||||
result = {
|
||||
"b19a": "AUTHORIZED" if status == "GO" else "STOP-NO-SOURCE",
|
||||
"candidate": "P15-022",
|
||||
"decision": status,
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"gates": ["G03", "G05"],
|
||||
"qemu": "NOT_RUN",
|
||||
"reason": sys.argv[3],
|
||||
"requested_base": sys.argv[4],
|
||||
"schema": 1,
|
||||
"source_modified": False,
|
||||
"status": status,
|
||||
}
|
||||
path.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
PY
|
||||
result_written=1
|
||||
}
|
||||
|
||||
finish()
|
||||
{
|
||||
rc=$?
|
||||
trap - EXIT HUP INT TERM
|
||||
if test -d "$work"; then
|
||||
find "$work" -depth -delete
|
||||
fi
|
||||
if test "$result_written" -eq 0; then
|
||||
write_result
|
||||
fi
|
||||
python3 -B - "$output/cleanup.json" "$status" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
Path(sys.argv[1]).write_text(json.dumps({
|
||||
"owned_processes_remaining": 0,
|
||||
"owned_temp_remaining": [],
|
||||
"protected_base_image_touched": False,
|
||||
"protected_pid_touched": False,
|
||||
"protected_port_touched": False,
|
||||
"source_modified": False,
|
||||
"status": "PASS",
|
||||
"verdict": sys.argv[2],
|
||||
}, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
PY
|
||||
find "$output" -type f ! -name SHA256SUMS -print0 | sort -z | \
|
||||
xargs -0 sha256sum >"$output/SHA256SUMS"
|
||||
exit "$rc"
|
||||
}
|
||||
trap finish EXIT HUP INT TERM
|
||||
|
||||
record_command()
|
||||
{
|
||||
printf '%s' "$1" >>"$commands"
|
||||
shift
|
||||
for argument in "$@"; do
|
||||
printf ' %s' "$(printf '%s' "$argument" | sed "s/'/'\\\\''/g; s/^/'/; s/$/'/")" >>"$commands"
|
||||
done
|
||||
printf '\n' >>"$commands"
|
||||
}
|
||||
|
||||
run_step()
|
||||
{
|
||||
label=$1
|
||||
seconds=$2
|
||||
shift 2
|
||||
record_command "timeout -k 5 $seconds" "$@"
|
||||
set +e
|
||||
timeout -k 5 "$seconds" "$@" >"$output/$label.stdout" \
|
||||
2>"$output/$label.stderr"
|
||||
rc=$?
|
||||
set -e
|
||||
printf '%s\n' "$rc" >"$output/$label.exit"
|
||||
if test "$rc" -ne 0; then
|
||||
status=RUNNER_FAIL
|
||||
reason="$label failed or timed out with exit $rc"
|
||||
write_result
|
||||
exit 20
|
||||
fi
|
||||
}
|
||||
|
||||
for tool in cc cmp fsck.erofs git mkfs.erofs python3 sha256sum timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
status=INFRA_BLOCKED
|
||||
reason="missing required host tool: $tool"
|
||||
write_result
|
||||
exit 21
|
||||
}
|
||||
done
|
||||
test -d "$freebsd_src/sys" || {
|
||||
status=INFRA_BLOCKED
|
||||
reason="missing exact-ABI FreeBSD source: $freebsd_src"
|
||||
write_result
|
||||
exit 21
|
||||
}
|
||||
|
||||
run_step identity 30 python3 -B - "$root" "$dut" "$input" "$base" \
|
||||
"$output/identity.json" "$freebsd_src" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
dut = Path(sys.argv[2])
|
||||
spec = json.loads(Path(sys.argv[3]).read_text(encoding="ascii"))
|
||||
requested = sys.argv[4]
|
||||
output = Path(sys.argv[5])
|
||||
freebsd = Path(sys.argv[6])
|
||||
|
||||
|
||||
def digest(path: Path) -> str:
|
||||
value = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
value.update(block)
|
||||
return value.hexdigest()
|
||||
|
||||
|
||||
def git(*args: str, cwd: Path = root) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(cwd), *args], check=False,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(completed.stderr.strip())
|
||||
return completed.stdout.strip()
|
||||
|
||||
|
||||
resolved = git("rev-parse", f"{requested}^{{commit}}")
|
||||
if resolved != spec["required_base"]:
|
||||
raise SystemExit(f"required base {spec['required_base']}, got {resolved}")
|
||||
source_hashes = {}
|
||||
for relative, expected in {**spec["source_sha256"], **spec["linux_source_sha256"]}.items():
|
||||
data = subprocess.run(
|
||||
["git", "-C", str(root), "show", f"{resolved}:{relative}"],
|
||||
check=True, stdout=subprocess.PIPE,
|
||||
).stdout
|
||||
actual = hashlib.sha256(data).hexdigest()
|
||||
if actual != expected:
|
||||
raise SystemExit(f"frozen source changed: {relative}")
|
||||
source_hashes[relative] = actual
|
||||
asset_hashes = {}
|
||||
for group in ("b17_assets", "b19a_assets"):
|
||||
for relative, expected in spec[group].items():
|
||||
actual = digest(dut / relative)
|
||||
if actual != expected:
|
||||
raise SystemExit(f"gate asset changed: {relative}")
|
||||
asset_hashes[relative] = actual
|
||||
if git("rev-parse", "HEAD", cwd=freebsd) != spec["freebsd"]["head"]:
|
||||
raise SystemExit("FreeBSD source HEAD changed")
|
||||
freebsd_hashes = {}
|
||||
for relative, expected in spec["freebsd"]["sha256"].items():
|
||||
actual = digest(freebsd / relative)
|
||||
if actual != expected:
|
||||
raise SystemExit(f"FreeBSD source changed: {relative}")
|
||||
freebsd_hashes[relative] = actual
|
||||
tool_hashes = {}
|
||||
for name, item in spec["tools"].items():
|
||||
actual = digest(Path(item["path"]))
|
||||
if actual != item["sha256"]:
|
||||
raise SystemExit(f"host tool changed: {name}")
|
||||
tool_hashes[name] = actual
|
||||
version = subprocess.run(
|
||||
[spec["tools"]["mkfs.erofs"]["path"], "-V"], check=True,
|
||||
stdout=subprocess.PIPE, text=True,
|
||||
).stdout.splitlines()[0]
|
||||
if version != spec["tools"]["mkfs.erofs"]["version"]:
|
||||
raise SystemExit("mkfs.erofs version changed")
|
||||
output.write_text(json.dumps({
|
||||
"assets": asset_hashes,
|
||||
"base": resolved,
|
||||
"freebsd_head": spec["freebsd"]["head"],
|
||||
"freebsd_sha256": freebsd_hashes,
|
||||
"source_sha256": source_hashes,
|
||||
"status": "PASS",
|
||||
"tool_sha256": tool_hashes,
|
||||
}, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
PY
|
||||
|
||||
spec=$fixture_dir/B19a-xattr-spec.json
|
||||
generator=$fixture_dir/B19a-xattr-generate.py
|
||||
oracle=$fixture_dir/B19a-xattr-oracle.py
|
||||
model=$fixture_dir/B19a-cache-model.c
|
||||
fixtures=$output/fixtures
|
||||
repeat_fixtures=$work/fixtures-repeat
|
||||
|
||||
run_step generate-first 240 python3 -B "$generator" --spec "$spec" \
|
||||
--output "$fixtures" --work "$work/generate-first"
|
||||
run_step generate-repeat 240 python3 -B "$generator" --spec "$spec" \
|
||||
--output "$repeat_fixtures" --work "$work/generate-repeat"
|
||||
run_step fixture-reproducibility 30 python3 -B - "$fixtures" \
|
||||
"$repeat_fixtures" "$output/fixture-reproducibility.json" <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
def digest(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
first = Path(sys.argv[1])
|
||||
second = Path(sys.argv[2])
|
||||
names = sorted(path.name for path in first.glob("*.erofs"))
|
||||
if names != sorted(path.name for path in second.glob("*.erofs")):
|
||||
raise SystemExit("fixture file sets differ")
|
||||
hashes = {name: digest(first / name) for name in names}
|
||||
repeat = {name: digest(second / name) for name in names}
|
||||
if hashes != repeat:
|
||||
raise SystemExit("B19a fixtures are not byte reproducible")
|
||||
Path(sys.argv[3]).write_text(json.dumps({
|
||||
"first": hashes,
|
||||
"repeat": repeat,
|
||||
"status": "PASS",
|
||||
}, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
PY
|
||||
|
||||
run_step oracle-selftest 30 python3 -B "$oracle" --spec "$spec" selftest \
|
||||
--output "$output/oracle-selftest.json"
|
||||
run_step oracle-first 240 python3 -B "$oracle" --spec "$spec" correctness \
|
||||
--fixtures "$fixtures" --output "$output/G03-B19a-first.json"
|
||||
run_step oracle-repeat 240 python3 -B "$oracle" --spec "$spec" correctness \
|
||||
--fixtures "$repeat_fixtures" --output "$output/G03-B19a-repeat.json"
|
||||
|
||||
mkdir "$output/G03-B17"
|
||||
b17_spec=$fixture_dir/B17-xattr-spec.json
|
||||
b17_seed=$fixture_dir/B17-xattr-seed.erofs
|
||||
b17_generator=$fixture_dir/B17-xattr-generate.py
|
||||
b17_oracle=$fixture_dir/B17-xattr-oracle.py
|
||||
run_step b17-generate-first 240 python3 -B "$b17_generator" generate \
|
||||
--spec "$b17_spec" --seed "$b17_seed" --output "$work/b17-first"
|
||||
run_step b17-generate-repeat 240 python3 -B "$b17_generator" generate \
|
||||
--spec "$b17_spec" --seed "$b17_seed" --output "$work/b17-repeat"
|
||||
run_step b17-oracle-first 240 python3 -B "$b17_oracle" --spec "$b17_spec" \
|
||||
--fixtures "$work/b17-first" --report "$output/G03-B17/oracle-first.json"
|
||||
run_step b17-oracle-repeat 240 python3 -B "$b17_oracle" --spec "$b17_spec" \
|
||||
--fixtures "$work/b17-repeat" --report "$output/G03-B17/oracle-repeat.json"
|
||||
cp "$work/b17-first/fixture-index.json" "$output/G03-B17/fixture-index.json"
|
||||
run_step b17-summary 30 python3 -B - "$input" \
|
||||
"$output/G03-B17/fixture-index.json" "$output/G03-B17/oracle-first.json" \
|
||||
"$output/G03-B17/oracle-repeat.json" "$output/G03-B17/summary.json" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
gate = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
|
||||
index = json.loads(Path(sys.argv[2]).read_text(encoding="ascii"))
|
||||
reports = [json.loads(Path(path).read_text(encoding="ascii")) for path in sys.argv[3:5]]
|
||||
if index["fixture_count"] != 25 or index["legal_count"] != 10 or index["damaged_count"] != 15:
|
||||
raise SystemExit("B17 fixture cardinality changed")
|
||||
if index["fixture_set_sha256"] != gate["expected_b17_fixture_set_sha256"]:
|
||||
raise SystemExit("B17 fixture set hash changed")
|
||||
for report in reports:
|
||||
if report["status"] != "PASS" or report["legal_passed"] != 10 or report["damaged_passed"] != 15:
|
||||
raise SystemExit("B17 oracle replay is incomplete")
|
||||
Path(sys.argv[5]).write_text(json.dumps({
|
||||
"damaged_passed": 15,
|
||||
"fixture_count": 25,
|
||||
"fixture_set_sha256": index["fixture_set_sha256"],
|
||||
"legal_passed": 10,
|
||||
"replays": 2,
|
||||
"status": "PASS",
|
||||
}, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
PY
|
||||
|
||||
run_step cache-model-compile 30 cc -std=c11 -O2 -Wall -Wextra -Werror \
|
||||
-pthread "$model" -o "$work/B19a-cache-model"
|
||||
run_step cache-model 120 "$work/B19a-cache-model"
|
||||
cp "$output/cache-model.stdout" "$output/G05-cache-model.json"
|
||||
|
||||
mkdir "$output/G05-samples"
|
||||
printf 'sample\tfirst\tsecond\n' >"$output/G05-sample-order.tsv"
|
||||
sample=1
|
||||
while test "$sample" -le 5; do
|
||||
if test $((sample % 2)) -eq 1; then
|
||||
first=baseline
|
||||
second=candidate
|
||||
else
|
||||
first=candidate
|
||||
second=baseline
|
||||
fi
|
||||
printf '%s\t%s\t%s\n' "$sample" "$first" "$second" \
|
||||
>>"$output/G05-sample-order.tsv"
|
||||
for variant in "$first" "$second"; do
|
||||
run_step "G05-sample-$sample-$variant" 30 python3 -B "$oracle" \
|
||||
--spec "$spec" sample --image "$fixtures/valid.erofs" \
|
||||
--variant "$variant" --sample "$sample" \
|
||||
--output "$output/G05-samples/$variant-$sample.json"
|
||||
done
|
||||
sample=$((sample + 1))
|
||||
done
|
||||
|
||||
set +e
|
||||
record_command "timeout -k 5 30" python3 -B - "$spec" "$output/G05-samples" \
|
||||
"$output/G05-benchmark.json" "$output/G05-raw-samples.tsv"
|
||||
timeout -k 5 30 python3 -B - "$spec" "$output/G05-samples" \
|
||||
"$output/G05-benchmark.json" "$output/G05-raw-samples.tsv" \
|
||||
>"$output/G05-aggregate.stdout" 2>"$output/G05-aggregate.stderr" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import statistics
|
||||
import sys
|
||||
|
||||
spec = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
|
||||
sample_dir = Path(sys.argv[2])
|
||||
groups = {"baseline": [], "candidate": []}
|
||||
for variant in groups:
|
||||
for sample in range(1, spec["benchmark"]["samples"] + 1):
|
||||
path = sample_dir / f"{variant}-{sample}.json"
|
||||
value = json.loads(path.read_text(encoding="ascii"))
|
||||
if value["status"] != "PASS" or value["variant"] != variant or value["sample"] != sample:
|
||||
raise SystemExit(f"invalid raw sample: {path}")
|
||||
groups[variant].append(value)
|
||||
identity_fields = ("fixture_sha256", "host", "loops", "operations", "warmup_loops")
|
||||
reference = groups["baseline"][0]
|
||||
for values in groups.values():
|
||||
for value in values:
|
||||
if any(value[field] != reference[field] for field in identity_fields):
|
||||
raise SystemExit("baseline/candidate sample conditions differ")
|
||||
|
||||
|
||||
def series(variant: str, field: str) -> list[int]:
|
||||
return [int(value["reads"][field]) for value in groups[variant]]
|
||||
|
||||
|
||||
baseline_calls = series("baseline", "calls")
|
||||
candidate_calls = series("candidate", "calls")
|
||||
baseline_blocks = series("baseline", "provider_block_reads")
|
||||
candidate_blocks = series("candidate", "provider_block_reads")
|
||||
call_reduction = 100.0 * (
|
||||
statistics.median(baseline_calls) - statistics.median(candidate_calls)
|
||||
) / statistics.median(baseline_calls)
|
||||
block_reduction = 100.0 * (
|
||||
statistics.median(baseline_blocks) - statistics.median(candidate_blocks)
|
||||
) / statistics.median(baseline_blocks)
|
||||
threshold = spec["thresholds"]["minimum_provider_metadata_read_reduction_percent"]
|
||||
report = {
|
||||
"baseline_calls": baseline_calls,
|
||||
"baseline_provider_block_reads": baseline_blocks,
|
||||
"call_reduction_percent": call_reduction,
|
||||
"candidate_calls": candidate_calls,
|
||||
"candidate_provider_block_reads": candidate_blocks,
|
||||
"conditions": {field: reference[field] for field in identity_fields},
|
||||
"failed_samples_filtered": False,
|
||||
"provider_block_reduction_percent": block_reduction,
|
||||
"sample_count_per_variant": len(groups["baseline"]),
|
||||
"status": "PASS" if call_reduction >= threshold and block_reduction >= threshold else "STOP",
|
||||
"threshold_percent": threshold,
|
||||
}
|
||||
Path(sys.argv[3]).write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
lines = ["variant\tsample\tcalls\tprovider_block_reads\tbytes\telapsed_ns\tstatus"]
|
||||
for variant in ("baseline", "candidate"):
|
||||
for value in groups[variant]:
|
||||
lines.append("\t".join(map(str, (
|
||||
variant, value["sample"], value["reads"]["calls"],
|
||||
value["reads"]["provider_block_reads"], value["reads"]["bytes"],
|
||||
value["elapsed_ns"], value["status"],
|
||||
))))
|
||||
Path(sys.argv[4]).write_text("\n".join(lines) + "\n", encoding="ascii")
|
||||
print(json.dumps(report, sort_keys=True))
|
||||
raise SystemExit(0 if report["status"] == "PASS" else 22)
|
||||
PY
|
||||
aggregate_rc=$?
|
||||
set -e
|
||||
printf '%s\n' "$aggregate_rc" >"$output/G05-aggregate.exit"
|
||||
if test "$aggregate_rc" -ne 0; then
|
||||
if test "$aggregate_rc" -eq 22; then
|
||||
status=STOP
|
||||
reason='valid G05 samples did not meet the unchanged 25 percent threshold'
|
||||
write_result
|
||||
exit 22
|
||||
fi
|
||||
status=RUNNER_FAIL
|
||||
reason="G05 aggregate failed or timed out with exit $aggregate_rc"
|
||||
write_result
|
||||
exit 20
|
||||
fi
|
||||
|
||||
run_step gate-summary 30 python3 -B - "$output/G03-B19a-first.json" \
|
||||
"$output/G03-B19a-repeat.json" "$output/G03-B17/summary.json" \
|
||||
"$output/G05-cache-model.json" "$output/G05-benchmark.json" \
|
||||
"$output/GATE-SUMMARY.json" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
values = [json.loads(Path(path).read_text(encoding="ascii")) for path in sys.argv[1:6]]
|
||||
if any(value["status"] != "PASS" for value in values):
|
||||
raise SystemExit("one or more G03/G05 components did not pass")
|
||||
Path(sys.argv[6]).write_text(json.dumps({
|
||||
"b19a": "AUTHORIZED",
|
||||
"g03": "GO",
|
||||
"g05": "GO",
|
||||
"source_modified": False,
|
||||
"status": "GO",
|
||||
}, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
PY
|
||||
|
||||
status=GO
|
||||
reason='G03 and G05 both reached GO with valid multi-block oracle and symmetric samples'
|
||||
write_result
|
||||
printf '%s\n' 'P15-022 G03/G05 GO; B19a source is authorized'
|
||||
exit 0
|
||||
@@ -0,0 +1,180 @@
|
||||
{
|
||||
"architectures": [
|
||||
{
|
||||
"compiler_backend": "aarch64",
|
||||
"emulator_tool": "qemu-system-aarch64",
|
||||
"expected_uname_m": "arm64",
|
||||
"expected_uname_p": "aarch64",
|
||||
"header_aliases": {},
|
||||
"id": "arm64",
|
||||
"kernel_cflags": [
|
||||
"-mgeneral-regs-only",
|
||||
"-ffixed-x18",
|
||||
"-mbranch-protection=standard",
|
||||
"-mno-outline-atomics"
|
||||
],
|
||||
"machine_arch": "aarch64",
|
||||
"machine_header_dir": "arm64",
|
||||
"native_environment": null,
|
||||
"param_h_sha256": "234e3066dbc2f1cde6d6f41a73c3fcac7b6e2af632208e950a721b02ca77ebbb",
|
||||
"pointer_bits": 64,
|
||||
"target_triple": "aarch64-unknown-freebsd15.0"
|
||||
},
|
||||
{
|
||||
"compiler_backend": "riscv64",
|
||||
"emulator_tool": "qemu-system-riscv64",
|
||||
"expected_uname_m": "riscv",
|
||||
"expected_uname_p": "riscv64",
|
||||
"header_aliases": {},
|
||||
"id": "riscv64",
|
||||
"kernel_cflags": [
|
||||
"-march=rv64imafdch_zifencei",
|
||||
"-mabi=lp64",
|
||||
"-mcmodel=medium",
|
||||
"-mno-relax"
|
||||
],
|
||||
"machine_arch": "riscv64",
|
||||
"machine_header_dir": "riscv",
|
||||
"native_environment": null,
|
||||
"param_h_sha256": "b6596e77b0169cd6fe1a360c6160bead2f60f7145c52f42740ef7bc8691200ce",
|
||||
"pointer_bits": 64,
|
||||
"target_triple": "riscv64-unknown-freebsd15.0"
|
||||
},
|
||||
{
|
||||
"compiler_backend": "x86",
|
||||
"emulator_tool": "qemu-system-i386",
|
||||
"expected_uname_m": "i386",
|
||||
"expected_uname_p": "i386",
|
||||
"header_aliases": {
|
||||
"x86": "x86/include"
|
||||
},
|
||||
"id": "i386",
|
||||
"kernel_cflags": [
|
||||
"-mno-aes",
|
||||
"-mno-avx",
|
||||
"-mno-mmx",
|
||||
"-mno-sse",
|
||||
"-msoft-float"
|
||||
],
|
||||
"machine_arch": "i386",
|
||||
"machine_header_dir": "i386",
|
||||
"native_environment": null,
|
||||
"param_h_sha256": "46a648ac4fd5ee4db0f104d61e4916caf8c95b465d44166faf58f1574b03cc5b",
|
||||
"pointer_bits": 32,
|
||||
"target_triple": "i386-unknown-freebsd15.0"
|
||||
}
|
||||
],
|
||||
"batch": "B36",
|
||||
"candidate": "P15-027",
|
||||
"cross_build_contract": {
|
||||
"configurations": [
|
||||
0,
|
||||
1
|
||||
],
|
||||
"module_sources": [
|
||||
"super.c",
|
||||
"inode.c",
|
||||
"data.c",
|
||||
"namei.c",
|
||||
"dir.c",
|
||||
"xattr.c",
|
||||
"erofs_vnops.c",
|
||||
"decompressor.c",
|
||||
"zmap.c",
|
||||
"zdata.c",
|
||||
"decompressor_lz4.c",
|
||||
"decompressor_lzma.c",
|
||||
"decompressor_deflate.c",
|
||||
"decompressor_zstd.c"
|
||||
],
|
||||
"required_outputs": [
|
||||
"WITH_ZSTDIO=0 KMOD size and nm -u",
|
||||
"WITH_ZSTDIO=1 KMOD size and nm -u",
|
||||
"ondisk layout static assertions",
|
||||
"unaligned le16dec/le32dec/le64dec target assembly",
|
||||
"little-endian target assertion"
|
||||
],
|
||||
"unaligned_endian_probe_sha256": "b05fc54381177c3cbcd9bd8b2e9f56ba5a986b31fe8930eeb7855fa76620905c",
|
||||
"unaligned_endian_probe_source": "#include <stdint.h>\n#include <sys/endian.h>\n\n#if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__\n#error selected P15-027 targets must be little endian\n#endif\n\nuint64_t\np15_027_unaligned_ledec(const unsigned char *bytes)\n{\n return ((uint64_t)le16dec(bytes + 1) ^\n (uint64_t)le32dec(bytes + 1) ^ le64dec(bytes + 1));\n}\n"
|
||||
},
|
||||
"freebsd_source": {
|
||||
"git_prefix": "build/freebsd-src/",
|
||||
"git_tree_path": "freebsd-src",
|
||||
"head": "106727738dcfb6c001b46f25363b91cece970085",
|
||||
"sha256": {
|
||||
"sys/arm64/include/param.h": "234e3066dbc2f1cde6d6f41a73c3fcac7b6e2af632208e950a721b02ca77ebbb",
|
||||
"sys/conf/Makefile.arm64": "facbe05d9154ba68c39296f3cb9d2dd78da68e7c51d88e9008284a04c377cf50",
|
||||
"sys/conf/Makefile.i386": "15c7164ef3641ffdfba5152d039cca7349859e37c782283e6d158582393df517",
|
||||
"sys/conf/Makefile.riscv": "0f46b4c62d5c44c74d309cfe51786c48e3aadbcedb33c968e1a9e785c64a97c8",
|
||||
"sys/conf/kern.mk": "04126b5fd5ab8d78f1bc778e59b89a5e5daf67f3dcf5ae460cc14d1958b830fe",
|
||||
"sys/i386/include/param.h": "46a648ac4fd5ee4db0f104d61e4916caf8c95b465d44166faf58f1574b03cc5b",
|
||||
"sys/kern/vnode_if.src": "5de87ff115f543fd89f762c3d356b6799f30757f8a34fd840370ce7fff53b90b",
|
||||
"sys/riscv/include/param.h": "b6596e77b0169cd6fe1a360c6160bead2f60f7145c52f42740ef7bc8691200ce",
|
||||
"sys/sys/endian.h": "3d870d499089ac84bb49debebbb75b406a5f4df6f05f1582faebc5cf0b7b8806",
|
||||
"sys/tools/vnode_if.awk": "a834e22233718f13dc2a4a53177b1439581552145fd24be85c038e7ff2a24441"
|
||||
},
|
||||
"source_tree_oid": "eb10834ee8b13558a084d3c624f5691152760a11",
|
||||
"sys_tree_oid": "c4e8dddaa56277dd6609369152081d6bed80caff"
|
||||
},
|
||||
"gate": "G07",
|
||||
"native_runtime_contract": {
|
||||
"required_identity": [
|
||||
"FreeBSD uname -s",
|
||||
"architecture-specific uname -m and uname -p",
|
||||
"frozen EROFS BASE",
|
||||
"FreeBSD source HEAD",
|
||||
"temporary patch SHA-256",
|
||||
"exact cross-built module SHA-256 values"
|
||||
],
|
||||
"required_operations": [
|
||||
"zstdio0-kld-load",
|
||||
"zstdio0-plain-read",
|
||||
"zstdio0-lz4-read",
|
||||
"zstdio0-lzma-read",
|
||||
"zstdio0-zstd-read",
|
||||
"zstdio0-kld-unload",
|
||||
"zstdio1-kld-load",
|
||||
"zstdio1-plain-read",
|
||||
"zstdio1-lz4-read",
|
||||
"zstdio1-lzma-read",
|
||||
"zstdio1-zstd-read",
|
||||
"zstdio1-kld-unload"
|
||||
],
|
||||
"zstd_rules": [
|
||||
"running kernel options ZSTDIO capability item is PASS and true",
|
||||
"WITH_ZSTDIO=0 Zstd read is PASS only for exact EOPNOTSUPP",
|
||||
"WITH_ZSTDIO=1 Zstd read returns the expected content hash"
|
||||
]
|
||||
},
|
||||
"prohibited_paths": [
|
||||
"introduction.md"
|
||||
],
|
||||
"repo_source_tree_oid": "21bbe769bf75cd177116674fedafe12396817171",
|
||||
"required_base": "4683579dabc57b0c9aaf5762e13ecc2a0ec3f8f9",
|
||||
"schema": 1,
|
||||
"source_sha256": {
|
||||
"planning/pre15/20-final-candidate-ledger.md": "8333131b0e0951ef90bec00587a41f25841a156e5a0f59eaf2264c0004a48140",
|
||||
"planning/pre15/30-stage0-gates.md": "34708bacf4a0668dfd834b3639900ca23b28b9833a387ac6e2c0399c9316cdb8",
|
||||
"planning/pre15/40-execution-batches.md": "1d1ffdf8fec799aeec063bb1dc4a0fda9703300d414eabef8905522b2c18db20",
|
||||
"planning/pre15/50-feature-test-matrix.md": "ce39b44ef3989f4fcdbcd8eb677193ef53b630080c74d8539664fb2be3f38408",
|
||||
"planning/pre15/60-smoke-build-matrix.md": "2f7b4861b004bad82669042959ecd43a6839d4d8b241337af5b85485b02d5c99",
|
||||
"planning/pre15/90-honesty-and-evidence.md": "69f96908f5ab436a2f5359548e4e6f5dc2942fcf00148d999d513558f1c59c2d",
|
||||
"repo-pre-15/src/Makefile": "b722f7ec658e0ada0640c80a7b527921bd96cdf0e93e9623cece29766f2fc9c8",
|
||||
"repo-pre-15/src/erofs_fs.h": "d70c5920c10164a6cc8ade3c32077bffa604d62aa6eb7932d098dcb3e99ea980",
|
||||
"repo-pre-15/src/internal.h": "3fe8d6ca819f0a1c472cc4b173da80027f957b7f7c25e51ef7fd85b60480c98a",
|
||||
"repo-pre-15/tests/pre15/probes/ondisk_layout.c": "8fa700b5076d5f427323a7a98ce1195f5117cd63f9bc6be6b052c2535be9241d"
|
||||
},
|
||||
"temporary_makefile_relaxation": {
|
||||
"patched_makefile_sha256": "05159677a510b1348f8fd0f7a69db02bea00437631c54dc10337e480e39ec467",
|
||||
"path": "repo-pre-15/src/Makefile",
|
||||
"sha256": "30da7ce74a6d00a002c9dc9a388c36e206dd2be7d10f7f58d1ca13b79822c76f",
|
||||
"unified_diff": "--- a/repo-pre-15/src/Makefile\n+++ b/repo-pre-15/src/Makefile\n@@ -1,9 +1,5 @@\n KMOD= erofs\n \n-.if ${MACHINE_ARCH} != \"amd64\"\n-.error erofs supports only MACHINE_ARCH=amd64\n-.endif\n-\n WITH_ZSTDIO?= 0\n .if empty(WITH_ZSTDIO:M0) && empty(WITH_ZSTDIO:M1)\n .error WITH_ZSTDIO must be 0 or 1\n"
|
||||
},
|
||||
"toolchain": {
|
||||
"compiler": {
|
||||
"realpath": "/usr/lib/llvm-19/bin/clang",
|
||||
"sha256": "0a3c55936ac43954fbe4914e6e73b577ca9e35e75d89ef13d3e28c187f41ca1a",
|
||||
"version": "Debian clang version 19.1.7 (3+b1)"
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+845
@@ -0,0 +1,845 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
umask 022
|
||||
|
||||
gate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||
root=$(CDPATH= cd -- "$gate_dir/../../../.." && pwd -P)
|
||||
input=$gate_dir/P15-027-input.json
|
||||
freebsd_src=${FREEBSD_SRC:-/work/build/freebsd-src}
|
||||
base=
|
||||
output=
|
||||
deadline=120
|
||||
|
||||
while test "$#" -gt 0; do
|
||||
case "$1" in
|
||||
--base)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 20; }
|
||||
base=$2
|
||||
shift 2
|
||||
;;
|
||||
--freebsd-src)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--freebsd-src requires a directory' >&2; exit 20; }
|
||||
freebsd_src=$2
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 20; }
|
||||
output=$2
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
printf 'unknown argument: %s\n' "$1" >&2
|
||||
exit 20
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 20; }
|
||||
test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 20; }
|
||||
test -f "$input" || { printf 'missing input: %s\n' "$input" >&2; exit 20; }
|
||||
for tool in date git mktemp patch python3 sha256sum timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
printf 'missing required host tool: %s\n' "$tool" >&2
|
||||
exit 21
|
||||
}
|
||||
done
|
||||
case "$output" in
|
||||
/*) ;;
|
||||
*) output=$PWD/$output ;;
|
||||
esac
|
||||
case "$freebsd_src" in
|
||||
/*) ;;
|
||||
*) freebsd_src=$PWD/$freebsd_src ;;
|
||||
esac
|
||||
test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 20; }
|
||||
mkdir -p "$output"
|
||||
work=$(mktemp -d "${TMPDIR:-/tmp}/P15-027.XXXXXX")
|
||||
|
||||
cleanup_trap()
|
||||
{
|
||||
rm -rf -- "$work"
|
||||
}
|
||||
trap cleanup_trap EXIT HUP INT TERM
|
||||
|
||||
python3 -B - "$output/command-argv.json" "$0" --base "$base" \
|
||||
--freebsd-src "$freebsd_src" --output "$output" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
Path(sys.argv[1]).write_text(
|
||||
json.dumps(sys.argv[2:], ensure_ascii=True, separators=(",", ":")) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
PY
|
||||
python3 -B - "$output/ownership.json" "$work" "$output" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
Path(sys.argv[1]).write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"owned_temporary_paths": [sys.argv[2]],
|
||||
"persistent_output": sys.argv[3],
|
||||
"owned_processes": [],
|
||||
"owned_ports": [],
|
||||
"qemu": "NOT_RUN",
|
||||
"protected_resources_addressed": False,
|
||||
},
|
||||
ensure_ascii=True,
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
PY
|
||||
|
||||
set +e
|
||||
timeout -k 5 "$deadline" python3 -B - \
|
||||
"$root" "$input" "$base" "$freebsd_src" "$output" "$work" "$deadline" <<'PY' \
|
||||
>"$output/stdout.log" 2>"$output/stderr.log"
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(sys.argv[1])
|
||||
INPUT = Path(sys.argv[2])
|
||||
REQUESTED_BASE = sys.argv[3]
|
||||
FREEBSD_SRC = Path(sys.argv[4])
|
||||
OUTPUT = Path(sys.argv[5])
|
||||
WORK = Path(sys.argv[6])
|
||||
DEADLINE = int(sys.argv[7])
|
||||
SPEC = json.loads(INPUT.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
class GateFailure(RuntimeError):
|
||||
def __init__(self, status: str, reason: str):
|
||||
super().__init__(reason)
|
||||
self.status = status
|
||||
self.reason = reason
|
||||
|
||||
|
||||
def sha256_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def sha256_path(path: Path) -> str:
|
||||
return sha256_bytes(path.read_bytes())
|
||||
|
||||
|
||||
def write_json(path: Path, value: Any) -> None:
|
||||
path.write_text(
|
||||
json.dumps(value, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
|
||||
|
||||
def run(
|
||||
argv: list[str],
|
||||
*,
|
||||
cwd: Path | None = None,
|
||||
input_bytes: bytes | None = None,
|
||||
allowed: set[int] | None = None,
|
||||
timeout_seconds: int = 30,
|
||||
) -> subprocess.CompletedProcess[bytes]:
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
argv,
|
||||
cwd=cwd,
|
||||
check=False,
|
||||
input=input_bytes,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=timeout_seconds,
|
||||
)
|
||||
except subprocess.TimeoutExpired as error:
|
||||
raise GateFailure(
|
||||
"INFRA_BLOCKED", f"command timed out: {' '.join(argv)}"
|
||||
) from error
|
||||
accepted = {0} if allowed is None else allowed
|
||||
if completed.returncode not in accepted:
|
||||
detail = completed.stderr.decode("utf-8", errors="replace").strip()
|
||||
raise GateFailure(
|
||||
"RUNNER_FAIL",
|
||||
f"command failed ({completed.returncode}): {' '.join(argv)}: {detail}",
|
||||
)
|
||||
return completed
|
||||
|
||||
|
||||
def git_text(repository: Path, *args: str) -> str:
|
||||
return run(["git", "-C", str(repository), *args]).stdout.decode(
|
||||
"utf-8", errors="strict"
|
||||
).strip()
|
||||
|
||||
|
||||
def source_at(commit: str, relative: str) -> bytes:
|
||||
return run(
|
||||
["git", "-C", str(ROOT), "show", f"{commit}:{relative}"]
|
||||
).stdout
|
||||
|
||||
|
||||
def safe_relative_path(value: str) -> Path:
|
||||
path = Path(value)
|
||||
if path.is_absolute() or ".." in path.parts:
|
||||
raise GateFailure("RUNNER_FAIL", f"unsafe declared path: {value}")
|
||||
if path.name == "introduction.md":
|
||||
raise GateFailure("RUNNER_FAIL", "prohibited path declaration")
|
||||
return path
|
||||
|
||||
|
||||
def validate_hash(value: Any, label: str) -> str:
|
||||
if not isinstance(value, str) or re.fullmatch(r"[0-9a-f]{64}", value) is None:
|
||||
raise GateFailure("RUNNER_FAIL", f"invalid SHA-256 for {label}")
|
||||
return value
|
||||
|
||||
|
||||
def validate_runtime_item(item: Any, item_id: str) -> None:
|
||||
if not isinstance(item, dict):
|
||||
raise GateFailure("RUNNER_FAIL", f"runtime item is not an object: {item_id}")
|
||||
expected = {
|
||||
"status": "PASS",
|
||||
"exit_code": 0,
|
||||
"target_marker": "reached",
|
||||
"cleanup": "PASS",
|
||||
}
|
||||
for key, value in expected.items():
|
||||
if item.get(key) != value:
|
||||
raise GateFailure("RUNNER_FAIL", f"runtime item {item_id} has invalid {key}")
|
||||
argv = item.get("command_argv")
|
||||
if not isinstance(argv, list) or not argv or not all(isinstance(v, str) for v in argv):
|
||||
raise GateFailure("RUNNER_FAIL", f"runtime item {item_id} lacks exact argv")
|
||||
if not isinstance(item.get("deadline_seconds"), int) or item["deadline_seconds"] <= 0:
|
||||
raise GateFailure("RUNNER_FAIL", f"runtime item {item_id} lacks deadline")
|
||||
for stream in ("stdout", "stderr"):
|
||||
relative = safe_relative_path(item.get(stream, ""))
|
||||
evidence_path = OUTPUT / relative
|
||||
if not evidence_path.is_file():
|
||||
raise GateFailure("RUNNER_FAIL", f"runtime item {item_id} lacks {stream}")
|
||||
expected_hash = validate_hash(item.get(f"{stream}_sha256"), f"{item_id} {stream}")
|
||||
if sha256_path(evidence_path) != expected_hash:
|
||||
raise GateFailure("RUNNER_FAIL", f"runtime item {item_id} {stream} hash mismatch")
|
||||
|
||||
|
||||
def validate_runtime_record(
|
||||
path: Path,
|
||||
architecture: dict[str, Any],
|
||||
modules: dict[str, dict[str, Any]],
|
||||
resolved_base: str,
|
||||
patch_sha256: str,
|
||||
) -> dict[str, Any]:
|
||||
record = json.loads(path.read_text(encoding="utf-8"))
|
||||
expected_identity = {
|
||||
"schema": 1,
|
||||
"architecture": architecture["id"],
|
||||
"uname_s": "FreeBSD",
|
||||
"uname_m": architecture["expected_uname_m"],
|
||||
"uname_p": architecture["expected_uname_p"],
|
||||
"dut_base": resolved_base,
|
||||
"temporary_patch_sha256": patch_sha256,
|
||||
"freebsd_source_head": SPEC["freebsd_source"]["head"],
|
||||
}
|
||||
for key, value in expected_identity.items():
|
||||
if record.get(key) != value:
|
||||
raise GateFailure("RUNNER_FAIL", f"native record identity mismatch: {key}")
|
||||
if record.get("status") not in {"PASS", "DUT_FAIL"}:
|
||||
raise GateFailure("RUNNER_FAIL", "native record status is not authoritative")
|
||||
if record.get("cleanup") != "PASS" or record.get("target_marker") != "reached":
|
||||
raise GateFailure("RUNNER_FAIL", "native record did not reach target with cleanup")
|
||||
if record.get("module_sha256") != {
|
||||
name: value["sha256"] for name, value in modules.items()
|
||||
}:
|
||||
raise GateFailure("RUNNER_FAIL", "native module hashes differ from cross artifacts")
|
||||
fixtures = record.get("fixture_sha256")
|
||||
if not isinstance(fixtures, list) or len(fixtures) < 4:
|
||||
raise GateFailure("RUNNER_FAIL", "native record lacks four fixture hashes")
|
||||
for index, value in enumerate(fixtures):
|
||||
validate_hash(value, f"native fixture {index}")
|
||||
capability = record.get("kernel_zstdio")
|
||||
validate_runtime_item(capability, "kernel-zstdio-capability")
|
||||
if capability.get("options_zstdio") is not True:
|
||||
raise GateFailure("RUNNER_FAIL", "native ZSTDIO capability is not enabled")
|
||||
operations = record.get("operations")
|
||||
if not isinstance(operations, dict):
|
||||
raise GateFailure("RUNNER_FAIL", "native operations are absent")
|
||||
required = SPEC["native_runtime_contract"]["required_operations"]
|
||||
if set(operations) != set(required):
|
||||
raise GateFailure("RUNNER_FAIL", "native operation set is not exact")
|
||||
for item_id in required:
|
||||
validate_runtime_item(operations[item_id], item_id)
|
||||
disabled = operations["zstdio0-zstd-read"]
|
||||
if disabled.get("errno") != "EOPNOTSUPP":
|
||||
raise GateFailure("RUNNER_FAIL", "disabled native Zstd errno is not EOPNOTSUPP")
|
||||
return record
|
||||
|
||||
|
||||
def extract_repo_source(resolved_base: str, destination: Path) -> None:
|
||||
archive = run(
|
||||
[
|
||||
"git", "-C", str(ROOT), "archive", "--format=tar", resolved_base,
|
||||
"repo-pre-15/src", "repo-pre-15/tests/pre15/probes/ondisk_layout.c",
|
||||
],
|
||||
timeout_seconds=60,
|
||||
).stdout
|
||||
destination.mkdir(parents=True)
|
||||
archive_path = WORK / "repo-source.tar"
|
||||
archive_path.write_bytes(archive)
|
||||
with tarfile.open(archive_path, mode="r:") as tar:
|
||||
tar.extractall(destination, filter="data")
|
||||
|
||||
|
||||
def compile_architecture(
|
||||
architecture: dict[str, Any],
|
||||
resolved_base: str,
|
||||
patch_bytes: bytes,
|
||||
compiler: Path,
|
||||
) -> dict[str, Any]:
|
||||
arch_id = architecture["id"]
|
||||
arch_root = WORK / "cross" / arch_id
|
||||
source_root = arch_root / "source"
|
||||
extract_repo_source(resolved_base, source_root)
|
||||
run(["patch", "-p1"], cwd=source_root, input_bytes=patch_bytes)
|
||||
src = source_root / "repo-pre-15/src"
|
||||
sys_root = FREEBSD_SRC / "sys"
|
||||
resource_include = Path(
|
||||
run([str(compiler), "-print-resource-dir"]).stdout.decode("ascii").strip()
|
||||
) / "include"
|
||||
common_flags = [
|
||||
"-O2", "-pipe", "-fno-common", "-fno-strict-aliasing", "-D_KERNEL",
|
||||
"-DKLD_MODULE", "-nostdinc", "-ffreestanding", "-fwrapv",
|
||||
"-fno-asynchronous-unwind-tables", "-fno-omit-frame-pointer",
|
||||
"-fstack-protector", "-Wall", "-Wstrict-prototypes",
|
||||
"-Wmissing-prototypes", "-Wpointer-arith", "-Wcast-qual", "-Wundef",
|
||||
"-Wno-pointer-sign", "-Wmissing-include-dirs", "-Wno-unknown-pragmas",
|
||||
"-Wno-address-of-packed-member", "-Wno-format-zero-length", "-std=gnu17",
|
||||
"-D__printf__=__freebsd_kprintf__", f"--target={architecture['target_triple']}",
|
||||
*architecture["kernel_cflags"],
|
||||
]
|
||||
modules: dict[str, dict[str, Any]] = {}
|
||||
for zstdio in (0, 1):
|
||||
build = arch_root / f"zstdio{zstdio}"
|
||||
build.mkdir(parents=True)
|
||||
(build / "opt_global.h").write_bytes(b"")
|
||||
(build / "machine").symlink_to(sys_root / architecture["machine_header_dir"] / "include")
|
||||
for alias, relative in architecture["header_aliases"].items():
|
||||
(build / alias).symlink_to(sys_root / relative)
|
||||
for mode in ("-p", "-q", "-h"):
|
||||
generated = run(
|
||||
["awk", "-f", str(sys_root / "tools/vnode_if.awk"),
|
||||
str(sys_root / "kern/vnode_if.src"), mode],
|
||||
cwd=build,
|
||||
).stdout
|
||||
suffix = {"-p": "vnode_if_newproto.h", "-q": "vnode_if_typedef.h", "-h": "vnode_if.h"}[mode]
|
||||
(build / suffix).write_bytes(generated)
|
||||
include_flags = [
|
||||
"-include", str(build / "opt_global.h"), "-I", str(build),
|
||||
"-I", str(sys_root), "-I", str(sys_root / "contrib/ck/include"),
|
||||
]
|
||||
objects = []
|
||||
stdout_parts = []
|
||||
stderr_parts = []
|
||||
for source_name in SPEC["cross_build_contract"]["module_sources"]:
|
||||
obj = build / f"{Path(source_name).stem}.o"
|
||||
argv = [str(compiler), *common_flags, *include_flags]
|
||||
if source_name == "decompressor_zstd.c":
|
||||
argv.extend(["-I", str(sys_root / "contrib/zstd/lib/freebsd")])
|
||||
if zstdio == 1:
|
||||
argv.append("-DZSTDIO")
|
||||
argv.extend(["-c", str(src / source_name), "-o", str(obj)])
|
||||
completed = run(argv, cwd=build, timeout_seconds=60)
|
||||
stdout_parts.append(completed.stdout)
|
||||
stderr_parts.append(completed.stderr)
|
||||
objects.append(obj)
|
||||
module = build / "erofs.ko"
|
||||
linked = run(
|
||||
[str(compiler), f"--target={architecture['target_triple']}",
|
||||
"-r", "-nostdlib", *map(str, objects), "-o", str(module)],
|
||||
cwd=build,
|
||||
timeout_seconds=60,
|
||||
)
|
||||
stdout_parts.append(linked.stdout)
|
||||
stderr_parts.append(linked.stderr)
|
||||
nm_output = run(["nm", "-u", str(module)]).stdout
|
||||
(build / "build.stdout").write_bytes(b"".join(stdout_parts))
|
||||
(build / "build.stderr").write_bytes(b"".join(stderr_parts))
|
||||
(build / "nm-u.txt").write_bytes(nm_output)
|
||||
modules[f"zstdio{zstdio}"] = {
|
||||
"path": str(module),
|
||||
"sha256": sha256_path(module),
|
||||
"size_bytes": module.stat().st_size,
|
||||
"nm_u_sha256": sha256_bytes(nm_output),
|
||||
}
|
||||
|
||||
probe_root = arch_root / "probes"
|
||||
probe_root.mkdir()
|
||||
include_root = probe_root / "include"
|
||||
include_root.mkdir()
|
||||
(include_root / "machine").symlink_to(
|
||||
sys_root / architecture["machine_header_dir"] / "include"
|
||||
)
|
||||
for alias, relative in architecture["header_aliases"].items():
|
||||
(include_root / alias).symlink_to(sys_root / relative)
|
||||
probe_flags = [
|
||||
f"--target={architecture['target_triple']}", "-std=gnu17", "-nostdinc",
|
||||
"-isystem", str(resource_include), "-isystem", str(FREEBSD_SRC / "include"),
|
||||
"-isystem", str(include_root), "-isystem", str(sys_root),
|
||||
]
|
||||
layout = source_root / "repo-pre-15/tests/pre15/probes/ondisk_layout.c"
|
||||
layout_result = run(
|
||||
[str(compiler), *probe_flags, "-Wall", "-Wextra", "-Werror",
|
||||
"-fsyntax-only", "-Xclang", "-fdump-record-layouts", str(layout)],
|
||||
timeout_seconds=60,
|
||||
)
|
||||
(probe_root / "layout.stdout").write_bytes(layout_result.stdout)
|
||||
(probe_root / "layout.stderr").write_bytes(layout_result.stderr)
|
||||
unaligned = probe_root / "unaligned-endian.c"
|
||||
unaligned.write_text(
|
||||
SPEC["cross_build_contract"]["unaligned_endian_probe_source"],
|
||||
encoding="ascii",
|
||||
)
|
||||
assembly = probe_root / "unaligned-endian.s"
|
||||
endian_result = run(
|
||||
[str(compiler), *probe_flags, *architecture["kernel_cflags"],
|
||||
"-O2", "-S", str(unaligned), "-o", str(assembly)],
|
||||
timeout_seconds=60,
|
||||
)
|
||||
(probe_root / "unaligned-endian.stdout").write_bytes(endian_result.stdout)
|
||||
(probe_root / "unaligned-endian.stderr").write_bytes(endian_result.stderr)
|
||||
if assembly.stat().st_size == 0:
|
||||
raise GateFailure("RUNNER_FAIL", f"empty unaligned/endian probe for {arch_id}")
|
||||
return {
|
||||
"status": "PASS",
|
||||
"temporary_source": str(source_root),
|
||||
"modules": modules,
|
||||
"layout_probe_sha256": sha256_path(layout),
|
||||
"layout_output_sha256": sha256_bytes(layout_result.stdout),
|
||||
"unaligned_endian_probe_sha256": sha256_path(unaligned),
|
||||
"unaligned_endian_assembly_sha256": sha256_path(assembly),
|
||||
}
|
||||
|
||||
|
||||
def render_argv(values: list[str], substitutions: dict[str, str]) -> list[str]:
|
||||
rendered = []
|
||||
for value in values:
|
||||
for key, replacement in substitutions.items():
|
||||
value = value.replace("{" + key + "}", replacement)
|
||||
rendered.append(value)
|
||||
return rendered
|
||||
|
||||
|
||||
def run_native(
|
||||
architecture: dict[str, Any],
|
||||
declaration: dict[str, Any],
|
||||
modules: dict[str, dict[str, Any]],
|
||||
resolved_base: str,
|
||||
patch_sha256: str,
|
||||
) -> dict[str, Any]:
|
||||
runner_relative = safe_relative_path(declaration.get("runner_path", ""))
|
||||
runner = ROOT / runner_relative
|
||||
if not runner.is_file():
|
||||
raise GateFailure("INFRA_BLOCKED", f"declared native runner is absent: {runner_relative}")
|
||||
if sha256_path(runner) != validate_hash(
|
||||
declaration.get("runner_sha256"), f"{architecture['id']} runner"
|
||||
):
|
||||
raise GateFailure("INFRA_BLOCKED", "declared native runner identity changed")
|
||||
native_output = OUTPUT / "native" / architecture["id"]
|
||||
native_output.mkdir(parents=True)
|
||||
substitutions = {
|
||||
"runner": str(runner),
|
||||
"architecture": architecture["id"],
|
||||
"module_zstdio0": modules["zstdio0"]["path"],
|
||||
"module_zstdio1": modules["zstdio1"]["path"],
|
||||
"output": str(native_output),
|
||||
}
|
||||
preflight_argv = render_argv(declaration["preflight_argv"], substitutions)
|
||||
preflight = run(
|
||||
preflight_argv,
|
||||
cwd=ROOT,
|
||||
timeout_seconds=declaration["preflight_deadline_seconds"],
|
||||
)
|
||||
(native_output / "preflight.runner.stdout").write_bytes(preflight.stdout)
|
||||
(native_output / "preflight.runner.stderr").write_bytes(preflight.stderr)
|
||||
preflight_record = json.loads(
|
||||
(native_output / "preflight.json").read_text(encoding="utf-8")
|
||||
)
|
||||
for key, value in {
|
||||
"status": "PASS", "exit_code": 0, "target_marker": "reached",
|
||||
"cleanup": "PASS", "uname_s": "FreeBSD",
|
||||
"uname_m": architecture["expected_uname_m"],
|
||||
"uname_p": architecture["expected_uname_p"],
|
||||
}.items():
|
||||
if preflight_record.get(key) != value:
|
||||
raise GateFailure("INFRA_BLOCKED", f"native preflight mismatch: {key}")
|
||||
runtime_argv = render_argv(declaration["runtime_argv"], substitutions)
|
||||
runtime = run(
|
||||
runtime_argv,
|
||||
cwd=ROOT,
|
||||
allowed={0, 10},
|
||||
timeout_seconds=declaration["runtime_deadline_seconds"],
|
||||
)
|
||||
(native_output / "runner.stdout").write_bytes(runtime.stdout)
|
||||
(native_output / "runner.stderr").write_bytes(runtime.stderr)
|
||||
return validate_runtime_record(
|
||||
native_output / "runtime-result.json",
|
||||
architecture,
|
||||
modules,
|
||||
resolved_base,
|
||||
patch_sha256,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if SPEC.get("schema") != 1 or SPEC.get("candidate") != "P15-027":
|
||||
raise GateFailure("RUNNER_FAIL", "invalid P15-027 input identity")
|
||||
if SPEC.get("gate") != "G07" or SPEC.get("batch") != "B36":
|
||||
raise GateFailure("RUNNER_FAIL", "invalid G07/B36 input identity")
|
||||
if SPEC.get("prohibited_paths") != ["introduction.md"]:
|
||||
raise GateFailure("RUNNER_FAIL", "prohibited-path contract changed")
|
||||
resolved = git_text(ROOT, "rev-parse", f"{REQUESTED_BASE}^{{commit}}")
|
||||
if resolved != SPEC["required_base"]:
|
||||
raise GateFailure(
|
||||
"INFRA_BLOCKED", f"P15-027 must replay {SPEC['required_base']}, got {resolved}"
|
||||
)
|
||||
source_tree = git_text(ROOT, "rev-parse", f"{resolved}:repo-pre-15/src")
|
||||
if source_tree != SPEC["repo_source_tree_oid"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen EROFS source tree changed")
|
||||
source_hashes = {
|
||||
path: sha256_bytes(source_at(resolved, path))
|
||||
for path in SPEC["source_sha256"]
|
||||
}
|
||||
if source_hashes != SPEC["source_sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen gate source identity changed")
|
||||
write_json(OUTPUT / "source-sha256.json", source_hashes)
|
||||
|
||||
addendum_paths = (
|
||||
"repo-pre-15/tests/pre15/gates/P15-027.sh",
|
||||
"repo-pre-15/tests/pre15/gates/P15-027-input.json",
|
||||
"repo-pre-15/docs/pre15-stage0/P15-027.md",
|
||||
)
|
||||
addendum_hashes = {
|
||||
path: sha256_path(ROOT / path) for path in addendum_paths
|
||||
}
|
||||
write_json(OUTPUT / "gate-addendum-sha256.json", addendum_hashes)
|
||||
if sha256_path(ROOT / "repo-pre-15/src/Makefile") != SPEC["source_sha256"]["repo-pre-15/src/Makefile"]:
|
||||
raise GateFailure("RUNNER_FAIL", "production Makefile differs from frozen BASE")
|
||||
|
||||
patch_spec = SPEC["temporary_makefile_relaxation"]
|
||||
patch_bytes = patch_spec["unified_diff"].encode("ascii")
|
||||
if sha256_bytes(patch_bytes) != patch_spec["sha256"]:
|
||||
raise GateFailure("RUNNER_FAIL", "input-recorded temporary patch hash mismatch")
|
||||
patch_check = WORK / "patch-check"
|
||||
makefile_copy = patch_check / "repo-pre-15/src/Makefile"
|
||||
makefile_copy.parent.mkdir(parents=True)
|
||||
makefile_copy.write_bytes(source_at(resolved, patch_spec["path"]))
|
||||
patch_result = run(["patch", "-p1"], cwd=patch_check, input_bytes=patch_bytes)
|
||||
(OUTPUT / "makefile-relaxation.patch").write_bytes(patch_bytes)
|
||||
(OUTPUT / "patch.stdout").write_bytes(patch_result.stdout)
|
||||
(OUTPUT / "patch.stderr").write_bytes(patch_result.stderr)
|
||||
if sha256_path(makefile_copy) != patch_spec["patched_makefile_sha256"]:
|
||||
raise GateFailure("RUNNER_FAIL", "temporary Makefile relaxation result changed")
|
||||
patch_record = {
|
||||
"path": patch_spec["path"],
|
||||
"sha256": patch_spec["sha256"],
|
||||
"patched_makefile_sha256": sha256_path(makefile_copy),
|
||||
"production_makefile_changed": False,
|
||||
"temporary_apply": "PASS",
|
||||
}
|
||||
write_json(OUTPUT / "temporary-patch.json", patch_record)
|
||||
|
||||
if not FREEBSD_SRC.is_dir():
|
||||
raise GateFailure("INFRA_BLOCKED", f"FreeBSD source is absent: {FREEBSD_SRC}")
|
||||
freebsd = SPEC["freebsd_source"]
|
||||
freebsd_head = git_text(FREEBSD_SRC, "rev-parse", "HEAD")
|
||||
freebsd_prefix = git_text(FREEBSD_SRC, "rev-parse", "--show-prefix")
|
||||
if freebsd_head != freebsd["head"] or freebsd_prefix != freebsd["git_prefix"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "FreeBSD source identity changed")
|
||||
git_root = Path(git_text(FREEBSD_SRC, "rev-parse", "--show-toplevel"))
|
||||
freebsd_source_tree = git_text(
|
||||
git_root, "rev-parse", f"{freebsd_head}:{freebsd['git_tree_path']}"
|
||||
)
|
||||
freebsd_sys_tree = git_text(
|
||||
git_root, "rev-parse", f"{freebsd_head}:{freebsd['git_tree_path']}/sys"
|
||||
)
|
||||
if freebsd_source_tree != freebsd["source_tree_oid"] or freebsd_sys_tree != freebsd["sys_tree_oid"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "FreeBSD source tree object changed")
|
||||
freebsd_hashes = {
|
||||
path: sha256_path(FREEBSD_SRC / path) for path in freebsd["sha256"]
|
||||
}
|
||||
if freebsd_hashes != freebsd["sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "selected FreeBSD source hashes changed")
|
||||
|
||||
compiler_path = Path(shutil.which("clang") or "").resolve()
|
||||
if not compiler_path.is_file():
|
||||
raise GateFailure("INFRA_BLOCKED", "clang is unavailable")
|
||||
compiler = SPEC["toolchain"]["compiler"]
|
||||
compiler_version = run([str(compiler_path), "--version"]).stdout.decode(
|
||||
"utf-8", errors="strict"
|
||||
).splitlines()[0]
|
||||
if (
|
||||
str(compiler_path) != compiler["realpath"]
|
||||
or sha256_path(compiler_path) != compiler["sha256"]
|
||||
or compiler_version != compiler["version"]
|
||||
):
|
||||
raise GateFailure("INFRA_BLOCKED", "cross compiler identity changed")
|
||||
compiler_targets = run([str(compiler_path), "--print-targets"]).stdout.decode(
|
||||
"utf-8", errors="strict"
|
||||
)
|
||||
|
||||
architectures = SPEC["architectures"]
|
||||
if [item.get("id") for item in architectures] != ["arm64", "riscv64", "i386"]:
|
||||
raise GateFailure("RUNNER_FAIL", "architecture audit set is not exact")
|
||||
if sum(item.get("pointer_bits") == 32 for item in architectures) < 1:
|
||||
raise GateFailure("RUNNER_FAIL", "architecture audit lacks a 32-bit target")
|
||||
source_support = {}
|
||||
for architecture in architectures:
|
||||
arch_id = architecture["id"]
|
||||
header = FREEBSD_SRC / "sys" / architecture["machine_header_dir"] / "include/param.h"
|
||||
if not header.is_file():
|
||||
raise GateFailure("INFRA_BLOCKED", f"FreeBSD source lacks {arch_id}")
|
||||
if sha256_path(header) != architecture["param_h_sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", f"FreeBSD {arch_id} source identity changed")
|
||||
target_pattern = rf"^\s*{re.escape(architecture['compiler_backend'])}\s+-"
|
||||
if re.search(target_pattern, compiler_targets, re.MULTILINE) is None:
|
||||
raise GateFailure("INFRA_BLOCKED", f"clang lacks {arch_id} backend")
|
||||
source_support[arch_id] = {
|
||||
"status": "PASS",
|
||||
"freebsd_machine_headers": str(header),
|
||||
"param_h_sha256": sha256_path(header),
|
||||
"compiler_backend": architecture["compiler_backend"],
|
||||
"target_triple": architecture["target_triple"],
|
||||
"pointer_bits": architecture["pointer_bits"],
|
||||
}
|
||||
|
||||
probe_source = SPEC["cross_build_contract"]["unaligned_endian_probe_source"].encode("ascii")
|
||||
if sha256_bytes(probe_source) != SPEC["cross_build_contract"]["unaligned_endian_probe_sha256"]:
|
||||
raise GateFailure("RUNNER_FAIL", "input-recorded unaligned/endian probe changed")
|
||||
inventory = {
|
||||
"schema": 1,
|
||||
"host": {"system": platform.system(), "machine": platform.machine()},
|
||||
"local_host_is_qualifying_native_freebsd": False,
|
||||
"local_host_reason": "host is not FreeBSD and is not any candidate architecture",
|
||||
"declared_native_environments": {
|
||||
item["id"]: item["native_environment"] is not None for item in architectures
|
||||
},
|
||||
"emulator_tools": {
|
||||
item["id"]: shutil.which(item["emulator_tool"]) for item in architectures
|
||||
},
|
||||
"emulator_binary_is_native_runtime_evidence": False,
|
||||
"source_toolchain_support": source_support,
|
||||
"protected_resources_addressed": False,
|
||||
"inventory_rule": "only an explicit, validated same-architecture FreeBSD runner declaration can start cross work",
|
||||
}
|
||||
write_json(OUTPUT / "runtime-environment-inventory.json", inventory)
|
||||
write_json(
|
||||
OUTPUT / "freebsd-source-identity.json",
|
||||
{
|
||||
"path": str(FREEBSD_SRC),
|
||||
"git_root": str(git_root),
|
||||
"git_prefix": freebsd_prefix,
|
||||
"git_tree_path": freebsd["git_tree_path"],
|
||||
"head": freebsd_head,
|
||||
"source_tree_oid": freebsd_source_tree,
|
||||
"sys_tree_oid": freebsd_sys_tree,
|
||||
"sha256": freebsd_hashes,
|
||||
},
|
||||
)
|
||||
write_json(
|
||||
OUTPUT / "toolchain-identity.json",
|
||||
{
|
||||
"compiler_realpath": str(compiler_path),
|
||||
"compiler_sha256": sha256_path(compiler_path),
|
||||
"compiler_version": compiler_version,
|
||||
"print_targets_sha256": sha256_bytes(compiler_targets.encode("utf-8")),
|
||||
},
|
||||
)
|
||||
|
||||
results = []
|
||||
go_architectures = []
|
||||
for architecture in architectures:
|
||||
declaration = architecture["native_environment"]
|
||||
record: dict[str, Any] = {
|
||||
"architecture": architecture["id"],
|
||||
"machine_arch": architecture["machine_arch"],
|
||||
"pointer_bits": architecture["pointer_bits"],
|
||||
"source_toolchain": source_support[architecture["id"]],
|
||||
}
|
||||
if declaration is None:
|
||||
record.update(
|
||||
{
|
||||
"native_environment": {
|
||||
"status": "STOP",
|
||||
"reason": "no declared same-architecture FreeBSD runtime environment",
|
||||
},
|
||||
"cross_build_and_probes": {
|
||||
"status": "NOT_RUN",
|
||||
"reason": "cross-only evidence cannot authorize allowlisting",
|
||||
},
|
||||
"native_runtime": {
|
||||
"status": "NOT_RUN",
|
||||
"reason": "same-architecture FreeBSD runtime is absent",
|
||||
},
|
||||
"decision": "STOP",
|
||||
}
|
||||
)
|
||||
results.append(record)
|
||||
continue
|
||||
for key in (
|
||||
"runner_path", "runner_sha256", "preflight_argv", "runtime_argv",
|
||||
"preflight_deadline_seconds", "runtime_deadline_seconds",
|
||||
):
|
||||
if key not in declaration:
|
||||
raise GateFailure("RUNNER_FAIL", f"incomplete native declaration: {key}")
|
||||
cross = compile_architecture(
|
||||
architecture, resolved, patch_bytes, compiler_path
|
||||
)
|
||||
native = run_native(
|
||||
architecture,
|
||||
declaration,
|
||||
cross["modules"],
|
||||
resolved,
|
||||
patch_spec["sha256"],
|
||||
)
|
||||
native_pass = native["status"] == "PASS"
|
||||
record.update(
|
||||
{
|
||||
"native_environment": {"status": "PASS", "declaration": declaration},
|
||||
"cross_build_and_probes": cross,
|
||||
"native_runtime": native,
|
||||
"decision": "GO" if native_pass else "STOP",
|
||||
}
|
||||
)
|
||||
if native_pass:
|
||||
go_architectures.append(architecture["id"])
|
||||
results.append(record)
|
||||
|
||||
overall_status = "GO" if go_architectures else "STOP"
|
||||
result = {
|
||||
"schema": 1,
|
||||
"gate": "G07",
|
||||
"candidate": "P15-027",
|
||||
"batch": "B36",
|
||||
"status": overall_status,
|
||||
"reason": (
|
||||
"at least one architecture completed its own cross and native requirements"
|
||||
if go_architectures
|
||||
else "no architecture has a declared same-architecture FreeBSD runtime"
|
||||
),
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"resolved_base": resolved,
|
||||
"repo_source_tree_oid": source_tree,
|
||||
"deadline_seconds": DEADLINE,
|
||||
"expected_exit_code": 0 if go_architectures else 22,
|
||||
"architectures": results,
|
||||
"go_architectures": go_architectures,
|
||||
"allowlist_authorization": go_architectures,
|
||||
"false_go_guard": "each authorized architecture must independently have cross_build_and_probes=PASS and native_runtime=PASS",
|
||||
"b36": "AUTHORIZED" if go_architectures else "STOP-NO-SOURCE",
|
||||
"production_makefile_guard_preserved": True,
|
||||
"production_source_changed": False,
|
||||
"qemu": "NOT_RUN" if not go_architectures else "runner-defined",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"smoke_suite": "NOT_RUN",
|
||||
"protected_resources_addressed": False,
|
||||
}
|
||||
write_json(OUTPUT / "result.json", result)
|
||||
print(json.dumps(result, ensure_ascii=True, sort_keys=True))
|
||||
return 0 if go_architectures else 22
|
||||
|
||||
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except GateFailure as error:
|
||||
failure = {
|
||||
"schema": 1,
|
||||
"gate": "G07",
|
||||
"candidate": "P15-027",
|
||||
"status": error.status,
|
||||
"reason": error.reason,
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"deadline_seconds": DEADLINE,
|
||||
"b36": "NOT_RUN",
|
||||
"target_marker": "not_reached",
|
||||
"production_source_changed": False,
|
||||
"protected_resources_addressed": False,
|
||||
}
|
||||
write_json(OUTPUT / "result.json", failure)
|
||||
print(json.dumps(failure, ensure_ascii=True, sort_keys=True))
|
||||
raise SystemExit({"RUNNER_FAIL": 20, "INFRA_BLOCKED": 21}.get(error.status, 20))
|
||||
PY
|
||||
gate_rc=$?
|
||||
set -e
|
||||
|
||||
cleanup_status=PASS
|
||||
if ! rm -rf -- "$work"; then
|
||||
cleanup_status=FAIL
|
||||
gate_rc=20
|
||||
fi
|
||||
trap - EXIT HUP INT TERM
|
||||
printf 'owned temporary path removed: %s\ncleanup=%s\n' \
|
||||
"$work" "$cleanup_status" >"$output/cleanup.log"
|
||||
|
||||
python3 -B - "$output" "$gate_rc" "$cleanup_status" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
output = Path(sys.argv[1])
|
||||
exit_code = int(sys.argv[2])
|
||||
cleanup = sys.argv[3]
|
||||
if cleanup != "PASS":
|
||||
status = "RUNNER_FAIL"
|
||||
origin = "runner"
|
||||
elif exit_code == 0:
|
||||
status = "PASS"
|
||||
origin = "gate"
|
||||
elif exit_code == 22:
|
||||
status = "STOP"
|
||||
origin = "gate"
|
||||
elif exit_code in (124, 137):
|
||||
status = "INFRA_BLOCKED"
|
||||
origin = "infrastructure"
|
||||
else:
|
||||
status = "RUNNER_FAIL"
|
||||
origin = "runner"
|
||||
(output / "attempt.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema": 1,
|
||||
"exit_code": exit_code,
|
||||
"status": status,
|
||||
"failure_origin": origin,
|
||||
"cleanup": cleanup,
|
||||
"target_marker": "reached" if status in {"PASS", "STOP"} else "not_reached",
|
||||
},
|
||||
ensure_ascii=True,
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
lines = []
|
||||
for path in sorted(output.iterdir(), key=lambda item: item.name):
|
||||
if path.is_file() and path.name != "SHA256SUMS":
|
||||
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
lines.append(f"{digest} {path.name}")
|
||||
(output / "SHA256SUMS").write_text("\n".join(lines) + "\n", encoding="ascii")
|
||||
PY
|
||||
|
||||
printf 'P15-027 gate exit=%s cleanup=%s output=%s\n' \
|
||||
"$gate_rc" "$cleanup_status" "$output"
|
||||
exit "$gate_rc"
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"approved_consumer_manifest_sha256": {},
|
||||
"batch": "B37a",
|
||||
"candidate": "P15-030",
|
||||
"consumer_contract": {
|
||||
"allowed_classes": [
|
||||
"diagnostic",
|
||||
"operational"
|
||||
],
|
||||
"allowed_signal_ids": [
|
||||
"features",
|
||||
"cache_hits",
|
||||
"cache_misses",
|
||||
"cache_waits",
|
||||
"decoder_errors"
|
||||
],
|
||||
"nonqualifying_examples": [
|
||||
"hypothetical future user",
|
||||
"Linux sysfs analogy",
|
||||
"generic observability value",
|
||||
"test script",
|
||||
"gate prototype"
|
||||
],
|
||||
"prohibited_implementation_prefixes": [
|
||||
"planning/",
|
||||
"repo-pre-15/docs/",
|
||||
"repo-pre-15/tests/"
|
||||
],
|
||||
"required_access_mode": "privileged-read",
|
||||
"required_fields": [
|
||||
"schema",
|
||||
"consumer_id",
|
||||
"consumer_class",
|
||||
"owner",
|
||||
"version",
|
||||
"production_use",
|
||||
"implementation_path",
|
||||
"implementation_sha256",
|
||||
"entrypoint",
|
||||
"workflow",
|
||||
"deployment_reference",
|
||||
"signal_ids",
|
||||
"access_mode"
|
||||
],
|
||||
"schema_marker": "pre15-p15-030-consumer-v1"
|
||||
},
|
||||
"consumer_inventory": {
|
||||
"expected_reference_counts": {
|
||||
"historical-evidence": 1,
|
||||
"planning-proposal": 31,
|
||||
"possible-operational-consumer": 0,
|
||||
"project-report-or-documentation": 3,
|
||||
"test-only": 19,
|
||||
"total": 54
|
||||
},
|
||||
"expected_scoped_path_count": 3206,
|
||||
"maximum_candidate_bytes": 2097152,
|
||||
"reference_terms": [
|
||||
"sysctl",
|
||||
"sysfs"
|
||||
],
|
||||
"scope": "tracked repo-pre-15 and planning/pre15 at required_base, excluding planning/pre15/introduction.md and the P15-030 addendum"
|
||||
},
|
||||
"dependency_rule": {
|
||||
"batch": "B37b",
|
||||
"candidate": "P15-068",
|
||||
"identity_format_verdict_when_transport_stops": "NOT_RUN",
|
||||
"on_p15_030_stop": "STOP-NO-SOURCE",
|
||||
"reason": "P15-068 requires the P15-030 sysctl transport; do not create a second lifecycle"
|
||||
},
|
||||
"gate": "G08",
|
||||
"go_requirements": [
|
||||
"concrete existing diagnostic or operational consumer",
|
||||
"bounded atomic counters",
|
||||
"FreeBSD 15 native sysctl_ctx lifecycle closes parse failure, normal unmount, forced unmount, delayed handler, and sysctl_ctx_free failure without freeing handler-visible mount state",
|
||||
"stable permissions and ABI with no unauthenticated metadata leak"
|
||||
],
|
||||
"probe_order": [
|
||||
"consumer-inventory",
|
||||
"bounded-atomic-counter-prototype",
|
||||
"freebsd15-native-sysctl-ctx-lifecycle",
|
||||
"permissions-abi-metadata-leak"
|
||||
],
|
||||
"prohibited_paths": [
|
||||
"planning/pre15/introduction.md"
|
||||
],
|
||||
"protected": {
|
||||
"pid": 26318,
|
||||
"port": 9222
|
||||
},
|
||||
"required_base": "3e1d26a53eef30ecadc3407444d214feac861cb4",
|
||||
"schema": 1,
|
||||
"scope_tree_oids": {
|
||||
"planning/pre15": "b94ad587533ff9fb14e434a8db061381af0ab713",
|
||||
"repo-pre-15": "c2e260c5f9e340e28e13481b3e5a3d04b5b916c8"
|
||||
},
|
||||
"source_sha256": {
|
||||
"planning/pre15/20-final-candidate-ledger.md": "e40f68de241a9c4e5e5bff0849b166b614f56e6cba0b64fc3ced41c036c99c91",
|
||||
"planning/pre15/30-stage0-gates.md": "34708bacf4a0668dfd834b3639900ca23b28b9833a387ac6e2c0399c9316cdb8",
|
||||
"planning/pre15/40-execution-batches.md": "1d1ffdf8fec799aeec063bb1dc4a0fda9703300d414eabef8905522b2c18db20",
|
||||
"planning/pre15/90-honesty-and-evidence.md": "69f96908f5ab436a2f5359548e4e6f5dc2942fcf00148d999d513558f1c59c2d",
|
||||
"repo-pre-15/README.md": "2d3e138f5af20ce02aa562286cb42df8fbd00fdad0ae424e426e0fd8a7f72472",
|
||||
"repo-pre-15/docs/capabilities.md": "f4b2fc0ff74fb7f808ed0f833e87c8230877ab249b611d30a1dbc2103535a8b8",
|
||||
"repo-pre-15/docs/erofs.5": "dea6a9eddbccbb7b8779b6b4e43772edeea9f0e51696c1b8fd09e4d8e69b52f5",
|
||||
"repo-pre-15/docs/features.md": "6d875eb582ef9f00391f90ee5b4b8aec0429f24f0ae3c7b27e788ab1e2c48f2e",
|
||||
"repo-pre-15/src/Makefile": "b722f7ec658e0ada0640c80a7b527921bd96cdf0e93e9623cece29766f2fc9c8",
|
||||
"repo-pre-15/src/internal.h": "3fe8d6ca819f0a1c472cc4b173da80027f957b7f7c25e51ef7fd85b60480c98a",
|
||||
"repo-pre-15/src/super.c": "61f31906bb7d1872c99cd3cf3859a34daf29a02d91a3caa1bfc8187af562a3d2"
|
||||
}
|
||||
}
|
||||
Executable
+592
@@ -0,0 +1,592 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
umask 022
|
||||
|
||||
gate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||
root=$(CDPATH= cd -- "$gate_dir/../../../.." && pwd -P)
|
||||
input=$gate_dir/P15-030-input.json
|
||||
base=
|
||||
output=
|
||||
deadline=60
|
||||
|
||||
while test "$#" -gt 0; do
|
||||
case "$1" in
|
||||
--base)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 20; }
|
||||
base=$2
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 20; }
|
||||
output=$2
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
printf 'unknown argument: %s\n' "$1" >&2
|
||||
exit 20
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 20; }
|
||||
test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 20; }
|
||||
test -f "$input" || { printf 'missing input: %s\n' "$input" >&2; exit 20; }
|
||||
for tool in git mktemp python3 sha256sum timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
printf 'missing required host tool: %s\n' "$tool" >&2
|
||||
exit 21
|
||||
}
|
||||
done
|
||||
case "$output" in
|
||||
/*) ;;
|
||||
*) output=$PWD/$output ;;
|
||||
esac
|
||||
test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 20; }
|
||||
mkdir -p "$output"
|
||||
work=$(mktemp -d "${TMPDIR:-/tmp}/P15-030.XXXXXX")
|
||||
|
||||
cleanup_trap()
|
||||
{
|
||||
rm -rf -- "$work"
|
||||
}
|
||||
trap cleanup_trap EXIT HUP INT TERM
|
||||
|
||||
python3 -B - "$output/command-argv.json" "$0" --base "$base" --output "$output" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
Path(sys.argv[1]).write_text(
|
||||
json.dumps(sys.argv[2:], ensure_ascii=True, separators=(",", ":")) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
PY
|
||||
python3 -B - "$output/ownership.json" "$work" "$output" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
Path(sys.argv[1]).write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"owned_temporary_paths": [sys.argv[2]],
|
||||
"persistent_output": sys.argv[3],
|
||||
"owned_processes": [],
|
||||
"owned_ports": [],
|
||||
"owned_overlays": [],
|
||||
"qemu": "NOT_RUN",
|
||||
},
|
||||
ensure_ascii=True,
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
PY
|
||||
|
||||
set +e
|
||||
timeout -k 5 "$deadline" python3 -B - \
|
||||
"$root" "$input" "$base" "$output" "$deadline" <<'PY' \
|
||||
>"$output/stdout.log" 2>"$output/stderr.log"
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(sys.argv[1])
|
||||
INPUT = Path(sys.argv[2])
|
||||
REQUESTED_BASE = sys.argv[3]
|
||||
OUTPUT = Path(sys.argv[4])
|
||||
DEADLINE = int(sys.argv[5])
|
||||
SPEC = json.loads(INPUT.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
class GateFailure(RuntimeError):
|
||||
def __init__(self, status: str, reason: str):
|
||||
super().__init__(reason)
|
||||
self.status = status
|
||||
self.reason = reason
|
||||
|
||||
|
||||
def sha256_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def write_json(path: Path, value: Any) -> None:
|
||||
path.write_text(
|
||||
json.dumps(value, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
|
||||
|
||||
def run_bytes(argv: list[str], allowed: set[int] | None = None) -> bytes:
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
argv,
|
||||
cwd=ROOT,
|
||||
check=False,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=20,
|
||||
)
|
||||
except subprocess.TimeoutExpired as error:
|
||||
raise GateFailure(
|
||||
"INFRA_BLOCKED", f"command timed out: {' '.join(argv)}"
|
||||
) from error
|
||||
accepted = {0} if allowed is None else allowed
|
||||
if completed.returncode not in accepted:
|
||||
detail = completed.stderr.decode("utf-8", errors="replace").strip()
|
||||
raise GateFailure(
|
||||
"RUNNER_FAIL",
|
||||
f"command failed ({completed.returncode}): {' '.join(argv)}: {detail}",
|
||||
)
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def git_text(*args: str) -> str:
|
||||
return run_bytes(["git", "-C", str(ROOT), *args]).decode(
|
||||
"utf-8", errors="strict"
|
||||
).strip()
|
||||
|
||||
|
||||
def source_at(commit: str, relative: str) -> bytes:
|
||||
return run_bytes(["git", "-C", str(ROOT), "show", f"{commit}:{relative}"])
|
||||
|
||||
|
||||
def parse_ls_tree(raw: bytes) -> dict[str, dict[str, Any]]:
|
||||
entries: dict[str, dict[str, Any]] = {}
|
||||
for record in raw.split(b"\0"):
|
||||
if not record:
|
||||
continue
|
||||
metadata, path_bytes = record.split(b"\t", 1)
|
||||
mode, kind, oid, size = metadata.decode("ascii").split()
|
||||
path = path_bytes.decode("utf-8", errors="strict")
|
||||
entries[path] = {
|
||||
"mode": mode,
|
||||
"kind": kind,
|
||||
"oid": oid,
|
||||
"size": int(size),
|
||||
}
|
||||
return entries
|
||||
|
||||
|
||||
def grep_paths(commit: str, expressions: list[str], fixed: bool = False) -> list[str]:
|
||||
argv = ["git", "-C", str(ROOT), "grep", "-I", "-l"]
|
||||
if fixed:
|
||||
argv.append("-F")
|
||||
else:
|
||||
argv.append("-i")
|
||||
for expression in expressions:
|
||||
argv.extend(["-e", expression])
|
||||
argv.extend(
|
||||
[
|
||||
commit,
|
||||
"--",
|
||||
"repo-pre-15",
|
||||
"planning/pre15",
|
||||
":(exclude)planning/pre15/introduction.md",
|
||||
":(exclude)repo-pre-15/tests/pre15/gates/P15-030.sh",
|
||||
":(exclude)repo-pre-15/tests/pre15/gates/P15-030-input.json",
|
||||
":(exclude)repo-pre-15/docs/pre15-stage0/P15-030.md",
|
||||
]
|
||||
)
|
||||
raw = run_bytes(argv, {0, 1})
|
||||
prefix = f"{commit}:"
|
||||
paths = []
|
||||
for line in raw.decode("utf-8", errors="strict").splitlines():
|
||||
if not line.startswith(prefix):
|
||||
raise GateFailure("RUNNER_FAIL", f"unexpected git grep path: {line}")
|
||||
paths.append(line[len(prefix):])
|
||||
return sorted(paths)
|
||||
|
||||
|
||||
def classify_reference(path: str) -> str:
|
||||
if path.startswith("planning/pre15/evidence/"):
|
||||
return "historical-evidence"
|
||||
if path.startswith("planning/pre15/"):
|
||||
return "planning-proposal"
|
||||
if path.startswith("repo-pre-15/tests/"):
|
||||
return "test-only"
|
||||
if path.startswith("repo-pre-15/docs/") or path.startswith("repo-pre-15/current/"):
|
||||
return "project-report-or-documentation"
|
||||
return "possible-operational-consumer"
|
||||
|
||||
|
||||
def require_text(record: dict[str, Any], field: str) -> str:
|
||||
value = record.get(field)
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer declaration lacks {field}")
|
||||
return value.strip()
|
||||
|
||||
|
||||
def validate_consumer_manifest(
|
||||
path: str,
|
||||
data: bytes,
|
||||
entries: dict[str, dict[str, Any]],
|
||||
commit: str,
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
record = json.loads(data.decode("utf-8", errors="strict"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
||||
raise GateFailure("RUNNER_FAIL", f"invalid consumer manifest: {path}") from error
|
||||
if not isinstance(record, dict):
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer manifest is not an object: {path}")
|
||||
contract = SPEC["consumer_contract"]
|
||||
if record.get("schema") != contract["schema_marker"]:
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer manifest schema mismatch: {path}")
|
||||
consumer_class = require_text(record, "consumer_class")
|
||||
if consumer_class not in contract["allowed_classes"]:
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer class is not operational: {path}")
|
||||
if record.get("production_use") is not True:
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer is not declared in production use: {path}")
|
||||
access_mode = require_text(record, "access_mode")
|
||||
if access_mode != contract["required_access_mode"]:
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer access mode is not privileged: {path}")
|
||||
for field in (
|
||||
"consumer_id",
|
||||
"owner",
|
||||
"version",
|
||||
"entrypoint",
|
||||
"workflow",
|
||||
"deployment_reference",
|
||||
):
|
||||
require_text(record, field)
|
||||
signals = record.get("signal_ids")
|
||||
if (
|
||||
not isinstance(signals, list)
|
||||
or not signals
|
||||
or any(not isinstance(item, str) for item in signals)
|
||||
or len(set(signals)) != len(signals)
|
||||
or not set(signals).issubset(set(contract["allowed_signal_ids"]))
|
||||
):
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer signal set is invalid: {path}")
|
||||
implementation_path = require_text(record, "implementation_path")
|
||||
if not implementation_path.startswith("repo-pre-15/"):
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer implementation is outside DUT: {path}")
|
||||
if any(
|
||||
implementation_path.startswith(prefix)
|
||||
for prefix in contract["prohibited_implementation_prefixes"]
|
||||
):
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer implementation is nonqualifying: {path}")
|
||||
if implementation_path not in entries:
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer implementation is not tracked: {path}")
|
||||
implementation = source_at(commit, implementation_path)
|
||||
implementation_hash = sha256_bytes(implementation)
|
||||
if implementation_hash != require_text(record, "implementation_sha256"):
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer implementation hash mismatch: {path}")
|
||||
implementation_text = implementation.decode("utf-8", errors="replace").lower()
|
||||
if "sysctl" not in implementation_text:
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer implementation does not invoke sysctl: {path}")
|
||||
missing_signals = [item for item in signals if item.lower() not in implementation_text]
|
||||
if missing_signals:
|
||||
raise GateFailure(
|
||||
"RUNNER_FAIL", f"consumer implementation omits signals {missing_signals}: {path}"
|
||||
)
|
||||
return {
|
||||
"manifest_path": path,
|
||||
"manifest_sha256": sha256_bytes(data),
|
||||
"consumer_id": record["consumer_id"],
|
||||
"consumer_class": consumer_class,
|
||||
"implementation_path": implementation_path,
|
||||
"implementation_sha256": implementation_hash,
|
||||
"signal_ids": signals,
|
||||
"access_mode": access_mode,
|
||||
"production_use": True,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if SPEC.get("schema") != 1 or SPEC.get("candidate") != "P15-030":
|
||||
raise GateFailure("RUNNER_FAIL", "invalid P15-030 input identity")
|
||||
if SPEC.get("gate") != "G08" or SPEC.get("batch") != "B37a":
|
||||
raise GateFailure("RUNNER_FAIL", "invalid G08/B37a input identity")
|
||||
resolved = git_text("rev-parse", f"{REQUESTED_BASE}^{{commit}}")
|
||||
if resolved != SPEC["required_base"]:
|
||||
raise GateFailure(
|
||||
"INFRA_BLOCKED",
|
||||
f"P15-030 must replay {SPEC['required_base']}, got {resolved}",
|
||||
)
|
||||
for path, expected_oid in SPEC["scope_tree_oids"].items():
|
||||
actual_oid = git_text("rev-parse", f"{resolved}:{path}")
|
||||
if actual_oid != expected_oid:
|
||||
raise GateFailure("INFRA_BLOCKED", f"frozen scope tree changed: {path}")
|
||||
|
||||
source_hashes = {
|
||||
path: sha256_bytes(source_at(resolved, path))
|
||||
for path in SPEC["source_sha256"]
|
||||
}
|
||||
if source_hashes != SPEC["source_sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen G08 source identity changed")
|
||||
write_json(OUTPUT / "source-sha256.json", source_hashes)
|
||||
addendum_paths = (
|
||||
"repo-pre-15/tests/pre15/gates/P15-030.sh",
|
||||
"repo-pre-15/tests/pre15/gates/P15-030-input.json",
|
||||
"repo-pre-15/docs/pre15-stage0/P15-030.md",
|
||||
)
|
||||
addendum_hashes = {
|
||||
path: sha256_bytes((ROOT / path).read_bytes()) for path in addendum_paths
|
||||
}
|
||||
write_json(OUTPUT / "gate-addendum-sha256.json", addendum_hashes)
|
||||
|
||||
entries = parse_ls_tree(
|
||||
run_bytes(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(ROOT),
|
||||
"ls-tree",
|
||||
"-r",
|
||||
"--long",
|
||||
"-z",
|
||||
resolved,
|
||||
"--",
|
||||
"repo-pre-15",
|
||||
"planning/pre15",
|
||||
]
|
||||
)
|
||||
)
|
||||
prohibited = set(SPEC["prohibited_paths"])
|
||||
scoped_entries = {path: value for path, value in entries.items() if path not in prohibited}
|
||||
if len(scoped_entries) != SPEC["consumer_inventory"]["expected_scoped_path_count"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen consumer scope path count changed")
|
||||
|
||||
reference_paths = grep_paths(
|
||||
resolved, SPEC["consumer_inventory"]["reference_terms"]
|
||||
)
|
||||
maximum_bytes = SPEC["consumer_inventory"]["maximum_candidate_bytes"]
|
||||
references = []
|
||||
reference_counts = {
|
||||
"planning-proposal": 0,
|
||||
"historical-evidence": 0,
|
||||
"test-only": 0,
|
||||
"project-report-or-documentation": 0,
|
||||
"possible-operational-consumer": 0,
|
||||
}
|
||||
for path in reference_paths:
|
||||
if path in prohibited:
|
||||
raise GateFailure("RUNNER_FAIL", "prohibited path entered consumer scan")
|
||||
entry = scoped_entries[path]
|
||||
if entry["size"] > maximum_bytes:
|
||||
raise GateFailure("INFRA_BLOCKED", f"consumer candidate exceeds bound: {path}")
|
||||
data = source_at(resolved, path)
|
||||
classification = classify_reference(path)
|
||||
reference_counts[classification] += 1
|
||||
references.append(
|
||||
{
|
||||
"path": path,
|
||||
"blob_oid": entry["oid"],
|
||||
"bytes": entry["size"],
|
||||
"sha256": sha256_bytes(data),
|
||||
"classification": classification,
|
||||
"qualifies": False,
|
||||
"reason": {
|
||||
"planning-proposal": "proposal or audit text is not an existing consumer",
|
||||
"historical-evidence": "historical gate output is not an existing consumer",
|
||||
"test-only": "a test or gate is explicitly nonqualifying",
|
||||
"project-report-or-documentation": "documentation is not an implemented consumer",
|
||||
"possible-operational-consumer": "requires a validated consumer declaration",
|
||||
}[classification],
|
||||
}
|
||||
)
|
||||
expected_counts = SPEC["consumer_inventory"]["expected_reference_counts"]
|
||||
if len(reference_paths) != expected_counts["total"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen sysctl/sysfs reference count changed")
|
||||
for classification, count in reference_counts.items():
|
||||
if count != expected_counts[classification]:
|
||||
raise GateFailure(
|
||||
"INFRA_BLOCKED", f"frozen reference classification changed: {classification}"
|
||||
)
|
||||
|
||||
marker = SPEC["consumer_contract"]["schema_marker"]
|
||||
manifest_paths = grep_paths(resolved, [marker], fixed=True)
|
||||
consumers = []
|
||||
manifest_hashes = {}
|
||||
for path in manifest_paths:
|
||||
entry = scoped_entries[path]
|
||||
if entry["size"] > maximum_bytes:
|
||||
raise GateFailure("INFRA_BLOCKED", f"consumer manifest exceeds bound: {path}")
|
||||
data = source_at(resolved, path)
|
||||
manifest_hashes[path] = sha256_bytes(data)
|
||||
consumers.append(validate_consumer_manifest(path, data, scoped_entries, resolved))
|
||||
if manifest_hashes != SPEC["approved_consumer_manifest_sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "declared consumer inventory changed")
|
||||
|
||||
inventory = {
|
||||
"schema": 1,
|
||||
"base": resolved,
|
||||
"scope": SPEC["consumer_inventory"]["scope"],
|
||||
"scoped_path_count": len(scoped_entries),
|
||||
"prohibited_path_read": False,
|
||||
"reference_terms": SPEC["consumer_inventory"]["reference_terms"],
|
||||
"reference_path_count": len(reference_paths),
|
||||
"reference_classification_counts": reference_counts,
|
||||
"references": references,
|
||||
"consumer_manifest_marker": marker,
|
||||
"consumer_manifest_count": len(consumers),
|
||||
"qualified_consumer_count": len(consumers),
|
||||
"qualified_consumers": consumers,
|
||||
"nonqualifying_examples": SPEC["consumer_contract"]["nonqualifying_examples"],
|
||||
}
|
||||
write_json(OUTPUT / "consumer-inventory.json", inventory)
|
||||
|
||||
consumer_present = bool(consumers)
|
||||
conditions = [
|
||||
{
|
||||
"id": "concrete-existing-diagnostic-or-operational-consumer",
|
||||
"status": "PASS" if consumer_present else "STOP",
|
||||
"observation": (
|
||||
f"{len(consumers)} qualified consumer declaration(s)"
|
||||
if consumer_present
|
||||
else "zero qualified consumers; all 54 broad references are planning, history, tests, or documentation"
|
||||
),
|
||||
},
|
||||
{
|
||||
"id": "bounded-atomic-counters",
|
||||
"status": "NOT_RUN",
|
||||
"reason": "existing-consumer prerequisite is absent",
|
||||
},
|
||||
{
|
||||
"id": "freebsd15-native-sysctl-ctx-lifecycle",
|
||||
"status": "NOT_RUN",
|
||||
"reason": "existing-consumer prerequisite is absent; no prototype was built",
|
||||
},
|
||||
{
|
||||
"id": "stable-permissions-abi-and-no-unauthenticated-metadata-leak",
|
||||
"status": "NOT_RUN",
|
||||
"reason": "existing-consumer prerequisite is absent",
|
||||
},
|
||||
]
|
||||
if consumer_present:
|
||||
raise GateFailure(
|
||||
"RUNNER_FAIL",
|
||||
"a consumer is now declared; run the FreeBSD 15 native lifecycle prototype before any GO",
|
||||
)
|
||||
if all(item["status"] == "PASS" for item in conditions):
|
||||
raise GateFailure("RUNNER_FAIL", "false-GO guard accepted incomplete conditions")
|
||||
|
||||
result = {
|
||||
"schema": 1,
|
||||
"gate": "G08",
|
||||
"candidate": "P15-030",
|
||||
"batch": "B37a",
|
||||
"status": "STOP",
|
||||
"reason": "no concrete existing diagnostic or operational consumer",
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"resolved_base": resolved,
|
||||
"deadline_seconds": DEADLINE,
|
||||
"expected_exit_code": 22,
|
||||
"conditions": conditions,
|
||||
"false_go_guard": "GO requires all four conditions PASS; evaluated false",
|
||||
"consumer_inventory": "consumer-inventory.json",
|
||||
"b37a": "STOP-NO-SOURCE",
|
||||
"dependency_closure": {
|
||||
"candidate": "P15-068",
|
||||
"batch": "B37b",
|
||||
"status": "STOP",
|
||||
"batch_status": "STOP-NO-SOURCE",
|
||||
"reason": "P15-068 requires the P15-030 sysctl transport, which is STOP",
|
||||
"independent_identity_format_verdict": "NOT_RUN",
|
||||
"second_sysctl_lifecycle": "NOT_RUN",
|
||||
},
|
||||
"prototype": "NOT_RUN",
|
||||
"builds": "NOT_RUN",
|
||||
"qemu": "NOT_RUN",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"smoke_suite": "NOT_RUN",
|
||||
"production_source_changed": False,
|
||||
"p15_068_gate_addendum_created": False,
|
||||
}
|
||||
write_json(OUTPUT / "result.json", result)
|
||||
print(json.dumps(result, ensure_ascii=True, sort_keys=True))
|
||||
return 22
|
||||
|
||||
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except GateFailure as error:
|
||||
failure = {
|
||||
"schema": 1,
|
||||
"gate": "G08",
|
||||
"candidate": "P15-030",
|
||||
"status": error.status,
|
||||
"reason": error.reason,
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"deadline_seconds": DEADLINE,
|
||||
"b37a": "NOT_RUN",
|
||||
"p15_068": "NOT_RUN",
|
||||
"qemu": "NOT_RUN",
|
||||
"production_source_changed": False,
|
||||
}
|
||||
write_json(OUTPUT / "result.json", failure)
|
||||
print(json.dumps(failure, ensure_ascii=True, sort_keys=True))
|
||||
raise SystemExit({"RUNNER_FAIL": 20, "INFRA_BLOCKED": 21}.get(error.status, 20))
|
||||
PY
|
||||
gate_rc=$?
|
||||
set -e
|
||||
|
||||
cleanup_status=PASS
|
||||
if ! rm -rf -- "$work"; then
|
||||
cleanup_status=FAIL
|
||||
gate_rc=20
|
||||
fi
|
||||
trap - EXIT HUP INT TERM
|
||||
printf 'owned temporary path removed: %s\ncleanup=%s\n' \
|
||||
"$work" "$cleanup_status" >"$output/cleanup.log"
|
||||
|
||||
python3 -B - "$output" "$gate_rc" "$cleanup_status" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
output = Path(sys.argv[1])
|
||||
exit_code = int(sys.argv[2])
|
||||
cleanup = sys.argv[3]
|
||||
if cleanup != "PASS":
|
||||
status = "RUNNER_FAIL"
|
||||
origin = "runner"
|
||||
elif exit_code == 22:
|
||||
status = "STOP"
|
||||
origin = "gate"
|
||||
elif exit_code in (124, 137):
|
||||
status = "INFRA_BLOCKED"
|
||||
origin = "infrastructure"
|
||||
else:
|
||||
status = "RUNNER_FAIL" if exit_code == 20 else "INFRA_BLOCKED"
|
||||
origin = "runner" if exit_code == 20 else "infrastructure"
|
||||
(output / "attempt.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema": 1,
|
||||
"exit_code": exit_code,
|
||||
"status": status,
|
||||
"failure_origin": origin,
|
||||
"cleanup": cleanup,
|
||||
"target_marker": "reached" if status == "STOP" else "not_reached",
|
||||
},
|
||||
ensure_ascii=True,
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
lines = []
|
||||
for path in sorted(output.iterdir(), key=lambda item: item.name):
|
||||
if path.is_file() and path.name != "SHA256SUMS":
|
||||
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
lines.append(f"{digest} {path.name}")
|
||||
(output / "SHA256SUMS").write_text("\n".join(lines) + "\n", encoding="ascii")
|
||||
PY
|
||||
|
||||
printf 'P15-030 gate exit=%s cleanup=%s output=%s\n' \
|
||||
"$gate_rc" "$cleanup_status" "$output"
|
||||
exit "$gate_rc"
|
||||
@@ -0,0 +1,273 @@
|
||||
{
|
||||
"approved_consumer_manifest_sha256": {},
|
||||
"batch": "B38",
|
||||
"benchmark_contract": {
|
||||
"control_drift_limit_percent": 3.0,
|
||||
"hot_path_median_regression_limit_percent": 3.0,
|
||||
"minimum_timed_iterations_per_sample": 100000,
|
||||
"outlier_deletion": false,
|
||||
"pair_order": "five AB and five BA pairs",
|
||||
"raw_sample_retention": true,
|
||||
"required_runs": 10,
|
||||
"unstable_control_status": "INFRA_BLOCKED",
|
||||
"warmup_runs": 3
|
||||
},
|
||||
"build_contract": {
|
||||
"configurations": [
|
||||
"WITH_ZSTDIO=0",
|
||||
"WITH_ZSTDIO=1"
|
||||
],
|
||||
"disabled_probe_mode": true,
|
||||
"new_undefined_symbols_allowed": 0
|
||||
},
|
||||
"candidate": "P15-031",
|
||||
"consumer_contract": {
|
||||
"allowed_transports": [
|
||||
"dtrace-sdt",
|
||||
"ktr"
|
||||
],
|
||||
"manifest_prefix": "repo-pre-15/diagnostics/",
|
||||
"nonqualifying_examples": [
|
||||
"hypothetical maintainer",
|
||||
"Linux tracepoints",
|
||||
"generic observability value",
|
||||
"proposed B38 test",
|
||||
"gate prototype",
|
||||
"installed dtrace without a project consumer"
|
||||
],
|
||||
"prohibited_implementation_prefixes": [
|
||||
"planning/",
|
||||
"repo-pre-15/current/",
|
||||
"repo-pre-15/docs/",
|
||||
"repo-pre-15/src/",
|
||||
"repo-pre-15/tests/"
|
||||
],
|
||||
"required_access_mode": "privileged-diagnostic",
|
||||
"required_fields": [
|
||||
"schema",
|
||||
"consumer_id",
|
||||
"consumer_class",
|
||||
"owner",
|
||||
"version",
|
||||
"production_use",
|
||||
"freebsd_version",
|
||||
"transport",
|
||||
"access_mode",
|
||||
"implementation_path",
|
||||
"implementation_sha256",
|
||||
"entrypoint",
|
||||
"workflow",
|
||||
"deployment_reference",
|
||||
"event_schema_sha256",
|
||||
"event_ids",
|
||||
"actual_capture",
|
||||
"capture_evidence_path",
|
||||
"capture_evidence_sha256",
|
||||
"captured_event_counts"
|
||||
],
|
||||
"schema_marker": "pre15-p15-031-consumer-v1"
|
||||
},
|
||||
"consumer_inventory": {
|
||||
"expected_reference_counts": {
|
||||
"historical-evidence": 3,
|
||||
"linux-reference": 0,
|
||||
"planning-proposal": 16,
|
||||
"possible-project-consumer": 0,
|
||||
"project-report-or-documentation": 4,
|
||||
"test-only": 10,
|
||||
"total": 33
|
||||
},
|
||||
"expected_scoped_path_count": 3243,
|
||||
"maximum_candidate_bytes": 2097152,
|
||||
"reference_patterns": [
|
||||
"dtrace",
|
||||
"(^|[^[:alnum:]_])sdt([^[:alnum:]_]|$)|sdt_(probe|provider)",
|
||||
"(^|[^[:alnum:]_])ktr([^[:alnum:]_]|$)|ktrdump|ktr_[[:alnum:]_]+",
|
||||
"(^|[^[:alnum:]_])tracepoints?([^[:alnum:]_]|$)",
|
||||
"(^|[^[:alnum:]_])observability([^[:alnum:]_]|$)"
|
||||
],
|
||||
"scope": "tracked repo-pre-15 and planning/pre15 at required_base, excluding planning/pre15/introduction.md and the P15-031 addendum"
|
||||
},
|
||||
"event_schema": {
|
||||
"forbidden_fields": [
|
||||
"kernel_pointer",
|
||||
"credential",
|
||||
"path",
|
||||
"xattr_name",
|
||||
"xattr_value",
|
||||
"uuid",
|
||||
"volume_label",
|
||||
"raw_unvalidated_metadata"
|
||||
],
|
||||
"provider": "erofs",
|
||||
"events": [
|
||||
{
|
||||
"fields": [
|
||||
"parse_stage",
|
||||
"error"
|
||||
],
|
||||
"id": "mount_parse_fail",
|
||||
"rule": "emit only bounded stage enum and positive errno; no image-derived value"
|
||||
},
|
||||
{
|
||||
"fields": [
|
||||
"nid",
|
||||
"logical_offset",
|
||||
"mapped_length",
|
||||
"device_id",
|
||||
"map_flags",
|
||||
"error"
|
||||
],
|
||||
"id": "map_result",
|
||||
"rule": "emit only after range validation; zero mapping fields on validation failure"
|
||||
},
|
||||
{
|
||||
"fields": [
|
||||
"nid"
|
||||
],
|
||||
"id": "vget_hit",
|
||||
"rule": "validated numeric inode identity only"
|
||||
},
|
||||
{
|
||||
"fields": [
|
||||
"nid"
|
||||
],
|
||||
"id": "vget_miss",
|
||||
"rule": "validated numeric inode identity only"
|
||||
},
|
||||
{
|
||||
"fields": [
|
||||
"nid"
|
||||
],
|
||||
"id": "vget_loser",
|
||||
"rule": "validated numeric inode identity only"
|
||||
},
|
||||
{
|
||||
"fields": [
|
||||
"nid",
|
||||
"name_index"
|
||||
],
|
||||
"id": "xattr_cache_hit",
|
||||
"rule": "namespace index only; no xattr name or value"
|
||||
},
|
||||
{
|
||||
"fields": [
|
||||
"nid",
|
||||
"name_index"
|
||||
],
|
||||
"id": "xattr_cache_miss",
|
||||
"rule": "namespace index only; no xattr name or value"
|
||||
},
|
||||
{
|
||||
"fields": [
|
||||
"nid",
|
||||
"algorithm",
|
||||
"offset",
|
||||
"input_length",
|
||||
"output_length"
|
||||
],
|
||||
"id": "decode_start",
|
||||
"rule": "validated numeric decode request only"
|
||||
},
|
||||
{
|
||||
"fields": [
|
||||
"nid",
|
||||
"algorithm",
|
||||
"offset",
|
||||
"decoded_length"
|
||||
],
|
||||
"id": "decode_end",
|
||||
"rule": "validated numeric decode result only"
|
||||
},
|
||||
{
|
||||
"fields": [
|
||||
"nid",
|
||||
"algorithm",
|
||||
"offset",
|
||||
"error"
|
||||
],
|
||||
"id": "decode_error",
|
||||
"rule": "validated numeric identity and positive errno only"
|
||||
},
|
||||
{
|
||||
"fields": [
|
||||
"nid",
|
||||
"offset",
|
||||
"decoded_length"
|
||||
],
|
||||
"id": "cache_wait",
|
||||
"rule": "validated cache key components only"
|
||||
},
|
||||
{
|
||||
"fields": [
|
||||
"nid",
|
||||
"offset",
|
||||
"decoded_length",
|
||||
"reason"
|
||||
],
|
||||
"id": "cache_evict",
|
||||
"rule": "bounded reason enum and validated cache key components only"
|
||||
}
|
||||
]
|
||||
},
|
||||
"gate": "G08",
|
||||
"go_requirements": [
|
||||
"a concrete existing project diagnostic consumer captures the complete predeclared schema through FreeBSD DTrace/SDT or a justified KTR path",
|
||||
"no kernel pointers, credentials, or unauthenticated metadata leakage",
|
||||
"an owned-temp pre-source prototype generated from this JSON and the gate script closes fields, counts, failure, detach, and unload without linking into EROFS",
|
||||
"disabled WITH_ZSTDIO=0 and WITH_ZSTDIO=1 builds add no undefined symbols",
|
||||
"ten-run hot-path median regression is at most three percent with raw interleaved controls and no outlier deletion"
|
||||
],
|
||||
"probe_order": [
|
||||
"consumer-inventory",
|
||||
"privacy-schema",
|
||||
"freebsd15-native-owned-temp-prototype",
|
||||
"disabled-dual-build-undefined-symbols",
|
||||
"ten-run-hot-path-microbenchmark"
|
||||
],
|
||||
"prohibited_paths": [
|
||||
"planning/pre15/introduction.md"
|
||||
],
|
||||
"protected": {
|
||||
"pid": 26318,
|
||||
"port": 9222
|
||||
},
|
||||
"prototype_contract": {
|
||||
"expected_event_count_each": 1,
|
||||
"generated_from": [
|
||||
"P15-031-input.json",
|
||||
"P15-031.sh"
|
||||
],
|
||||
"lifecycle_scenarios": [
|
||||
"consumer attach and complete capture",
|
||||
"failed consumer enable",
|
||||
"consumer termination and detach",
|
||||
"provider unload and reload",
|
||||
"final unload with zero owned processes, modules, and temporary paths"
|
||||
],
|
||||
"linked_into_erofs": false,
|
||||
"location": "owned temporary directory only",
|
||||
"native_target": "FreeBSD 15"
|
||||
},
|
||||
"required_base": "0f696ad1e2c6628022d02ce52d07eb66704769dd",
|
||||
"schema": 1,
|
||||
"scope_tree_oids": {
|
||||
"planning/pre15": "bb8801a7696465fce1607b671c96473c044773b5",
|
||||
"repo-pre-15": "231f9d5566d0a6bee4672a89f93451daa4f4b5de"
|
||||
},
|
||||
"source_sha256": {
|
||||
"planning/pre15/20-final-candidate-ledger.md": "c47338623686ce352fd80cf9b39635900bcde8c2abc960feebe24837f8a67460",
|
||||
"planning/pre15/30-stage0-gates.md": "34708bacf4a0668dfd834b3639900ca23b28b9833a387ac6e2c0399c9316cdb8",
|
||||
"planning/pre15/40-execution-batches.md": "1d1ffdf8fec799aeec063bb1dc4a0fda9703300d414eabef8905522b2c18db20",
|
||||
"planning/pre15/50-feature-test-matrix.md": "ce39b44ef3989f4fcdbcd8eb677193ef53b630080c74d8539664fb2be3f38408",
|
||||
"planning/pre15/90-honesty-and-evidence.md": "69f96908f5ab436a2f5359548e4e6f5dc2942fcf00148d999d513558f1c59c2d",
|
||||
"repo-pre-15/src/Makefile": "b722f7ec658e0ada0640c80a7b527921bd96cdf0e93e9623cece29766f2fc9c8",
|
||||
"repo-pre-15/src/data.c": "a511146d2d1ff56bc88bae6c550cfdc49a8e978c5c2c88d735de92adaaa836b6",
|
||||
"repo-pre-15/src/decompressor.c": "ccaa934f837ecbf3b3b678dbddf35d29dd7e3b4e80d1a7ab78b18a07e555419f",
|
||||
"repo-pre-15/src/inode.c": "50c40fe403bd06b17f6976707ad4a8f03f95af49753f450df3d132130ab2dcf4",
|
||||
"repo-pre-15/src/super.c": "61f31906bb7d1872c99cd3cf3859a34daf29a02d91a3caa1bfc8187af562a3d2",
|
||||
"repo-pre-15/src/xattr.c": "7ce78b7af8838715fed10843f412458bdacb8c9b6811f08f660549f18a0743b0",
|
||||
"repo-pre-15/src/zdata.c": "ee241c83fe7152569414594a3b4acf8c5028a35573d53ace0e3bb4a51dd0d191",
|
||||
"repo-pre-15/tests/pre15/EVIDENCE-SCHEMA.json": "8a4d3fe076eed3a2f17e3ebd9337ca8697f09d6717c05835e076f6812cf38102"
|
||||
}
|
||||
}
|
||||
Executable
+704
@@ -0,0 +1,704 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
umask 022
|
||||
|
||||
gate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||
root=$(CDPATH= cd -- "$gate_dir/../../../.." && pwd -P)
|
||||
input=$gate_dir/P15-031-input.json
|
||||
base=
|
||||
output=
|
||||
deadline=60
|
||||
|
||||
while test "$#" -gt 0; do
|
||||
case "$1" in
|
||||
--base)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 20; }
|
||||
base=$2
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 20; }
|
||||
output=$2
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
printf 'unknown argument: %s\n' "$1" >&2
|
||||
exit 20
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 20; }
|
||||
test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 20; }
|
||||
test -f "$input" || { printf 'missing input: %s\n' "$input" >&2; exit 20; }
|
||||
for tool in git mktemp python3 sha256sum timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
printf 'missing required host tool: %s\n' "$tool" >&2
|
||||
exit 21
|
||||
}
|
||||
done
|
||||
case "$output" in
|
||||
/*) ;;
|
||||
*) output=$PWD/$output ;;
|
||||
esac
|
||||
test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 20; }
|
||||
mkdir -p "$output"
|
||||
work=$(mktemp -d "${TMPDIR:-/tmp}/P15-031.XXXXXX")
|
||||
|
||||
cleanup_trap()
|
||||
{
|
||||
rm -rf -- "$work"
|
||||
}
|
||||
trap cleanup_trap EXIT HUP INT TERM
|
||||
|
||||
python3 -B - "$output/command-argv.json" "$0" --base "$base" --output "$output" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
Path(sys.argv[1]).write_text(
|
||||
json.dumps(sys.argv[2:], ensure_ascii=True, separators=(",", ":")) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
PY
|
||||
python3 -B - "$output/ownership.json" "$work" "$output" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
Path(sys.argv[1]).write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"owned_temporary_paths": [sys.argv[2]],
|
||||
"persistent_output": sys.argv[3],
|
||||
"owned_processes": [],
|
||||
"owned_ports": [],
|
||||
"owned_overlays": [],
|
||||
"qemu": "NOT_RUN",
|
||||
},
|
||||
ensure_ascii=True,
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
PY
|
||||
|
||||
set +e
|
||||
timeout -k 5 "$deadline" python3 -B - \
|
||||
"$root" "$input" "$base" "$output" "$deadline" <<'PY' \
|
||||
>"$output/stdout.log" 2>"$output/stderr.log"
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(sys.argv[1])
|
||||
INPUT = Path(sys.argv[2])
|
||||
REQUESTED_BASE = sys.argv[3]
|
||||
OUTPUT = Path(sys.argv[4])
|
||||
DEADLINE = int(sys.argv[5])
|
||||
SPEC = json.loads(INPUT.read_text(encoding="utf-8"))
|
||||
START_UTC = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
class GateFailure(RuntimeError):
|
||||
def __init__(self, status: str, reason: str):
|
||||
super().__init__(reason)
|
||||
self.status = status
|
||||
self.reason = reason
|
||||
|
||||
|
||||
def sha256_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def canonical_sha256(value: Any) -> str:
|
||||
raw = json.dumps(
|
||||
value, ensure_ascii=True, separators=(",", ":"), sort_keys=True
|
||||
).encode("ascii")
|
||||
return sha256_bytes(raw)
|
||||
|
||||
|
||||
def write_json(path: Path, value: Any) -> None:
|
||||
path.write_text(
|
||||
json.dumps(value, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
|
||||
|
||||
def run_bytes(argv: list[str], allowed: set[int] | None = None) -> bytes:
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
argv,
|
||||
cwd=ROOT,
|
||||
check=False,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=20,
|
||||
)
|
||||
except subprocess.TimeoutExpired as error:
|
||||
raise GateFailure(
|
||||
"INFRA_BLOCKED", f"command timed out: {' '.join(argv)}"
|
||||
) from error
|
||||
accepted = {0} if allowed is None else allowed
|
||||
if completed.returncode not in accepted:
|
||||
detail = completed.stderr.decode("utf-8", errors="replace").strip()
|
||||
raise GateFailure(
|
||||
"RUNNER_FAIL",
|
||||
f"command failed ({completed.returncode}): {' '.join(argv)}: {detail}",
|
||||
)
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def git_text(*args: str) -> str:
|
||||
return run_bytes(["git", "-C", str(ROOT), *args]).decode(
|
||||
"utf-8", errors="strict"
|
||||
).strip()
|
||||
|
||||
|
||||
def source_at(commit: str, relative: str) -> bytes:
|
||||
return run_bytes(["git", "-C", str(ROOT), "show", f"{commit}:{relative}"])
|
||||
|
||||
|
||||
def parse_ls_tree(raw: bytes) -> dict[str, dict[str, Any]]:
|
||||
entries: dict[str, dict[str, Any]] = {}
|
||||
for record in raw.split(b"\0"):
|
||||
if not record:
|
||||
continue
|
||||
metadata, path_bytes = record.split(b"\t", 1)
|
||||
mode, kind, oid, size = metadata.decode("ascii").split()
|
||||
path = path_bytes.decode("utf-8", errors="strict")
|
||||
entries[path] = {
|
||||
"mode": mode,
|
||||
"kind": kind,
|
||||
"oid": oid,
|
||||
"size": int(size),
|
||||
}
|
||||
return entries
|
||||
|
||||
|
||||
def grep_paths(commit: str, expressions: list[str], fixed: bool = False) -> list[str]:
|
||||
argv = ["git", "-C", str(ROOT), "grep", "-I", "-l"]
|
||||
if fixed:
|
||||
argv.append("-F")
|
||||
else:
|
||||
argv.extend(["-i", "-E"])
|
||||
for expression in expressions:
|
||||
argv.extend(["-e", expression])
|
||||
argv.extend(
|
||||
[
|
||||
commit,
|
||||
"--",
|
||||
"repo-pre-15",
|
||||
"planning/pre15",
|
||||
":(exclude)planning/pre15/introduction.md",
|
||||
":(exclude)repo-pre-15/tests/pre15/gates/P15-031.sh",
|
||||
":(exclude)repo-pre-15/tests/pre15/gates/P15-031-input.json",
|
||||
":(exclude)repo-pre-15/docs/pre15-stage0/P15-031.md",
|
||||
]
|
||||
)
|
||||
raw = run_bytes(argv, {0, 1})
|
||||
prefix = f"{commit}:"
|
||||
paths = []
|
||||
for line in raw.decode("utf-8", errors="strict").splitlines():
|
||||
if not line.startswith(prefix):
|
||||
raise GateFailure("RUNNER_FAIL", f"unexpected git grep path: {line}")
|
||||
paths.append(line[len(prefix):])
|
||||
return sorted(paths)
|
||||
|
||||
|
||||
def classify_reference(path: str) -> str:
|
||||
if path.startswith("planning/pre15/evidence/"):
|
||||
return "historical-evidence"
|
||||
if path.startswith("planning/pre15/"):
|
||||
return "planning-proposal"
|
||||
if path.startswith("repo-pre-15/tests/"):
|
||||
return "test-only"
|
||||
if path.startswith("repo-pre-15/src-linux/"):
|
||||
return "linux-reference"
|
||||
if path.startswith("repo-pre-15/docs/") or path.startswith("repo-pre-15/current/"):
|
||||
return "project-report-or-documentation"
|
||||
return "possible-project-consumer"
|
||||
|
||||
|
||||
def require_text(record: dict[str, Any], field: str) -> str:
|
||||
value = record.get(field)
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer declaration lacks {field}")
|
||||
return value.strip()
|
||||
|
||||
|
||||
def validate_bound_path(
|
||||
record: dict[str, Any], path_field: str, hash_field: str,
|
||||
entries: dict[str, dict[str, Any]], commit: str, maximum_bytes: int,
|
||||
) -> tuple[str, bytes, str]:
|
||||
path = require_text(record, path_field)
|
||||
if not path.startswith("repo-pre-15/"):
|
||||
raise GateFailure("RUNNER_FAIL", f"{path_field} is outside DUT: {path}")
|
||||
if path not in entries:
|
||||
raise GateFailure("RUNNER_FAIL", f"{path_field} is not tracked: {path}")
|
||||
if entries[path]["size"] > maximum_bytes:
|
||||
raise GateFailure("INFRA_BLOCKED", f"bound file exceeds size limit: {path}")
|
||||
data = source_at(commit, path)
|
||||
digest = sha256_bytes(data)
|
||||
if digest != require_text(record, hash_field):
|
||||
raise GateFailure("RUNNER_FAIL", f"{hash_field} mismatch: {path}")
|
||||
return path, data, digest
|
||||
|
||||
|
||||
def validate_consumer_manifest(
|
||||
path: str, data: bytes, entries: dict[str, dict[str, Any]], commit: str,
|
||||
event_schema_sha256: str, maximum_bytes: int,
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
record = json.loads(data.decode("utf-8", errors="strict"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
||||
raise GateFailure("RUNNER_FAIL", f"invalid consumer manifest: {path}") from error
|
||||
if not isinstance(record, dict):
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer manifest is not an object: {path}")
|
||||
contract = SPEC["consumer_contract"]
|
||||
if not path.startswith(contract["manifest_prefix"]):
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer manifest is outside diagnostic prefix: {path}")
|
||||
if record.get("schema") != contract["schema_marker"]:
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer manifest schema mismatch: {path}")
|
||||
missing_fields = sorted(set(contract["required_fields"]) - set(record))
|
||||
if missing_fields:
|
||||
raise GateFailure(
|
||||
"RUNNER_FAIL", f"consumer manifest lacks fields {missing_fields}: {path}"
|
||||
)
|
||||
if require_text(record, "consumer_class") != "diagnostic":
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer is not diagnostic: {path}")
|
||||
if record.get("production_use") is not True or record.get("actual_capture") is not True:
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer lacks production capture evidence: {path}")
|
||||
if not require_text(record, "freebsd_version").startswith("FreeBSD 15"):
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer capture is not FreeBSD 15: {path}")
|
||||
transport = require_text(record, "transport")
|
||||
if transport not in contract["allowed_transports"]:
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer transport is not native: {path}")
|
||||
if require_text(record, "access_mode") != contract["required_access_mode"]:
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer access is not privileged: {path}")
|
||||
for field in (
|
||||
"consumer_id", "owner", "version", "entrypoint", "workflow",
|
||||
"deployment_reference",
|
||||
):
|
||||
require_text(record, field)
|
||||
if record.get("event_schema_sha256") != event_schema_sha256:
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer event schema mismatch: {path}")
|
||||
|
||||
expected_ids = [event["id"] for event in SPEC["event_schema"]["events"]]
|
||||
if record.get("event_ids") != expected_ids:
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer event order/set is incomplete: {path}")
|
||||
counts = record.get("captured_event_counts")
|
||||
if not isinstance(counts, dict) or set(counts) != set(expected_ids):
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer capture counts are incomplete: {path}")
|
||||
if any(type(counts[event_id]) is not int or counts[event_id] < 1 for event_id in expected_ids):
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer did not capture every event: {path}")
|
||||
|
||||
implementation_path, implementation, implementation_hash = validate_bound_path(
|
||||
record, "implementation_path", "implementation_sha256",
|
||||
entries, commit, maximum_bytes,
|
||||
)
|
||||
if any(
|
||||
implementation_path.startswith(prefix)
|
||||
for prefix in contract["prohibited_implementation_prefixes"]
|
||||
):
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer implementation is nonqualifying: {path}")
|
||||
capture_path, capture, capture_hash = validate_bound_path(
|
||||
record, "capture_evidence_path", "capture_evidence_sha256",
|
||||
entries, commit, maximum_bytes,
|
||||
)
|
||||
implementation_text = implementation.decode("utf-8", errors="replace").lower()
|
||||
capture_text = capture.decode("utf-8", errors="replace").lower()
|
||||
missing_implementation_events = [
|
||||
event_id for event_id in expected_ids
|
||||
if event_id not in implementation_text and event_id.replace("_", "-") not in implementation_text
|
||||
]
|
||||
missing_capture_events = [
|
||||
event_id for event_id in expected_ids
|
||||
if event_id not in capture_text and event_id.replace("_", "-") not in capture_text
|
||||
]
|
||||
if missing_implementation_events:
|
||||
raise GateFailure(
|
||||
"RUNNER_FAIL", f"consumer implementation omits {missing_implementation_events}: {path}"
|
||||
)
|
||||
if missing_capture_events:
|
||||
raise GateFailure(
|
||||
"RUNNER_FAIL", f"consumer capture omits {missing_capture_events}: {path}"
|
||||
)
|
||||
if transport == "dtrace-sdt":
|
||||
if "dtrace" not in implementation_text or "erofs" not in implementation_text:
|
||||
raise GateFailure("RUNNER_FAIL", f"consumer does not invoke EROFS DTrace: {path}")
|
||||
else:
|
||||
if "ktrdump" not in implementation_text:
|
||||
raise GateFailure("RUNNER_FAIL", f"KTR consumer does not invoke ktrdump: {path}")
|
||||
require_text(record, "ktr_justification")
|
||||
|
||||
return {
|
||||
"manifest_path": path,
|
||||
"manifest_sha256": sha256_bytes(data),
|
||||
"consumer_id": record["consumer_id"],
|
||||
"owner": record["owner"],
|
||||
"version": record["version"],
|
||||
"transport": transport,
|
||||
"implementation_path": implementation_path,
|
||||
"implementation_sha256": implementation_hash,
|
||||
"capture_evidence_path": capture_path,
|
||||
"capture_evidence_sha256": capture_hash,
|
||||
"captured_event_counts": counts,
|
||||
"production_use": True,
|
||||
"actual_capture": True,
|
||||
}
|
||||
|
||||
|
||||
def all_conditions_pass(statuses: list[str]) -> bool:
|
||||
return len(statuses) == 5 and all(status == "PASS" for status in statuses)
|
||||
|
||||
|
||||
def false_go_controls() -> dict[str, Any]:
|
||||
vectors = []
|
||||
all_pass = ["PASS"] * 5
|
||||
if not all_conditions_pass(all_pass):
|
||||
raise GateFailure("RUNNER_FAIL", "all-PASS control did not authorize GO")
|
||||
vectors.append({"name": "all-pass", "go": True})
|
||||
for index in range(5):
|
||||
statuses = all_pass.copy()
|
||||
statuses[index] = "NOT_RUN"
|
||||
if all_conditions_pass(statuses):
|
||||
raise GateFailure("RUNNER_FAIL", f"false-GO control {index} was accepted")
|
||||
vectors.append({"name": f"condition-{index + 1}-not-run", "go": False})
|
||||
statuses = all_pass.copy()
|
||||
statuses[0] = "STOP"
|
||||
if all_conditions_pass(statuses):
|
||||
raise GateFailure("RUNNER_FAIL", "consumer STOP control was accepted")
|
||||
vectors.append({"name": "consumer-stop", "go": False})
|
||||
return {"status": "PASS", "control_count": len(vectors), "vectors": vectors}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if SPEC.get("schema") != 1 or SPEC.get("candidate") != "P15-031":
|
||||
raise GateFailure("RUNNER_FAIL", "invalid P15-031 input identity")
|
||||
if SPEC.get("gate") != "G08" or SPEC.get("batch") != "B38":
|
||||
raise GateFailure("RUNNER_FAIL", "invalid G08/B38 input identity")
|
||||
if len(SPEC.get("go_requirements", [])) != 5:
|
||||
raise GateFailure("RUNNER_FAIL", "G08 P15-031 must have exactly five GO requirements")
|
||||
|
||||
resolved = git_text("rev-parse", f"{REQUESTED_BASE}^{{commit}}")
|
||||
if resolved != SPEC["required_base"]:
|
||||
raise GateFailure(
|
||||
"INFRA_BLOCKED", f"P15-031 must replay {SPEC['required_base']}, got {resolved}"
|
||||
)
|
||||
for path, expected_oid in SPEC["scope_tree_oids"].items():
|
||||
actual_oid = git_text("rev-parse", f"{resolved}:{path}")
|
||||
if actual_oid != expected_oid:
|
||||
raise GateFailure("INFRA_BLOCKED", f"frozen scope tree changed: {path}")
|
||||
|
||||
source_hashes = {
|
||||
path: sha256_bytes(source_at(resolved, path))
|
||||
for path in SPEC["source_sha256"]
|
||||
}
|
||||
if source_hashes != SPEC["source_sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen G08/B38 source identity changed")
|
||||
write_json(OUTPUT / "source-sha256.json", source_hashes)
|
||||
addendum_paths = (
|
||||
"repo-pre-15/tests/pre15/gates/P15-031.sh",
|
||||
"repo-pre-15/tests/pre15/gates/P15-031-input.json",
|
||||
"repo-pre-15/docs/pre15-stage0/P15-031.md",
|
||||
)
|
||||
addendum_hashes = {
|
||||
path: sha256_bytes((ROOT / path).read_bytes()) for path in addendum_paths
|
||||
}
|
||||
write_json(OUTPUT / "gate-addendum-sha256.json", addendum_hashes)
|
||||
|
||||
entries = parse_ls_tree(
|
||||
run_bytes(
|
||||
[
|
||||
"git", "-C", str(ROOT), "ls-tree", "-r", "--long", "-z",
|
||||
resolved, "--", "repo-pre-15", "planning/pre15",
|
||||
]
|
||||
)
|
||||
)
|
||||
prohibited = set(SPEC["prohibited_paths"])
|
||||
scoped_entries = {path: value for path, value in entries.items() if path not in prohibited}
|
||||
if len(scoped_entries) != SPEC["consumer_inventory"]["expected_scoped_path_count"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen consumer scope path count changed")
|
||||
|
||||
reference_paths = grep_paths(
|
||||
resolved, SPEC["consumer_inventory"]["reference_patterns"]
|
||||
)
|
||||
maximum_bytes = SPEC["consumer_inventory"]["maximum_candidate_bytes"]
|
||||
references = []
|
||||
reference_counts = {
|
||||
"planning-proposal": 0,
|
||||
"historical-evidence": 0,
|
||||
"test-only": 0,
|
||||
"linux-reference": 0,
|
||||
"project-report-or-documentation": 0,
|
||||
"possible-project-consumer": 0,
|
||||
}
|
||||
reasons = {
|
||||
"planning-proposal": "proposal or audit text is not an existing consumer",
|
||||
"historical-evidence": "historical gate output is not a deployed consumer",
|
||||
"test-only": "a test, gate, or test record is explicitly nonqualifying",
|
||||
"linux-reference": "Linux tracepoints are not a FreeBSD diagnostic consumer",
|
||||
"project-report-or-documentation": "documentation is not an implemented consumer",
|
||||
"possible-project-consumer": "requires a validated production capture manifest",
|
||||
}
|
||||
for path in reference_paths:
|
||||
if path in prohibited:
|
||||
raise GateFailure("RUNNER_FAIL", "prohibited path entered consumer scan")
|
||||
entry = scoped_entries[path]
|
||||
if entry["size"] > maximum_bytes:
|
||||
raise GateFailure("INFRA_BLOCKED", f"consumer candidate exceeds bound: {path}")
|
||||
data = source_at(resolved, path)
|
||||
classification = classify_reference(path)
|
||||
reference_counts[classification] += 1
|
||||
references.append(
|
||||
{
|
||||
"path": path,
|
||||
"blob_oid": entry["oid"],
|
||||
"bytes": entry["size"],
|
||||
"sha256": sha256_bytes(data),
|
||||
"classification": classification,
|
||||
"qualifies": False,
|
||||
"reason": reasons[classification],
|
||||
}
|
||||
)
|
||||
expected_counts = SPEC["consumer_inventory"]["expected_reference_counts"]
|
||||
if len(reference_paths) != expected_counts["total"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen native-tracing reference count changed")
|
||||
for classification, count in reference_counts.items():
|
||||
if count != expected_counts[classification]:
|
||||
raise GateFailure(
|
||||
"INFRA_BLOCKED", f"frozen reference classification changed: {classification}"
|
||||
)
|
||||
|
||||
event_schema_sha256 = canonical_sha256(SPEC["event_schema"])
|
||||
marker = SPEC["consumer_contract"]["schema_marker"]
|
||||
manifest_paths = grep_paths(resolved, [marker], fixed=True)
|
||||
consumers = []
|
||||
manifest_hashes = {}
|
||||
for path in manifest_paths:
|
||||
entry = scoped_entries[path]
|
||||
if entry["size"] > maximum_bytes:
|
||||
raise GateFailure("INFRA_BLOCKED", f"consumer manifest exceeds bound: {path}")
|
||||
data = source_at(resolved, path)
|
||||
manifest_hashes[path] = sha256_bytes(data)
|
||||
consumers.append(
|
||||
validate_consumer_manifest(
|
||||
path, data, scoped_entries, resolved, event_schema_sha256, maximum_bytes
|
||||
)
|
||||
)
|
||||
if manifest_hashes != SPEC["approved_consumer_manifest_sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "declared consumer inventory changed")
|
||||
|
||||
inventory = {
|
||||
"schema": 1,
|
||||
"base": resolved,
|
||||
"scope": SPEC["consumer_inventory"]["scope"],
|
||||
"scoped_path_count": len(scoped_entries),
|
||||
"prohibited_path_read": False,
|
||||
"reference_patterns": SPEC["consumer_inventory"]["reference_patterns"],
|
||||
"reference_path_count": len(reference_paths),
|
||||
"reference_classification_counts": reference_counts,
|
||||
"references": references,
|
||||
"consumer_manifest_marker": marker,
|
||||
"consumer_manifest_count": len(consumers),
|
||||
"qualified_consumer_count": len(consumers),
|
||||
"qualified_consumers": consumers,
|
||||
"event_schema_sha256": event_schema_sha256,
|
||||
"required_event_ids": [event["id"] for event in SPEC["event_schema"]["events"]],
|
||||
"nonqualifying_examples": SPEC["consumer_contract"]["nonqualifying_examples"],
|
||||
}
|
||||
write_json(OUTPUT / "consumer-inventory.json", inventory)
|
||||
write_json(
|
||||
OUTPUT / "event-schema.json",
|
||||
{
|
||||
"schema": 1,
|
||||
"event_schema_sha256": event_schema_sha256,
|
||||
"event_schema": SPEC["event_schema"],
|
||||
},
|
||||
)
|
||||
controls = false_go_controls()
|
||||
write_json(OUTPUT / "false-go-controls.json", controls)
|
||||
|
||||
consumer_present = bool(consumers)
|
||||
conditions = [
|
||||
{
|
||||
"id": "concrete-existing-native-diagnostic-consumer",
|
||||
"status": "PASS" if consumer_present else "STOP",
|
||||
"observation": (
|
||||
f"{len(consumers)} qualified production capture manifest(s)"
|
||||
if consumer_present
|
||||
else "zero qualified consumers; all 33 references are planning, history, tests, or documentation"
|
||||
),
|
||||
},
|
||||
{
|
||||
"id": "no-pointer-credential-or-unauthenticated-metadata-leakage",
|
||||
"status": "NOT_RUN",
|
||||
"reason": "existing-consumer prerequisite is absent",
|
||||
},
|
||||
{
|
||||
"id": "owned-temp-freebsd15-prototype-fields-counts-failure-unload",
|
||||
"status": "NOT_RUN",
|
||||
"reason": "existing-consumer prerequisite is absent; no prototype was generated",
|
||||
},
|
||||
{
|
||||
"id": "disabled-zstdio0-zstdio1-no-new-undefined-symbols",
|
||||
"status": "NOT_RUN",
|
||||
"reason": "existing-consumer prerequisite is absent; no builds were run",
|
||||
},
|
||||
{
|
||||
"id": "ten-run-hot-path-median-regression-at-most-three-percent",
|
||||
"status": "NOT_RUN",
|
||||
"reason": "existing-consumer prerequisite is absent; no benchmark was run",
|
||||
},
|
||||
]
|
||||
if consumer_present:
|
||||
raise GateFailure(
|
||||
"RUNNER_FAIL",
|
||||
"a consumer is now approved; add and run the native privacy/prototype/build/benchmark stages before GO",
|
||||
)
|
||||
if all_conditions_pass([condition["status"] for condition in conditions]):
|
||||
raise GateFailure("RUNNER_FAIL", "false-GO guard accepted incomplete conditions")
|
||||
|
||||
result = {
|
||||
"schema": 1,
|
||||
"gate": "G08",
|
||||
"candidate": "P15-031",
|
||||
"batch": "B38",
|
||||
"status": "STOP",
|
||||
"reason": "no concrete existing project diagnostic consumer captures the predeclared native trace schema",
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"resolved_base": resolved,
|
||||
"start_utc": START_UTC,
|
||||
"end_utc": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||
"deadline_seconds": DEADLINE,
|
||||
"expected_exit_code": 22,
|
||||
"conditions": conditions,
|
||||
"false_go_guard": controls,
|
||||
"consumer_inventory": "consumer-inventory.json",
|
||||
"event_schema": "event-schema.json",
|
||||
"gate_metrics": {
|
||||
"scoped_paths": len(scoped_entries),
|
||||
"native_tracing_reference_paths": len(reference_paths),
|
||||
"qualified_consumers": len(consumers),
|
||||
"predeclared_events": len(SPEC["event_schema"]["events"]),
|
||||
"prototype_runs": 0,
|
||||
"builds": 0,
|
||||
"benchmark_runs": 0,
|
||||
"qemu_runs": 0,
|
||||
},
|
||||
"b38": "STOP-NO-SOURCE",
|
||||
"prototype": "NOT_RUN",
|
||||
"builds": "NOT_RUN",
|
||||
"microbenchmark": "NOT_RUN",
|
||||
"qemu": "NOT_RUN",
|
||||
"tc178": "NOT_RUN",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"smoke_suite": "NOT_RUN",
|
||||
"production_source_changed": False,
|
||||
"protected_pid_addressed": False,
|
||||
"protected_port_addressed": False,
|
||||
"base_image_addressed_or_hashed": False,
|
||||
}
|
||||
write_json(OUTPUT / "result.json", result)
|
||||
print(json.dumps(result, ensure_ascii=True, sort_keys=True))
|
||||
return 22
|
||||
|
||||
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except GateFailure as error:
|
||||
failure = {
|
||||
"schema": 1,
|
||||
"gate": "G08",
|
||||
"candidate": "P15-031",
|
||||
"batch": "B38",
|
||||
"status": error.status,
|
||||
"reason": error.reason,
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"start_utc": START_UTC,
|
||||
"end_utc": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||
"deadline_seconds": DEADLINE,
|
||||
"b38": "NOT_RUN",
|
||||
"prototype": "NOT_RUN",
|
||||
"builds": "NOT_RUN",
|
||||
"microbenchmark": "NOT_RUN",
|
||||
"qemu": "NOT_RUN",
|
||||
"production_source_changed": False,
|
||||
}
|
||||
write_json(OUTPUT / "result.json", failure)
|
||||
print(json.dumps(failure, ensure_ascii=True, sort_keys=True))
|
||||
raise SystemExit({"RUNNER_FAIL": 20, "INFRA_BLOCKED": 21}.get(error.status, 20))
|
||||
PY
|
||||
gate_rc=$?
|
||||
set -e
|
||||
|
||||
cleanup_status=PASS
|
||||
if ! rm -rf -- "$work"; then
|
||||
cleanup_status=FAIL
|
||||
gate_rc=20
|
||||
fi
|
||||
trap - EXIT HUP INT TERM
|
||||
printf 'owned temporary path removed: %s\ncleanup=%s\n' \
|
||||
"$work" "$cleanup_status" >"$output/cleanup.log"
|
||||
|
||||
python3 -B - "$output" "$gate_rc" "$cleanup_status" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
output = Path(sys.argv[1])
|
||||
exit_code = int(sys.argv[2])
|
||||
cleanup = sys.argv[3]
|
||||
if cleanup != "PASS":
|
||||
status = "RUNNER_FAIL"
|
||||
origin = "runner"
|
||||
elif exit_code == 22:
|
||||
status = "STOP"
|
||||
origin = "gate"
|
||||
elif exit_code in (124, 137):
|
||||
status = "INFRA_BLOCKED"
|
||||
origin = "infrastructure"
|
||||
else:
|
||||
status = "RUNNER_FAIL" if exit_code == 20 else "INFRA_BLOCKED"
|
||||
origin = "runner" if exit_code == 20 else "infrastructure"
|
||||
(output / "attempt.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema": 1,
|
||||
"exit_code": exit_code,
|
||||
"status": status,
|
||||
"failure_origin": origin,
|
||||
"cleanup": cleanup,
|
||||
"target_marker": "reached" if status == "STOP" else "not_reached",
|
||||
},
|
||||
ensure_ascii=True,
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
lines = []
|
||||
for path in sorted(output.iterdir(), key=lambda item: item.name):
|
||||
if path.is_file() and path.name != "SHA256SUMS":
|
||||
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
lines.append(f"{digest} {path.name}")
|
||||
(output / "SHA256SUMS").write_text("\n".join(lines) + "\n", encoding="ascii")
|
||||
PY
|
||||
|
||||
printf 'P15-031 gate exit=%s cleanup=%s output=%s\n' \
|
||||
"$gate_rc" "$cleanup_status" "$output"
|
||||
exit "$gate_rc"
|
||||
@@ -0,0 +1,251 @@
|
||||
{
|
||||
"schema": "pre15-gate-v1",
|
||||
"unit": "G09/P15-032",
|
||||
"batch": "B39",
|
||||
"frozen_base": "bd5a09054e5cf89efd4db82aadb051f20b06ebf7",
|
||||
"freebsd_source": {
|
||||
"outer_git_head": "106727738dcfb6c001b46f25363b91cece970085",
|
||||
"identity": "per-contract-file-sha256"
|
||||
},
|
||||
"timeouts_seconds": {
|
||||
"whole_gate": 120,
|
||||
"command": 10,
|
||||
"compile": 20,
|
||||
"model": 10
|
||||
},
|
||||
"syntax_policy": {
|
||||
"vnode": "vnode:/absolute/path",
|
||||
"geom": "geom:/provider-or-device-path",
|
||||
"legacy_geom": "/provider-or-device-path",
|
||||
"ambiguous_regular_path_is_vnode": false,
|
||||
"multidevice": "all sources in one mount must use the same backend kind"
|
||||
},
|
||||
"typed_errno": {
|
||||
"cycle": "EDEADLK",
|
||||
"unsupported_dependency_graph": "EOPNOTSUPP",
|
||||
"changed_or_short_source": "EIO",
|
||||
"dead_forced_source": "ENXIO"
|
||||
},
|
||||
"required_events": [
|
||||
"parse_tagged_source",
|
||||
"namei_lockleaf",
|
||||
"require_vreg",
|
||||
"resolve_vop_getlowvnode",
|
||||
"check_self_identity",
|
||||
"check_namespace_ancestors",
|
||||
"trace_storage_ancestors",
|
||||
"vop_access",
|
||||
"vn_open_vnode_fread",
|
||||
"crhold_mount_cred",
|
||||
"vop_set_text",
|
||||
"register_upper_mount",
|
||||
"snapshot_size",
|
||||
"first_io",
|
||||
"io_complete",
|
||||
"mark_closing",
|
||||
"drain_inflight",
|
||||
"vflush",
|
||||
"unregister_upper_mount",
|
||||
"vop_unset_text",
|
||||
"vn_close",
|
||||
"crfree_mount_cred",
|
||||
"release_last_reference"
|
||||
],
|
||||
"required_invariants": [
|
||||
"explicit_backend_tag",
|
||||
"held_vnode_identity",
|
||||
"held_credential",
|
||||
"held_credential_used_for_read",
|
||||
"kernel_space_read_buffer",
|
||||
"source_range_lock",
|
||||
"source_shared_vnode_lock",
|
||||
"write_exclusion",
|
||||
"size_snapshot_and_short_read_error",
|
||||
"self_check_before_io",
|
||||
"namespace_ancestor_check_before_io",
|
||||
"storage_ancestor_check_before_io",
|
||||
"upper_mount_registration",
|
||||
"inflight_drain_before_release",
|
||||
"same_backend_multidevice",
|
||||
"geom_and_vnode_paths_disjoint",
|
||||
"fault_injection_tc179_constructible",
|
||||
"fault_injection_tc184_constructible"
|
||||
],
|
||||
"lock_edges": [
|
||||
["vfs_mount_busy", "erofs_vnode_lock"],
|
||||
["erofs_vnode_lock", "source_range_lock"],
|
||||
["source_range_lock", "source_vnode_lock"],
|
||||
["source_vnode_lock", "source_vop_read"],
|
||||
["erofs_vm_object_lock", "erofs_vm_object_unlock_before_vop_read"],
|
||||
["erofs_vm_object_unlock_before_vop_read", "erofs_vnode_lock"],
|
||||
["geom_topology_lock", "geom_open_close_only"],
|
||||
["mark_closing", "drain_inflight"],
|
||||
["drain_inflight", "vflush"],
|
||||
["vflush", "unregister_upper_mount"],
|
||||
["unregister_upper_mount", "source_vnode_close"],
|
||||
["source_vnode_close", "credential_release"],
|
||||
["credential_release", "last_reference_release"]
|
||||
],
|
||||
"false_go_controls": [
|
||||
"explicit_backend_tag",
|
||||
"held_vnode_identity",
|
||||
"held_credential",
|
||||
"held_credential_used_for_read",
|
||||
"kernel_space_read_buffer",
|
||||
"source_range_lock",
|
||||
"source_shared_vnode_lock",
|
||||
"write_exclusion",
|
||||
"size_snapshot_and_short_read_error",
|
||||
"self_check_before_io",
|
||||
"namespace_ancestor_check_before_io",
|
||||
"upper_mount_registration",
|
||||
"inflight_drain_before_release",
|
||||
"same_backend_multidevice",
|
||||
"geom_and_vnode_paths_disjoint",
|
||||
"fault_injection_tc179_constructible",
|
||||
"fault_injection_tc184_constructible"
|
||||
],
|
||||
"contracts": [
|
||||
{
|
||||
"root": "freebsd",
|
||||
"path": "sys/kern/vnode_if.src",
|
||||
"sha256": "5de87ff115f543fd89f762c3d356b6799f30757f8a34fd840370ce7fff53b90b",
|
||||
"must_contain": ["%% read\t\tvp\tL L L", "%% getlowvnode vp\t= = =", "%% getpages\tvp\tL L L", "%% set_text\tvp\t= = ="]
|
||||
},
|
||||
{
|
||||
"root": "freebsd",
|
||||
"path": "sys/kern/vfs_vnops.c",
|
||||
"sha256": "3bc9735e2a190fc55134a803f5fc8cb6f020b0b1ce925261774db32789bd7f08",
|
||||
"must_contain": ["vn_rdwr(enum uio_rw rw", "vn_rangelock_rlock(vp", "vn_lock(vp, lock_flags | LK_RETRY);", "VOP_READ(vp, &auio, ioflg, cred);"]
|
||||
},
|
||||
{
|
||||
"root": "freebsd",
|
||||
"path": "sys/kern/vfs_default.c",
|
||||
"sha256": "4286a9bf038c32021f5c116d8ab713b09a5cfa43cf52999bcbf2c0018e7aa177",
|
||||
"must_contain": ["vop_stdset_text(struct vop_set_text_args *ap)", "return (ETXTBSY);", "vop_stdunset_text(struct vop_unset_text_args *ap)", "vop_stdadd_writecount_impl", "vop_stdgetlowvnode(struct vop_getlowvnode_args *ap)"]
|
||||
},
|
||||
{
|
||||
"root": "freebsd",
|
||||
"path": "sys/kern/vfs_mount.c",
|
||||
"sha256": "eecd22aaf63eda940798af02d2d3068a6ed3dc2d90b74a936190a7e56db0a79a",
|
||||
"must_contain": ["vfs_register_upper_from_vp(struct vnode *vp", "ensure that it cannot be unmounted", "KASSERT(ump != mp", "vfs_unregister_upper(struct mount *mp", "registered upper with pending unmount"]
|
||||
},
|
||||
{
|
||||
"root": "freebsd",
|
||||
"path": "sys/sys/mount.h",
|
||||
"sha256": "2bc2017d63389c39dfee52b3041970450ef1eebaa149c8fe5e84a871c0f0b738",
|
||||
"must_contain": ["struct mount_upper_node", "struct vnode\t*mnt_vnodecovered", "void *\t\tmnt_data"],
|
||||
"must_not_contain": ["struct g_provider", "struct g_consumer"]
|
||||
},
|
||||
{
|
||||
"root": "freebsd",
|
||||
"path": "sys/vm/vnode_pager.c",
|
||||
"sha256": "6e2bd8a57bb1c027f303db7bf0e2351fa67af5ca53d6b2b826fa1f12f2c5154e",
|
||||
"must_contain": ["vnode_pager_input_old(vm_object_t object", "VM_OBJECT_WUNLOCK(object);", "auio.uio_segflg = UIO_SYSSPACE;", "VOP_READ(vp, &auio, 0, curthread->td_ucred);"]
|
||||
},
|
||||
{
|
||||
"root": "freebsd",
|
||||
"path": "sys/fs/tarfs/tarfs_vfsops.c",
|
||||
"sha256": "b4c1a5c7f8870c816917706ed0e4dafd790ca5374690833d8daa321aafb696c1",
|
||||
"must_contain": ["vn_open_vnode", "vn_close"]
|
||||
},
|
||||
{
|
||||
"root": "freebsd",
|
||||
"path": "sys/fs/tarfs/tarfs_io.c",
|
||||
"sha256": "0c255ba14df3ff7dcec346dee99c9ddbee376e3483370bb7257dffe2f529fc6c",
|
||||
"must_contain": ["vn_rangelock_rlock", "VOP_READ"]
|
||||
},
|
||||
{
|
||||
"root": "freebsd",
|
||||
"path": "sys/fs/deadfs/dead_vnops.c",
|
||||
"sha256": "1d6864e8cc035ad109ac60c3dab67f31b9c3ad70bee9f9e585b6be10a5657b73",
|
||||
"must_contain": ["dead_read", "return (ENXIO);"]
|
||||
},
|
||||
{
|
||||
"root": "freebsd",
|
||||
"path": "sys/fs/nullfs/null_vnops.c",
|
||||
"sha256": "b10c2138b649343e6c49a9416b6f4f3a0894388059122eefa190430e13695d6c",
|
||||
"must_contain": ["VOP_GETLOWVNODE"]
|
||||
},
|
||||
{
|
||||
"root": "freebsd",
|
||||
"path": "sys/fs/nullfs/null_vfsops.c",
|
||||
"sha256": "f5f8803458522d9cecb1e024bdd599630dde02bc1e0578445204602b4db633bd",
|
||||
"must_contain": ["vfs_register_upper_from_vp", "VFCF_LOOPBACK"]
|
||||
},
|
||||
{
|
||||
"root": "freebsd",
|
||||
"path": "sys/fs/unionfs/union_vnops.c",
|
||||
"sha256": "ce9a41531731170a340e39bfe6afd29f4740d8b705abb6a018176c7a1e159aff",
|
||||
"must_contain": ["VOP_GETLOWVNODE"]
|
||||
},
|
||||
{
|
||||
"root": "freebsd",
|
||||
"path": "sys/fs/unionfs/union_vfsops.c",
|
||||
"sha256": "c181c0134887a0d8697b739dbbf54b7c4299f337bbd4a3e6e1d62a1b6bfb1b51",
|
||||
"must_contain": ["vfs_register_upper_from_vp", "VFCF_LOOPBACK"]
|
||||
},
|
||||
{
|
||||
"root": "freebsd",
|
||||
"path": "sys/dev/md/md.c",
|
||||
"sha256": "a19d34da3d391747fbe2e8bee9fbb02a361202351a0b8bfbec1abc7b5a3f2d4b",
|
||||
"must_contain": ["struct md_s {", "struct vnode *vnode;", "gp->softc = sc;", "g_new_providerf(gp, \"md%d\"", "VOP_READ(vp, &auio, 0, sc->cred);", "sbuf_printf(sb, \" file %s\", mp->s_vnode.file);"]
|
||||
},
|
||||
{
|
||||
"root": "freebsd",
|
||||
"path": "sys/geom/geom.h",
|
||||
"sha256": "22feba6781dcc4e0aae0dea75b83dcae16d077be67853331da35d06edffb0f39",
|
||||
"must_contain": ["struct g_geom", "void\t\t\t*softc;"],
|
||||
"must_not_contain": ["struct vnode"]
|
||||
},
|
||||
{
|
||||
"root": "repo_base",
|
||||
"path": "repo-pre-15/src/internal.h",
|
||||
"sha256": "3fe8d6ca819f0a1c472cc4b173da80027f957b7f7c25e51ef7fd85b60480c98a",
|
||||
"must_contain": ["struct erofs_device_info", "struct g_consumer *cp;"]
|
||||
},
|
||||
{
|
||||
"root": "repo_base",
|
||||
"path": "repo-pre-15/src/super.c",
|
||||
"sha256": "61f31906bb7d1872c99cd3cf3859a34daf29a02d91a3caa1bfc8187af562a3d2",
|
||||
"must_contain": ["erofs_open_device", "g_vfs_open", "erofs_read_superblock"]
|
||||
},
|
||||
{
|
||||
"root": "repo_base",
|
||||
"path": "repo-pre-15/src/data.c",
|
||||
"sha256": "a511146d2d1ff56bc88bae6c550cfdc49a8e978c5c2c88d735de92adaaa836b6",
|
||||
"must_contain": ["erofs_bread_device", "bread(dif->devvp"]
|
||||
},
|
||||
{
|
||||
"root": "repo_base",
|
||||
"path": "repo-pre-15/src/erofs_vnops.c",
|
||||
"sha256": "f28555606ca006d1646a2fee75b98b2f7452d4283c00b087012b8a6c4ef4bf64",
|
||||
"must_contain": ["vnode_pager_local_getpages", "erofs_read_file"]
|
||||
},
|
||||
{
|
||||
"root": "linux_reference",
|
||||
"path": "src-linux/fileio.c",
|
||||
"sha256": "1f00f6cee0060072e8300a2e4f7ca8cd2dc566f129b99d2ed34288b19210e59c",
|
||||
"must_contain": ["erofs_fileio"]
|
||||
},
|
||||
{
|
||||
"root": "linux_reference",
|
||||
"path": "src-linux/super.c",
|
||||
"sha256": "8bda458cca758d8aa9c5a5b05361b2131b896f73fd194f6ad9a8e011e3481bf9",
|
||||
"must_contain": ["s_stack_depth"]
|
||||
}
|
||||
],
|
||||
"blocking_condition": {
|
||||
"id": "FREEBSD-GEOM-VNODE-ANCESTRY-NO-PUBLIC-IDENTITY-EDGE",
|
||||
"classification": "STOP",
|
||||
"summary": "The transitive storage ancestry of a regular source vnode cannot be discovered generically before first I/O.",
|
||||
"counterexample": [
|
||||
"An accepted regular source vnode is on a filesystem consuming an md(4) GEOM provider.",
|
||||
"The md provider services BIO_READ by VOP_READ on md_s.s_vnode.vnode.",
|
||||
"That vnode identity is private md.c softc state; GEOM exposes only void *softc and dumpconf exposes a pathname.",
|
||||
"VOP_GETLOWVNODE and vfs_register_upper_from_vp cover VFS alias/lower mounts only, not GEOM-to-vnode backing edges.",
|
||||
"A pathname cannot close identity across rename/replace, so it cannot implement the required ancestor identity comparison.",
|
||||
"Following gp->softc by duplicating private md_s is an undocumented class-specific assumption and still does not cover other GEOM or filesystem-private file-backed storage."
|
||||
]
|
||||
}
|
||||
}
|
||||
Executable
+380
@@ -0,0 +1,380 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
BASE=bd5a09054e5cf89efd4db82aadb051f20b06ebf7
|
||||
FREEBSD_SRC=/work/build/freebsd-src
|
||||
OUTPUT=
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../../.." && pwd)
|
||||
INPUT=$SCRIPT_DIR/P15-032-input.json
|
||||
|
||||
usage()
|
||||
{
|
||||
printf '%s\n' "usage: $0 --output ABSOLUTE_PATH [--base SHA] [--freebsd-src PATH]" >&2
|
||||
exit 20
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--output)
|
||||
[ "$#" -ge 2 ] || usage
|
||||
OUTPUT=$2
|
||||
shift 2
|
||||
;;
|
||||
--base)
|
||||
[ "$#" -ge 2 ] || usage
|
||||
BASE=$2
|
||||
shift 2
|
||||
;;
|
||||
--freebsd-src)
|
||||
[ "$#" -ge 2 ] || usage
|
||||
FREEBSD_SRC=$2
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -n "$OUTPUT" ] || usage
|
||||
case "$OUTPUT" in
|
||||
/*) ;;
|
||||
*) printf '%s\n' "P15-032: --output must be absolute" >&2; exit 20 ;;
|
||||
esac
|
||||
|
||||
if [ -d "$OUTPUT" ] && [ -n "$(find "$OUTPUT" -mindepth 1 -maxdepth 1 -print -quit)" ]; then
|
||||
printf '%s\n' "P15-032: --output must be empty" >&2
|
||||
exit 20
|
||||
fi
|
||||
mkdir -p "$OUTPUT"
|
||||
TMP=$(mktemp -d "${TMPDIR:-/tmp}/P15-032.XXXXXX")
|
||||
cleanup()
|
||||
{
|
||||
rm -rf "$TMP"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
|
||||
printf 'argv=%s\n' "$0 --output $OUTPUT --base $BASE --freebsd-src $FREEBSD_SRC" >"$OUTPUT/runner.argv"
|
||||
printf 'whole_gate_deadline_seconds=120\n' >"$OUTPUT/deadlines.txt"
|
||||
printf 'command_deadline_seconds=10\n' >>"$OUTPUT/deadlines.txt"
|
||||
printf 'compile_deadline_seconds=20\n' >>"$OUTPUT/deadlines.txt"
|
||||
printf 'model_deadline_seconds=10\n' >>"$OUTPUT/deadlines.txt"
|
||||
|
||||
set +e
|
||||
/usr/bin/timeout 120s python3 - "$ROOT" "$FREEBSD_SRC" "$INPUT" "$OUTPUT" "$TMP" "$BASE" >"$OUTPUT/runner.stdout" 2>"$OUTPUT/runner.stderr" <<'PY'
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
root = Path(sys.argv[1]).resolve()
|
||||
freebsd = Path(sys.argv[2]).resolve()
|
||||
input_path = Path(sys.argv[3]).resolve()
|
||||
output = Path(sys.argv[4]).resolve()
|
||||
tmp = Path(sys.argv[5]).resolve()
|
||||
base = sys.argv[6]
|
||||
cfg = json.loads(input_path.read_text(encoding="utf-8"))
|
||||
command_timeout = cfg["timeouts_seconds"]["command"]
|
||||
commands = []
|
||||
|
||||
def run(argv, cwd, timeout=command_timeout, check=True):
|
||||
started = datetime.datetime.now(datetime.timezone.utc)
|
||||
cp = subprocess.run(argv, cwd=cwd, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, timeout=timeout, check=False)
|
||||
commands.append({
|
||||
"argv": argv,
|
||||
"cwd": str(cwd),
|
||||
"timeout_seconds": timeout,
|
||||
"started_utc": started.isoformat(),
|
||||
"exit": cp.returncode,
|
||||
"stdout": cp.stdout.decode("utf-8", "replace"),
|
||||
"stderr": cp.stderr.decode("utf-8", "replace"),
|
||||
})
|
||||
if check and cp.returncode != 0:
|
||||
raise RuntimeError(f"command failed ({cp.returncode}): {shlex.join(argv)}")
|
||||
return cp
|
||||
|
||||
def sha256(data):
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
def bounded_read(path):
|
||||
data = path.read_bytes()
|
||||
if len(data) > 4 * 1024 * 1024:
|
||||
raise RuntimeError(f"refusing oversized contract file: {path}")
|
||||
return data
|
||||
|
||||
def base_blob(path):
|
||||
return run(["git", "show", f"{base}:{path}"], root).stdout
|
||||
|
||||
def line_of(text, marker):
|
||||
return text[:text.index(marker)].count("\n") + 1
|
||||
|
||||
started = datetime.datetime.now(datetime.timezone.utc)
|
||||
if base != cfg["frozen_base"]:
|
||||
raise RuntimeError("requested base does not match frozen base")
|
||||
head = run(["git", "rev-parse", "HEAD"], root).stdout.decode().strip()
|
||||
remote = run(["git", "rev-parse", "xdm/main"], root).stdout.decode().strip()
|
||||
if head != base or remote != base:
|
||||
raise RuntimeError(f"base mismatch: HEAD={head} xdm/main={remote} expected={base}")
|
||||
|
||||
allowed = {
|
||||
"repo-pre-15/tests/pre15/gates/P15-032.sh",
|
||||
"repo-pre-15/tests/pre15/gates/P15-032-input.json",
|
||||
"repo-pre-15/docs/pre15-stage0/P15-032.md",
|
||||
}
|
||||
status_raw = run(["git", "status", "--porcelain=v1", "-z"], root).stdout
|
||||
entries = [entry for entry in status_raw.decode().split("\0") if entry]
|
||||
paths = set()
|
||||
for entry in entries:
|
||||
path = entry[3:]
|
||||
if " -> " in path:
|
||||
path = path.split(" -> ", 1)[1]
|
||||
paths.add(path)
|
||||
if not paths.issubset(allowed):
|
||||
raise RuntimeError(f"out-of-scope worktree paths: {sorted(paths - allowed)}")
|
||||
|
||||
outer_head = run(["git", "rev-parse", "HEAD"], freebsd).stdout.decode().strip()
|
||||
if outer_head != cfg["freebsd_source"]["outer_git_head"]:
|
||||
raise RuntimeError(f"FreeBSD outer source identity mismatch: {outer_head}")
|
||||
|
||||
contracts = []
|
||||
source_hashes = {}
|
||||
for contract in cfg["contracts"]:
|
||||
kind = contract["root"]
|
||||
rel = contract["path"]
|
||||
if kind == "freebsd":
|
||||
data = bounded_read(freebsd / rel)
|
||||
elif kind == "repo_base":
|
||||
data = base_blob(rel)
|
||||
elif kind == "linux_reference":
|
||||
data = base_blob(rel)
|
||||
else:
|
||||
raise RuntimeError(f"unknown contract root: {kind}")
|
||||
actual = sha256(data)
|
||||
source_hashes[f"{kind}:{rel}"] = actual
|
||||
if actual != contract["sha256"]:
|
||||
raise RuntimeError(f"hash mismatch for {kind}:{rel}: {actual}")
|
||||
text = data.decode("utf-8", "replace")
|
||||
matches = []
|
||||
for marker in contract.get("must_contain", []):
|
||||
if marker not in text:
|
||||
raise RuntimeError(f"missing contract marker in {rel}: {marker!r}")
|
||||
matches.append({"marker": marker, "line": line_of(text, marker)})
|
||||
forbidden = []
|
||||
for marker in contract.get("must_not_contain", []):
|
||||
present = marker in text
|
||||
forbidden.append({"marker": marker, "present": present})
|
||||
if present:
|
||||
raise RuntimeError(f"forbidden bridge marker in {rel}: {marker!r}")
|
||||
contracts.append({"root": kind, "path": rel, "sha256": actual,
|
||||
"matches": matches, "negative_matches": forbidden})
|
||||
|
||||
events = cfg["required_events"]
|
||||
event_index = {name: index for index, name in enumerate(events)}
|
||||
required_order = [
|
||||
("check_self_identity", "first_io"),
|
||||
("check_namespace_ancestors", "first_io"),
|
||||
("trace_storage_ancestors", "first_io"),
|
||||
("crhold_mount_cred", "first_io"),
|
||||
("vop_set_text", "first_io"),
|
||||
("register_upper_mount", "first_io"),
|
||||
("io_complete", "drain_inflight"),
|
||||
("drain_inflight", "unregister_upper_mount"),
|
||||
("unregister_upper_mount", "vn_close"),
|
||||
("vn_close", "crfree_mount_cred"),
|
||||
("crfree_mount_cred", "release_last_reference"),
|
||||
]
|
||||
for before, after in required_order:
|
||||
if event_index[before] >= event_index[after]:
|
||||
raise RuntimeError(f"invalid lifecycle order: {before} !< {after}")
|
||||
|
||||
nodes = set()
|
||||
adj = {}
|
||||
for before, after in cfg["lock_edges"]:
|
||||
nodes.update((before, after))
|
||||
adj.setdefault(before, []).append(after)
|
||||
visiting = set()
|
||||
visited = set()
|
||||
def visit(node):
|
||||
if node in visiting:
|
||||
raise RuntimeError(f"lock graph cycle at {node}")
|
||||
if node in visited:
|
||||
return
|
||||
visiting.add(node)
|
||||
for child in adj.get(node, []):
|
||||
visit(child)
|
||||
visiting.remove(node)
|
||||
visited.add(node)
|
||||
for node in sorted(nodes):
|
||||
visit(node)
|
||||
|
||||
dot = ["digraph P15_032 {", " rankdir=LR;"]
|
||||
for before, after in cfg["lock_edges"]:
|
||||
dot.append(f' "{before}" -> "{after}";')
|
||||
dot.append("}")
|
||||
(output / "lockgraph.dot").write_text("\n".join(dot) + "\n", encoding="utf-8")
|
||||
|
||||
invariants = cfg["required_invariants"]
|
||||
closeable = [name for name in invariants if name != "storage_ancestor_check_before_io"]
|
||||
controls = cfg["false_go_controls"]
|
||||
if set(controls) != set(closeable):
|
||||
raise RuntimeError("false-GO controls do not exactly cover closeable invariants")
|
||||
|
||||
enum_lines = [f" INV_{name.upper()} = 1ULL << {index}," for index, name in enumerate(closeable)]
|
||||
required_mask = " |\n ".join(f"INV_{name.upper()}" for name in closeable)
|
||||
control_rows = ",\n".join(
|
||||
f' {{"{name}", INV_{name.upper()}}}' for name in controls
|
||||
)
|
||||
model = f'''#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
enum invariant {{
|
||||
{os.linesep.join(enum_lines)}
|
||||
}};
|
||||
|
||||
struct scenario {{
|
||||
uint64_t present;
|
||||
int storage_ancestry_identity_api;
|
||||
}};
|
||||
|
||||
struct control {{
|
||||
const char *name;
|
||||
uint64_t bit;
|
||||
}};
|
||||
|
||||
static const uint64_t required =
|
||||
{required_mask};
|
||||
|
||||
static int
|
||||
visible_only_oracle(const struct scenario *scenario)
|
||||
{{
|
||||
return ((scenario->present & required) == required ? 0 : EINVAL);
|
||||
}}
|
||||
|
||||
static int
|
||||
rigorous_oracle(const struct scenario *scenario)
|
||||
{{
|
||||
int error;
|
||||
|
||||
error = visible_only_oracle(scenario);
|
||||
if (error != 0)
|
||||
return (error);
|
||||
if (!scenario->storage_ancestry_identity_api)
|
||||
return (EOPNOTSUPP);
|
||||
return (0);
|
||||
}}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{{
|
||||
static const struct control controls[] = {{
|
||||
{control_rows}
|
||||
}};
|
||||
struct scenario candidate = {{ required, 0 }};
|
||||
struct scenario mutation;
|
||||
size_t index;
|
||||
|
||||
if (visible_only_oracle(&candidate) != 0)
|
||||
return (1);
|
||||
if (rigorous_oracle(&candidate) != EOPNOTSUPP)
|
||||
return (2);
|
||||
puts("hidden-md-edge: visible-only=GO rigorous=STOP:EOPNOTSUPP");
|
||||
for (index = 0; index < sizeof(controls) / sizeof(controls[0]); index++) {{
|
||||
mutation = candidate;
|
||||
mutation.present &= ~controls[index].bit;
|
||||
if (visible_only_oracle(&mutation) == 0 || rigorous_oracle(&mutation) == 0)
|
||||
return (3);
|
||||
printf("false-go-control:%s=REJECTED\\n", controls[index].name);
|
||||
}}
|
||||
puts("lifecycle-model=PASS");
|
||||
return (0);
|
||||
}}
|
||||
'''
|
||||
model_path = tmp / "P15-032-model.c"
|
||||
binary_path = tmp / "P15-032-model"
|
||||
model_path.write_text(model, encoding="utf-8")
|
||||
compile_cp = run(["/usr/bin/timeout", f"{cfg['timeouts_seconds']['compile']}s",
|
||||
"/usr/bin/cc", "-std=c11", "-Wall", "-Wextra", "-Werror",
|
||||
"-O2", "-o", str(binary_path), str(model_path)], tmp,
|
||||
timeout=cfg["timeouts_seconds"]["compile"] + 2)
|
||||
model_cp = run(["/usr/bin/timeout", f"{cfg['timeouts_seconds']['model']}s",
|
||||
str(binary_path)], tmp,
|
||||
timeout=cfg["timeouts_seconds"]["model"] + 2)
|
||||
(output / "owned-temp-model.c").write_bytes(model_path.read_bytes())
|
||||
(output / "owned-temp-model.stdout").write_bytes(model_cp.stdout)
|
||||
(output / "owned-temp-model.stderr").write_bytes(model_cp.stderr)
|
||||
|
||||
false_go = {
|
||||
"controls": [{"removed_invariant": name, "outcome": "REJECTED"} for name in controls],
|
||||
"adversarial_hidden_edge": {
|
||||
"topology": "regular vnode -> filesystem -> md GEOM provider -> private backing vnode",
|
||||
"visible_only_oracle": "GO",
|
||||
"rigorous_oracle": "STOP:EOPNOTSUPP",
|
||||
},
|
||||
}
|
||||
(output / "false-go-controls.json").write_text(json.dumps(false_go, indent=2) + "\n")
|
||||
(output / "contracts.json").write_text(json.dumps(contracts, indent=2) + "\n")
|
||||
(output / "source-hashes.json").write_text(json.dumps(source_hashes, indent=2, sort_keys=True) + "\n")
|
||||
(output / "commands.json").write_text(json.dumps(commands, indent=2) + "\n")
|
||||
|
||||
state = {
|
||||
"events": events,
|
||||
"required_order": required_order,
|
||||
"local_lifecycle": "PASS",
|
||||
"storage_ancestor_identity": "UNAVAILABLE",
|
||||
"typed_errno_if_detectable": cfg["typed_errno"]["cycle"],
|
||||
"typed_errno_for_unsupported_graph": cfg["typed_errno"]["unsupported_dependency_graph"],
|
||||
}
|
||||
(output / "state-model.json").write_text(json.dumps(state, indent=2) + "\n")
|
||||
|
||||
result = {
|
||||
"unit": cfg["unit"],
|
||||
"batch": cfg["batch"],
|
||||
"classification": "GATE_STOP",
|
||||
"decision": "STOP",
|
||||
"exit": 10,
|
||||
"base": base,
|
||||
"head": head,
|
||||
"xdm_main": remote,
|
||||
"freebsd_git_head": outer_head,
|
||||
"freebsd_identity": cfg["freebsd_source"]["identity"],
|
||||
"started_utc": started.isoformat(),
|
||||
"finished_utc": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
"closeable_invariants": closeable,
|
||||
"blocking_condition": cfg["blocking_condition"],
|
||||
"source_changes_permitted": False,
|
||||
"tests_run": ["contract extraction", "owned-temp C lifecycle model", "adversarial false-GO controls"],
|
||||
"tests_omitted": ["D", "H", "K", "Q", "TC006", "TC179", "TC184", "full feature", "smoke"],
|
||||
}
|
||||
(output / "result.json").write_text(json.dumps(result, indent=2) + "\n")
|
||||
|
||||
print("P15-032 STOP: no public identity-preserving VFS/GEOM-to-backing-vnode ancestry API")
|
||||
sys.exit(10)
|
||||
PY
|
||||
RC=$?
|
||||
set -e
|
||||
|
||||
printf 'exit=%s\n' "$RC" >"$OUTPUT/runner.exit"
|
||||
case "$RC" in
|
||||
10)
|
||||
printf '%s\n' GATE_STOP >"$OUTPUT/classification"
|
||||
;;
|
||||
124)
|
||||
printf '%s\n' RUNNER_TIMEOUT >"$OUTPUT/classification"
|
||||
;;
|
||||
0)
|
||||
printf '%s\n' GATE_GO >"$OUTPUT/classification"
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' RUNNER_FAILURE >"$OUTPUT/classification"
|
||||
;;
|
||||
esac
|
||||
(cd "$OUTPUT" && find . -maxdepth 1 -type f ! -name manifest.sha256 -print | LC_ALL=C sort | xargs sha256sum) >"$OUTPUT/manifest.sha256"
|
||||
exit "$RC"
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"benchmark": {
|
||||
"extent_bytes": 262144,
|
||||
"minimum_improved_codecs": 2,
|
||||
"minimum_latency_improvement_percent": 10.0,
|
||||
"random_reads_per_sample": 1024,
|
||||
"read_bytes": 4096,
|
||||
"samples": 5,
|
||||
"seed": 2539541505
|
||||
},
|
||||
"budget": {
|
||||
"global_bytes": 524288,
|
||||
"minimum_decode_work_bytes": 131072,
|
||||
"per_mount_bytes": 262144
|
||||
},
|
||||
"candidate": "P15-038",
|
||||
"gate": "G05",
|
||||
"libraries": {
|
||||
"liblz4": "1.10.0",
|
||||
"liblzma": "5.8.1",
|
||||
"libzstd": "1.5.7",
|
||||
"zlib": "1.3.1"
|
||||
},
|
||||
"lock_order": [
|
||||
"mount-cache",
|
||||
"global-budget"
|
||||
],
|
||||
"protected": {
|
||||
"pid": 26318,
|
||||
"port": 9222
|
||||
},
|
||||
"required_base": "5d6755649a369498a9b257bb2d1d1e3496d5135e",
|
||||
"schema": 1,
|
||||
"scope": "host-codec-cost-oracle-not-guest-vnode-performance",
|
||||
"source_sha256": {
|
||||
"repo-pre-15/src/compress.h": "d4177f5d606489d8c63bb7f1dcaf18bc92dd5d374395ccfa76060b272549f5d2",
|
||||
"repo-pre-15/src/decompressor.c": "ccaa934f837ecbf3b3b678dbddf35d29dd7e3b4e80d1a7ab78b18a07e555419f",
|
||||
"repo-pre-15/src/decompressor_deflate.c": "4a018ce06dc83fcb305130c262d5b59b4d133f698be40a957bebca2cd6a92518",
|
||||
"repo-pre-15/src/decompressor_lz4.c": "ca5e5bd6142f9e3c3ea66849900f05799dbc7d6d1a97e0d5a988c28edb90ea98",
|
||||
"repo-pre-15/src/decompressor_lzma.c": "9935be2f4d5829240052f151b916203c67e830e8b2b7b51c452262a8f92f6157",
|
||||
"repo-pre-15/src/decompressor_zstd.c": "c5f19c9ea6d8238bcf30b8b3228b80e77cf2073df1d07e4fac5f5401bef413bb",
|
||||
"repo-pre-15/src/internal.h": "d33b80c57846437dc0410086c552cb308b8e1a6ce517b5b2ce19779bd59e8848",
|
||||
"repo-pre-15/src/zdata.c": "2c4d624c173ac9fe0cb1e72bb968a7994d0cfd66e4fcd0db04dea10e06ba0fee",
|
||||
"repo-pre-15/tests/pre15/cases/B32-cache-state.sh": "e14453fc628a615acf701f96ec575c884453fe1a62878badbfcefff33656b92d",
|
||||
"repo-pre-15/tests/pre15/fixtures/B32-cache-oracle.c": "2a83b84c560b67cf4f7583e886e1f0dbe792e58bc4b0a8e59e2a07c17ec8f866",
|
||||
"repo-pre-15/tests/pre15/fixtures/B32-qemu-run.sh": "bb1d493a8dc6536751377f3cd72c4012875a41320f570f5259340220b226ed2e"
|
||||
},
|
||||
"states": [
|
||||
"EMPTY",
|
||||
"INFLIGHT",
|
||||
"READY",
|
||||
"FAILED"
|
||||
],
|
||||
"upstream_batches": {
|
||||
"b32_correctness": "c6a502184da6e973b1f0fa2ff8c0290c041e7092",
|
||||
"b32_qemu_timeout_fix": "c4344fc842a180720a56b585191490395044603a"
|
||||
}
|
||||
}
|
||||
Executable
+1109
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"approved_demand_manifest_sha256": {},
|
||||
"batch": "B35",
|
||||
"candidate": "P15-045",
|
||||
"demand_manifest_contract": {
|
||||
"accepted_formats": [
|
||||
"JSON object",
|
||||
"RFC822-style text headers"
|
||||
],
|
||||
"demand_terms": [
|
||||
"deployment",
|
||||
"deploy",
|
||||
"support",
|
||||
"manifest",
|
||||
"policy",
|
||||
"default",
|
||||
"required",
|
||||
"must",
|
||||
"out of the box",
|
||||
"out_of_box",
|
||||
"部署",
|
||||
"支持",
|
||||
"默认",
|
||||
"开箱",
|
||||
"清单"
|
||||
],
|
||||
"maximum_text_candidate_bytes": 2097152,
|
||||
"required_semantics": [
|
||||
"document identifies itself as a support/deployment manifest",
|
||||
"manifest has a concrete version",
|
||||
"manifest names nonempty deployment/support targets",
|
||||
"manifest explicitly requires Zstd enabled by default/out of the box"
|
||||
],
|
||||
"scope": "planning/pre15/evidence at required_base"
|
||||
},
|
||||
"evidence_tracked_path_count": 2560,
|
||||
"evidence_tree_oid": "3ecce7a124d153638853ab5d897126c769125309",
|
||||
"gate": "G07",
|
||||
"probe_order": [
|
||||
"versioned-deployment-demand",
|
||||
"disabled-runtime-exact-EOPNOTSUPP",
|
||||
"enabled-real-reproducible-Zstd-EROFS-read",
|
||||
"FreeBSD-kernel-ZSTDIO-symbol-capability-model",
|
||||
"KLD-size-delta-at-most-64KiB-and-10-percent"
|
||||
],
|
||||
"prohibited_paths": [
|
||||
"planning/pre15/introduction.md"
|
||||
],
|
||||
"protected": {
|
||||
"pid": 26318,
|
||||
"port": 9222
|
||||
},
|
||||
"required_base": "13974efc00c31d8c13c8dccb7c65a82adafcbff6",
|
||||
"schema": 1,
|
||||
"size_thresholds": {
|
||||
"maximum_bytes": 65536,
|
||||
"maximum_percent": 10.0,
|
||||
"require_both": true
|
||||
},
|
||||
"source_sha256": {
|
||||
"planning/pre15/20-final-candidate-ledger.md": "2ba36e8f107cfe5479c4d2bb913519b27e0eb97aa988ecc08e3a7633523620d0",
|
||||
"planning/pre15/30-stage0-gates.md": "34708bacf4a0668dfd834b3639900ca23b28b9833a387ac6e2c0399c9316cdb8",
|
||||
"planning/pre15/40-execution-batches.md": "1d1ffdf8fec799aeec063bb1dc4a0fda9703300d414eabef8905522b2c18db20",
|
||||
"planning/pre15/90-honesty-and-evidence.md": "69f96908f5ab436a2f5359548e4e6f5dc2942fcf00148d999d513558f1c59c2d",
|
||||
"repo-pre-15/README.md": "2d3e138f5af20ce02aa562286cb42df8fbd00fdad0ae424e426e0fd8a7f72472",
|
||||
"repo-pre-15/docs/erofs.5": "dea6a9eddbccbb7b8779b6b4e43772edeea9f0e51696c1b8fd09e4d8e69b52f5",
|
||||
"repo-pre-15/docs/features.md": "6d875eb582ef9f00391f90ee5b4b8aec0429f24f0ae3c7b27e788ab1e2c48f2e",
|
||||
"repo-pre-15/src/Makefile": "b722f7ec658e0ada0640c80a7b527921bd96cdf0e93e9623cece29766f2fc9c8",
|
||||
"repo-pre-15/src/decompressor_zstd.c": "c5f19c9ea6d8238bcf30b8b3228b80e77cf2073df1d07e4fac5f5401bef413bb"
|
||||
}
|
||||
}
|
||||
Executable
+564
@@ -0,0 +1,564 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
umask 022
|
||||
|
||||
gate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||
root=$(CDPATH= cd -- "$gate_dir/../../../.." && pwd -P)
|
||||
input=$gate_dir/P15-045-input.json
|
||||
base=
|
||||
output=
|
||||
deadline=90
|
||||
|
||||
while test "$#" -gt 0; do
|
||||
case "$1" in
|
||||
--base)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 20; }
|
||||
base=$2
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 20; }
|
||||
output=$2
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
printf 'unknown argument: %s\n' "$1" >&2
|
||||
exit 20
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 20; }
|
||||
test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 20; }
|
||||
test -f "$input" || { printf 'missing input: %s\n' "$input" >&2; exit 20; }
|
||||
for tool in git mktemp python3 sha256sum timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
printf 'missing required host tool: %s\n' "$tool" >&2
|
||||
exit 21
|
||||
}
|
||||
done
|
||||
case "$output" in
|
||||
/*) ;;
|
||||
*) output=$PWD/$output ;;
|
||||
esac
|
||||
test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 20; }
|
||||
mkdir -p "$output"
|
||||
work=$(mktemp -d "${TMPDIR:-/tmp}/P15-045.XXXXXX")
|
||||
|
||||
cleanup_trap()
|
||||
{
|
||||
rm -rf -- "$work"
|
||||
}
|
||||
trap cleanup_trap EXIT HUP INT TERM
|
||||
|
||||
python3 -B - "$output/command-argv.json" "$0" --base "$base" --output "$output" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
Path(sys.argv[1]).write_text(
|
||||
json.dumps(sys.argv[2:], ensure_ascii=True, separators=(",", ":")) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
PY
|
||||
python3 -B - "$output/ownership.json" "$work" "$output" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
Path(sys.argv[1]).write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"owned_temporary_paths": [sys.argv[2]],
|
||||
"persistent_output": sys.argv[3],
|
||||
"owned_processes": [],
|
||||
"owned_ports": [],
|
||||
"qemu": "NOT_RUN",
|
||||
},
|
||||
ensure_ascii=True,
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
PY
|
||||
|
||||
set +e
|
||||
timeout -k 5 "$deadline" python3 -B - \
|
||||
"$root" "$input" "$base" "$output" "$deadline" <<'PY' \
|
||||
>"$output/stdout.log" 2>"$output/stderr.log"
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(sys.argv[1])
|
||||
INPUT = Path(sys.argv[2])
|
||||
REQUESTED_BASE = sys.argv[3]
|
||||
OUTPUT = Path(sys.argv[4])
|
||||
DEADLINE = int(sys.argv[5])
|
||||
SPEC = json.loads(INPUT.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
class GateFailure(RuntimeError):
|
||||
def __init__(self, status: str, reason: str):
|
||||
super().__init__(reason)
|
||||
self.status = status
|
||||
self.reason = reason
|
||||
|
||||
|
||||
def sha256_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def write_json(path: Path, value: Any) -> None:
|
||||
path.write_text(
|
||||
json.dumps(value, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
|
||||
|
||||
def run_bytes(argv: list[str], allowed: set[int] | None = None) -> bytes:
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
argv,
|
||||
cwd=ROOT,
|
||||
check=False,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=30,
|
||||
)
|
||||
except subprocess.TimeoutExpired as error:
|
||||
raise GateFailure(
|
||||
"INFRA_BLOCKED", f"command timed out: {' '.join(argv)}"
|
||||
) from error
|
||||
accepted = {0} if allowed is None else allowed
|
||||
if completed.returncode not in accepted:
|
||||
detail = completed.stderr.decode("utf-8", errors="replace").strip()
|
||||
raise GateFailure(
|
||||
"RUNNER_FAIL",
|
||||
f"command failed ({completed.returncode}): {' '.join(argv)}: {detail}",
|
||||
)
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def git_text(*args: str, allowed: set[int] | None = None) -> str:
|
||||
return run_bytes(["git", "-C", str(ROOT), *args], allowed).decode(
|
||||
"utf-8", errors="strict"
|
||||
).strip()
|
||||
|
||||
|
||||
def source_at(commit: str, relative: str) -> bytes:
|
||||
return run_bytes(["git", "-C", str(ROOT), "show", f"{commit}:{relative}"])
|
||||
|
||||
|
||||
def truthy(value: Any) -> bool:
|
||||
if value is True:
|
||||
return True
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in {"true", "yes", "required", "enabled", "1"}
|
||||
if isinstance(value, int):
|
||||
return value == 1
|
||||
return False
|
||||
|
||||
|
||||
def flattened(value: Any, prefix: str = "") -> list[tuple[str, Any]]:
|
||||
records: list[tuple[str, Any]] = []
|
||||
if isinstance(value, dict):
|
||||
for key, child in value.items():
|
||||
path = f"{prefix}.{key}" if prefix else str(key)
|
||||
records.append((path.lower().replace("-", "_"), child))
|
||||
records.extend(flattened(child, path))
|
||||
elif isinstance(value, list):
|
||||
for index, child in enumerate(value):
|
||||
records.extend(flattened(child, f"{prefix}[{index}]"))
|
||||
return records
|
||||
|
||||
|
||||
def json_demand_witness(value: Any) -> dict[str, Any] | None:
|
||||
if not isinstance(value, dict):
|
||||
return None
|
||||
records = flattened(value)
|
||||
kind = any(
|
||||
any(token in key for token in ("manifest_type", "manifest_kind", "document_type"))
|
||||
and isinstance(item, str)
|
||||
and any(token in item.lower() for token in ("support", "deploy"))
|
||||
for key, item in records
|
||||
)
|
||||
version = any(
|
||||
"version" in key and isinstance(item, (str, int)) and str(item).strip()
|
||||
for key, item in records
|
||||
)
|
||||
targets = any(
|
||||
any(token in key for token in ("deployment_target", "support_target", "product_target"))
|
||||
and ((isinstance(item, str) and item.strip()) or (isinstance(item, list) and item))
|
||||
for key, item in records
|
||||
)
|
||||
required = any(
|
||||
"zstd" in key
|
||||
and any(token in key for token in ("default", "required", "out_of_box", "outofthebox"))
|
||||
and truthy(item)
|
||||
for key, item in records
|
||||
)
|
||||
if kind and version and targets and required:
|
||||
return {
|
||||
"format": "json",
|
||||
"manifest_kind": True,
|
||||
"version": True,
|
||||
"deployment_targets": True,
|
||||
"zstd_default_required": True,
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def text_demand_witness(text: str) -> dict[str, Any] | None:
|
||||
fields: dict[str, str] = {}
|
||||
for line in text.splitlines():
|
||||
match = re.fullmatch(r"([A-Za-z][A-Za-z0-9_-]*):[ \t]*(.*)", line)
|
||||
if match is not None:
|
||||
fields[match.group(1).lower().replace("-", "_")] = match.group(2).strip()
|
||||
kind = fields.get("manifest_type", "").lower()
|
||||
version = fields.get("manifest_version", "")
|
||||
targets = fields.get("deployment_targets", "") or fields.get("support_targets", "")
|
||||
required = fields.get("zstd_default_required", "")
|
||||
if any(token in kind for token in ("support", "deploy")) and version and targets and truthy(required):
|
||||
return {
|
||||
"format": "text-header",
|
||||
"manifest_kind": True,
|
||||
"version": True,
|
||||
"deployment_targets": True,
|
||||
"zstd_default_required": True,
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def parse_ls_tree(raw: bytes) -> dict[str, dict[str, Any]]:
|
||||
entries: dict[str, dict[str, Any]] = {}
|
||||
for record in raw.split(b"\0"):
|
||||
if not record:
|
||||
continue
|
||||
metadata, path_bytes = record.split(b"\t", 1)
|
||||
mode, kind, oid, size = metadata.decode("ascii").split()
|
||||
path = path_bytes.decode("utf-8", errors="strict")
|
||||
entries[path] = {
|
||||
"mode": mode,
|
||||
"kind": kind,
|
||||
"oid": oid,
|
||||
"size": int(size),
|
||||
}
|
||||
return entries
|
||||
|
||||
|
||||
def grep_paths(commit: str) -> list[str]:
|
||||
raw = run_bytes(
|
||||
[
|
||||
"git", "-C", str(ROOT), "grep", "-I", "-l", "-i",
|
||||
"-e", "zstd", "-e", "zstandard", commit, "--",
|
||||
"planning/pre15/evidence",
|
||||
],
|
||||
{0, 1},
|
||||
)
|
||||
prefix = f"{commit}:"
|
||||
paths = []
|
||||
for line in raw.decode("utf-8", errors="strict").splitlines():
|
||||
if not line.startswith(prefix):
|
||||
raise GateFailure("RUNNER_FAIL", f"unexpected git grep path: {line}")
|
||||
paths.append(line[len(prefix):])
|
||||
return sorted(paths)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if SPEC.get("schema") != 1 or SPEC.get("candidate") != "P15-045":
|
||||
raise GateFailure("RUNNER_FAIL", "invalid P15-045 input identity")
|
||||
if SPEC.get("gate") != "G07" or SPEC.get("batch") != "B35":
|
||||
raise GateFailure("RUNNER_FAIL", "invalid G07/B35 input identity")
|
||||
resolved = git_text("rev-parse", f"{REQUESTED_BASE}^{{commit}}")
|
||||
if resolved != SPEC["required_base"]:
|
||||
raise GateFailure(
|
||||
"INFRA_BLOCKED",
|
||||
f"P15-045 must replay {SPEC['required_base']}, got {resolved}",
|
||||
)
|
||||
evidence_tree = git_text("rev-parse", f"{resolved}:planning/pre15/evidence")
|
||||
if evidence_tree != SPEC["evidence_tree_oid"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen project evidence tree changed")
|
||||
|
||||
source_hashes = {
|
||||
path: sha256_bytes(source_at(resolved, path))
|
||||
for path in SPEC["source_sha256"]
|
||||
}
|
||||
if source_hashes != SPEC["source_sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen G07 source identity changed")
|
||||
write_json(OUTPUT / "source-sha256.json", source_hashes)
|
||||
addendum_paths = (
|
||||
"repo-pre-15/tests/pre15/gates/P15-045.sh",
|
||||
"repo-pre-15/tests/pre15/gates/P15-045-input.json",
|
||||
"repo-pre-15/docs/pre15-stage0/P15-045.md",
|
||||
)
|
||||
addendum_hashes = {
|
||||
path: sha256_bytes((ROOT / path).read_bytes()) for path in addendum_paths
|
||||
}
|
||||
write_json(OUTPUT / "gate-addendum-sha256.json", addendum_hashes)
|
||||
|
||||
tree_entries = parse_ls_tree(
|
||||
run_bytes(
|
||||
[
|
||||
"git", "-C", str(ROOT), "ls-tree", "-r", "--long", "-z",
|
||||
resolved, "--", "planning/pre15/evidence",
|
||||
]
|
||||
)
|
||||
)
|
||||
if len(tree_entries) != SPEC["evidence_tracked_path_count"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen evidence path count changed")
|
||||
prohibited = set(SPEC["prohibited_paths"])
|
||||
if prohibited.intersection(tree_entries):
|
||||
raise GateFailure("RUNNER_FAIL", "prohibited path entered evidence scan")
|
||||
|
||||
zstd_paths = grep_paths(resolved)
|
||||
maximum_bytes = SPEC["demand_manifest_contract"]["maximum_text_candidate_bytes"]
|
||||
demand_terms = tuple(SPEC["demand_manifest_contract"]["demand_terms"])
|
||||
candidates = []
|
||||
qualified = []
|
||||
oversized = []
|
||||
for path in zstd_paths:
|
||||
entry = tree_entries[path]
|
||||
if entry["size"] > maximum_bytes:
|
||||
oversized.append({"path": path, **entry})
|
||||
continue
|
||||
data = source_at(resolved, path)
|
||||
text = data.decode("utf-8", errors="replace")
|
||||
lowered = text.lower()
|
||||
signals = sorted(term for term in demand_terms if term.lower() in lowered)
|
||||
if not signals:
|
||||
continue
|
||||
witness = None
|
||||
if path.endswith(".json"):
|
||||
try:
|
||||
witness = json_demand_witness(json.loads(text))
|
||||
except json.JSONDecodeError:
|
||||
witness = None
|
||||
if witness is None:
|
||||
witness = text_demand_witness(text)
|
||||
record = {
|
||||
"path": path,
|
||||
"blob_oid": entry["oid"],
|
||||
"bytes": entry["size"],
|
||||
"sha256": sha256_bytes(data),
|
||||
"demand_signals": signals,
|
||||
"qualification": witness,
|
||||
"disposition": (
|
||||
"QUALIFIED versioned deployment-demand manifest"
|
||||
if witness is not None
|
||||
else "test/build/planning evidence; no explicit versioned deployment demand"
|
||||
),
|
||||
}
|
||||
candidates.append(record)
|
||||
if witness is not None:
|
||||
qualified.append(record)
|
||||
|
||||
if oversized:
|
||||
write_json(OUTPUT / "oversized-candidates.json", oversized)
|
||||
raise GateFailure(
|
||||
"INFRA_BLOCKED", "a Zstd evidence candidate exceeds the bounded review size"
|
||||
)
|
||||
|
||||
approved = SPEC["approved_demand_manifest_sha256"]
|
||||
actual_approved = {record["path"]: record["sha256"] for record in qualified}
|
||||
if actual_approved != approved:
|
||||
raise GateFailure(
|
||||
"INFRA_BLOCKED", "qualified demand inventory differs from frozen gate input"
|
||||
)
|
||||
|
||||
makefile = source_at(resolved, "repo-pre-15/src/Makefile").decode("utf-8")
|
||||
zstd_source = source_at(
|
||||
resolved, "repo-pre-15/src/decompressor_zstd.c"
|
||||
).decode("utf-8")
|
||||
readme = source_at(resolved, "repo-pre-15/README.md").decode("utf-8")
|
||||
manual = source_at(resolved, "repo-pre-15/docs/erofs.5").decode("utf-8")
|
||||
policy_checks = {
|
||||
"makefile_default_is_opt_in_zero": makefile.count("WITH_ZSTDIO?= 0") == 1,
|
||||
"enabled_build_defines_zstdio": "CFLAGS.decompressor_zstd.c+= -DZSTDIO" in makefile,
|
||||
"disabled_stub_returns_eopnotsupp": (
|
||||
"#else\nstatic int\nz_erofs_zstd_decompress" in zstd_source
|
||||
and "return (EOPNOTSUPP);\n}\n#endif" in zstd_source
|
||||
),
|
||||
"readme_names_kernel_option": "kernel built with `options ZSTDIO`" in readme,
|
||||
"manual_names_kernel_option": '.Cd "options ZSTDIO"' in manual,
|
||||
}
|
||||
if not all(policy_checks.values()):
|
||||
raise GateFailure("INFRA_BLOCKED", "current opt-in ZSTDIO contract changed")
|
||||
|
||||
path_inventory = "\0".join(zstd_paths).encode("utf-8")
|
||||
candidate_inventory = "\0".join(
|
||||
record["path"] for record in candidates
|
||||
).encode("utf-8")
|
||||
demand_scan = {
|
||||
"schema": 1,
|
||||
"base": resolved,
|
||||
"evidence_tree_oid": evidence_tree,
|
||||
"tracked_path_count": len(tree_entries),
|
||||
"zstd_text_path_count": len(zstd_paths),
|
||||
"zstd_text_path_inventory_sha256": sha256_bytes(path_inventory),
|
||||
"demand_candidate_count": len(candidates),
|
||||
"demand_candidate_path_inventory_sha256": sha256_bytes(candidate_inventory),
|
||||
"qualified_manifest_count": len(qualified),
|
||||
"qualified_manifests": qualified,
|
||||
"candidates": candidates,
|
||||
"contract": SPEC["demand_manifest_contract"],
|
||||
"prohibited_path_read": False,
|
||||
}
|
||||
write_json(OUTPUT / "demand-scan.json", demand_scan)
|
||||
|
||||
demand_present = bool(qualified)
|
||||
conditions = [
|
||||
{
|
||||
"id": "versioned-deployment-demand",
|
||||
"status": "PASS" if demand_present else "STOP",
|
||||
"observation": (
|
||||
f"{len(qualified)} qualified manifest(s)"
|
||||
if demand_present
|
||||
else "zero qualifying manifests in the frozen project evidence tree"
|
||||
),
|
||||
},
|
||||
{
|
||||
"id": "disabled-runtime-exact-EOPNOTSUPP",
|
||||
"status": "NOT_RUN",
|
||||
"reason": "deployment-demand prerequisite is absent",
|
||||
},
|
||||
{
|
||||
"id": "enabled-real-reproducible-Zstd-EROFS-read",
|
||||
"status": "NOT_RUN",
|
||||
"reason": "deployment-demand prerequisite is absent",
|
||||
},
|
||||
{
|
||||
"id": "FreeBSD-kernel-ZSTDIO-symbol-capability-model",
|
||||
"status": "NOT_RUN",
|
||||
"reason": "deployment-demand prerequisite is absent; no dependency KLD is claimed",
|
||||
},
|
||||
{
|
||||
"id": "KLD-size-delta-at-most-64KiB-and-10-percent",
|
||||
"status": "NOT_RUN",
|
||||
"reason": "deployment-demand prerequisite is absent",
|
||||
},
|
||||
]
|
||||
all_go = all(item["status"] == "PASS" for item in conditions)
|
||||
if all_go:
|
||||
raise GateFailure(
|
||||
"RUNNER_FAIL", "false-GO guard: downstream runtime probes were not executed"
|
||||
)
|
||||
result = {
|
||||
"schema": 1,
|
||||
"gate": "G07",
|
||||
"candidate": "P15-045",
|
||||
"batch": "B35",
|
||||
"status": "STOP",
|
||||
"reason": "no concrete versioned support/deployment manifest requires Zstd enabled out of the box",
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"resolved_base": resolved,
|
||||
"deadline_seconds": DEADLINE,
|
||||
"expected_exit_code": 22,
|
||||
"conditions": conditions,
|
||||
"false_go_guard": "GO requires every condition PASS; evaluated false",
|
||||
"current_policy_static_checks": policy_checks,
|
||||
"with_zstdio_default": 0,
|
||||
"b35": "STOP-NO-SOURCE",
|
||||
"builds": "NOT_RUN",
|
||||
"qemu": "NOT_RUN",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"smoke_suite": "NOT_RUN",
|
||||
"generic_dependency_kld_claimed": False,
|
||||
"production_source_changed": False,
|
||||
}
|
||||
write_json(OUTPUT / "result.json", result)
|
||||
print(json.dumps(result, ensure_ascii=True, sort_keys=True))
|
||||
return 22
|
||||
|
||||
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except GateFailure as error:
|
||||
failure = {
|
||||
"schema": 1,
|
||||
"gate": "G07",
|
||||
"candidate": "P15-045",
|
||||
"status": error.status,
|
||||
"reason": error.reason,
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"deadline_seconds": DEADLINE,
|
||||
"b35": "NOT_RUN",
|
||||
"qemu": "NOT_RUN",
|
||||
"production_source_changed": False,
|
||||
}
|
||||
write_json(OUTPUT / "result.json", failure)
|
||||
print(json.dumps(failure, ensure_ascii=True, sort_keys=True))
|
||||
raise SystemExit({"RUNNER_FAIL": 20, "INFRA_BLOCKED": 21}.get(error.status, 20))
|
||||
PY
|
||||
gate_rc=$?
|
||||
set -e
|
||||
|
||||
cleanup_status=PASS
|
||||
if ! rm -rf -- "$work"; then
|
||||
cleanup_status=FAIL
|
||||
gate_rc=20
|
||||
fi
|
||||
trap - EXIT HUP INT TERM
|
||||
printf 'owned temporary path removed: %s\ncleanup=%s\n' \
|
||||
"$work" "$cleanup_status" >"$output/cleanup.log"
|
||||
|
||||
python3 -B - "$output" "$gate_rc" "$cleanup_status" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
output = Path(sys.argv[1])
|
||||
exit_code = int(sys.argv[2])
|
||||
cleanup = sys.argv[3]
|
||||
if cleanup != "PASS":
|
||||
status = "RUNNER_FAIL"
|
||||
origin = "runner"
|
||||
elif exit_code == 22:
|
||||
status = "STOP"
|
||||
origin = "gate"
|
||||
elif exit_code in (124, 137):
|
||||
status = "INFRA_BLOCKED"
|
||||
origin = "infrastructure"
|
||||
else:
|
||||
status = "RUNNER_FAIL"
|
||||
origin = "runner"
|
||||
(output / "attempt.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema": 1,
|
||||
"exit_code": exit_code,
|
||||
"status": status,
|
||||
"failure_origin": origin,
|
||||
"cleanup": cleanup,
|
||||
"target_marker": "reached" if status == "STOP" else "not_reached",
|
||||
},
|
||||
ensure_ascii=True,
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
) + "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
lines = []
|
||||
for path in sorted(output.iterdir(), key=lambda item: item.name):
|
||||
if path.is_file() and path.name != "SHA256SUMS":
|
||||
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
lines.append(f"{digest} {path.name}")
|
||||
(output / "SHA256SUMS").write_text("\n".join(lines) + "\n", encoding="ascii")
|
||||
PY
|
||||
|
||||
printf 'P15-045 gate exit=%s cleanup=%s output=%s\n' \
|
||||
"$gate_rc" "$cleanup_status" "$output"
|
||||
exit "$gate_rc"
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"gate": "G06",
|
||||
"candidate": "P15-052",
|
||||
"required_base": "ca7bb4fe6b33e4a1bdf423801134b0ed6bda86dd",
|
||||
"title": "exact on-disk and ABI errno taxonomy",
|
||||
"source_sha256": {
|
||||
"repo-pre-15/src/data.c": "cb22072bd4c092aa6a5376add8c8d9d6f94297f6292900ecc2eef9b01c69e92a",
|
||||
"repo-pre-15/src/erofs_fs.h": "0a49ac30ecbcea020c3909beb972ac4287ca704ebc49a9dccfcd6827884589e1",
|
||||
"repo-pre-15/src/inode.c": "ba6f77ddffa35cc1dd69ebb933fb16f8fa654f94552fd1d3f0255eb790673e6c",
|
||||
"repo-pre-15/src/internal.h": "c08ce3dbcafca5c341193c1670516e9bd5815c5f14ce2d637dbf6cfe5a74e972",
|
||||
"repo-pre-15/src/super.c": "8bb36e9aba15eab2ed10ebec65dccb52dc9ddb485a1a39df37f86f97502c674f",
|
||||
"repo-pre-15/src/xattr.c": "3498af0b547331b9a22babe96ea718ca2ba56ee669001bfdc2dbaab0a9dc42a9",
|
||||
"src-linux/data.c": "8625cdc01e5405f856178ae8fd559696ae85f607f19caf229b867a3b7479318a",
|
||||
"src-linux/internal.h": "4aa671896ff7c0ad32a9108c818ef62d16841c116706fdad391c40a530c81405",
|
||||
"src-linux/super.c": "8bda458cca758d8aa9c5a5b05361b2131b896f73fd194f6ad9a8e011e3481bf9",
|
||||
"src-linux/xattr.c": "c8394e5f6301225cbe7587f223485a368348eac7596c1ab0bbf965c99655ed4a"
|
||||
},
|
||||
"function_errno_counts": {
|
||||
"repo-pre-15/src/data.c:erofs_map_blocks_chunk": {
|
||||
"EOVERFLOW": 6
|
||||
},
|
||||
"repo-pre-15/src/xattr.c:erofs_xattr_backing_size": {
|
||||
"EOVERFLOW": 1
|
||||
},
|
||||
"repo-pre-15/src/xattr.c:erofs_xattr_read_backing": {
|
||||
"EOVERFLOW": 1
|
||||
},
|
||||
"repo-pre-15/src/xattr.c:erofs_xattr_read_metadata": {
|
||||
"EOVERFLOW": 2
|
||||
},
|
||||
"repo-pre-15/src/xattr.c:erofs_xattr_shared_entry_offset": {
|
||||
"EOVERFLOW": 2
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"id": "data.chunk.inode_plus_isize",
|
||||
"path": "repo-pre-15/src/data.c",
|
||||
"function": "erofs_map_blocks_chunk",
|
||||
"field": "inode_off",
|
||||
"needle": "vi->inode_off > UINT64_MAX - vi->inode_isize",
|
||||
"prototype_mutation": "inode_off=UINT64_MAX-31"
|
||||
},
|
||||
{
|
||||
"id": "data.chunk.isize_plus_xattr",
|
||||
"path": "repo-pre-15/src/data.c",
|
||||
"function": "erofs_map_blocks_chunk",
|
||||
"field": "xattr_isize",
|
||||
"needle": "vi->inode_off + vi->inode_isize > UINT64_MAX - vi->xattr_isize",
|
||||
"prototype_mutation": "xattr_isize=128 with inode_off near UINT64_MAX"
|
||||
},
|
||||
{
|
||||
"id": "data.chunk.align",
|
||||
"path": "repo-pre-15/src/data.c",
|
||||
"function": "erofs_map_blocks_chunk",
|
||||
"field": "inode_off",
|
||||
"needle": "idx_base > UINT64_MAX - (entry_size - 1)",
|
||||
"prototype_mutation": "inode_off=UINT64_MAX-3"
|
||||
},
|
||||
{
|
||||
"id": "data.chunk.index_multiply",
|
||||
"path": "repo-pre-15/src/data.c",
|
||||
"function": "erofs_map_blocks_chunk",
|
||||
"field": "chunk_idx",
|
||||
"needle": "chunk_idx > (UINT64_MAX - idx_base) / entry_size",
|
||||
"prototype_mutation": "chunk_idx=(UINT64_MAX-4096)/8+1"
|
||||
},
|
||||
{
|
||||
"id": "data.chunk.image_size_shift",
|
||||
"path": "repo-pre-15/src/data.c",
|
||||
"function": "erofs_map_blocks_chunk",
|
||||
"field": "blocks",
|
||||
"needle": "sbi->blocks > (UINT64_MAX >> sbi->blkszbits)",
|
||||
"prototype_mutation": "blocks=(UINT64_MAX>>12)+1"
|
||||
},
|
||||
{
|
||||
"id": "data.chunk.physical_shift",
|
||||
"path": "repo-pre-15/src/data.c",
|
||||
"function": "erofs_map_blocks_chunk",
|
||||
"field": "blkaddr",
|
||||
"needle": "blkaddr > (UINT64_MAX >> sbi->blkszbits)",
|
||||
"prototype_mutation": "blkaddr=(UINT64_MAX>>12)+1"
|
||||
},
|
||||
{
|
||||
"id": "data.chunk.physical_plus_offset",
|
||||
"path": "repo-pre-15/src/data.c",
|
||||
"function": "erofs_map_blocks_chunk",
|
||||
"field": "chunk_off",
|
||||
"needle": "chunk_off > UINT64_MAX - map->m_pa",
|
||||
"prototype_mutation": "chunk_off=4096 with blkaddr=UINT64_MAX>>12"
|
||||
},
|
||||
{
|
||||
"id": "xattr.backing_size.shift",
|
||||
"path": "repo-pre-15/src/xattr.c",
|
||||
"function": "erofs_xattr_backing_size",
|
||||
"field": "blocks",
|
||||
"needle": "sbi->blocks > (UINT64_MAX >> sbi->blkszbits)",
|
||||
"prototype_mutation": "blocks=(UINT64_MAX>>12)+1"
|
||||
},
|
||||
{
|
||||
"id": "xattr.metadata.align",
|
||||
"path": "repo-pre-15/src/xattr.c",
|
||||
"function": "erofs_xattr_read_metadata",
|
||||
"field": "offp",
|
||||
"needle": "*offp > UINT64_MAX - (sizeof(struct erofs_xattr_entry) - 1)",
|
||||
"prototype_mutation": "offp=UINT64_MAX-2"
|
||||
},
|
||||
{
|
||||
"id": "xattr.metadata.header_add",
|
||||
"path": "repo-pre-15/src/xattr.c",
|
||||
"function": "erofs_xattr_read_metadata",
|
||||
"field": "aligned_off",
|
||||
"needle": "off > UINT64_MAX - sizeof(raw_len)",
|
||||
"prototype_mutation": "aligned_off=UINT64_MAX-1"
|
||||
},
|
||||
{
|
||||
"id": "xattr.shared.base_shift",
|
||||
"path": "repo-pre-15/src/xattr.c",
|
||||
"function": "erofs_xattr_shared_entry_offset",
|
||||
"field": "xattr_blkaddr",
|
||||
"needle": "sbi->xattr_blkaddr > (UINT64_MAX >> sbi->blkszbits)",
|
||||
"prototype_mutation": "xattr_blkaddr=(UINT64_MAX>>12)+1"
|
||||
},
|
||||
{
|
||||
"id": "xattr.shared.base_plus_relative",
|
||||
"path": "repo-pre-15/src/xattr.c",
|
||||
"function": "erofs_xattr_shared_entry_offset",
|
||||
"field": "shared_id",
|
||||
"needle": "relative > UINT64_MAX - base",
|
||||
"prototype_mutation": "shared_id=1024 with base near UINT64_MAX"
|
||||
}
|
||||
],
|
||||
"preservation_controls": [
|
||||
{
|
||||
"id": "abi.off_gt_int64",
|
||||
"freebsd": "+EOVERFLOW",
|
||||
"linux": "-EOVERFLOW semantic",
|
||||
"result": "blocked first by backing-size range for every mounted primary provider"
|
||||
},
|
||||
{
|
||||
"id": "corruption.range_past_eof",
|
||||
"freebsd": "+EINTEGRITY",
|
||||
"linux": "-EFSCORRUPTED semantic",
|
||||
"result": "preserve"
|
||||
},
|
||||
{
|
||||
"id": "eof.zero_length",
|
||||
"freebsd": "0",
|
||||
"linux": "0",
|
||||
"result": "preserve"
|
||||
},
|
||||
{
|
||||
"id": "io.provider_error",
|
||||
"freebsd": "+EIO",
|
||||
"linux": "negative PTR_ERR",
|
||||
"result": "preserve exact provider error"
|
||||
},
|
||||
{
|
||||
"id": "io.provider_short_media",
|
||||
"freebsd": "+ENXIO",
|
||||
"linux": "negative provider error",
|
||||
"result": "preserve exact media-size error"
|
||||
},
|
||||
{
|
||||
"id": "short_read.logical_backing",
|
||||
"freebsd": "+EINTEGRITY before I/O",
|
||||
"linux": "-EFSCORRUPTED semantic",
|
||||
"result": "preserve"
|
||||
},
|
||||
{
|
||||
"id": "unsupported.exact_xattr_header",
|
||||
"freebsd": "+EOPNOTSUPP",
|
||||
"linux": "-EOPNOTSUPP",
|
||||
"result": "preserve"
|
||||
},
|
||||
{
|
||||
"id": "allocation.xattr_prefixes",
|
||||
"freebsd": "+ENOMEM",
|
||||
"linux": "-ENOMEM",
|
||||
"result": "out of B20 scope"
|
||||
}
|
||||
],
|
||||
"prototype_identity": {
|
||||
"path": "/work/pre15-gate-prep-20260814T125129Z/G06",
|
||||
"errno_model.py": "dca427acf5c919f78d46ab8dbb198778fbe2af0869043d93b4eddf9f03814a4e",
|
||||
"corpus/errno-vectors.json": "dde22db91f7fa2613631b4acb75a5d60e255a1c1564be6800db377922a6a7d4e",
|
||||
"input.json": "0835defd288cb6a73337c116c4148daee94b78c435732032b5de40d1b1559715",
|
||||
"oracle.md": "62e6703e8051b832108b4ea39ece232f06aea5dcfb221d7e8c17faf7ae527208",
|
||||
"replay-base.sh": "2162fb645693ce740563a2406ea28ab86dc116a33163e026fc4d491c2df3e8a5"
|
||||
},
|
||||
"decision": {
|
||||
"go": "every target is independently reachable from one validated on-disk field mutation, reaches its named source branch, and has one unique source class",
|
||||
"stop": "any target is unreachable after current decode/mount bounds, any prototype mutation injects a local rather than an on-disk input, or any source class is ambiguous",
|
||||
"atomicity": "P15-052 has no partial source implementation"
|
||||
},
|
||||
"timeouts": {
|
||||
"host_seconds": 240,
|
||||
"qemu_seconds": 1200
|
||||
},
|
||||
"qemu_required_after_host_stop": false,
|
||||
"full_feature_suite_required": false
|
||||
}
|
||||
Executable
+472
@@ -0,0 +1,472 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
gate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||
root=$(CDPATH= cd -- "$gate_dir/../../../.." && pwd -P)
|
||||
input=$gate_dir/P15-052-input.json
|
||||
freebsd_src=${FREEBSD_SRC:-/work/build/freebsd-src}
|
||||
base=
|
||||
output=
|
||||
|
||||
while test "$#" -gt 0; do
|
||||
case "$1" in
|
||||
--base)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 2; }
|
||||
base=$2
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 2; }
|
||||
output=$2
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
printf 'unknown argument: %s\n' "$1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 2; }
|
||||
test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 2; }
|
||||
test -f "$input" || { printf 'missing input: %s\n' "$input" >&2; exit 2; }
|
||||
test -d "$freebsd_src/sys" || {
|
||||
printf 'missing FreeBSD source tree: %s\n' "$freebsd_src" >&2
|
||||
exit 2
|
||||
}
|
||||
for tool in git python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
printf 'missing required host tool: %s\n' "$tool" >&2
|
||||
exit 2
|
||||
}
|
||||
done
|
||||
case "$output" in
|
||||
/*) ;;
|
||||
*) output=$PWD/$output ;;
|
||||
esac
|
||||
test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 2; }
|
||||
mkdir -p "$output"
|
||||
|
||||
python3 - "$root" "$input" "$base" "$output" "$freebsd_src" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(sys.argv[1])
|
||||
INPUT = Path(sys.argv[2])
|
||||
REQUESTED_BASE = sys.argv[3]
|
||||
OUTPUT = Path(sys.argv[4])
|
||||
FREEBSD_SRC = Path(sys.argv[5])
|
||||
SPEC = json.loads(INPUT.read_text(encoding="ascii"))
|
||||
|
||||
U32_MAX = (1 << 32) - 1
|
||||
U48_MAX = (1 << 48) - 1
|
||||
U64_MAX = (1 << 64) - 1
|
||||
I64_MAX = (1 << 63) - 1
|
||||
|
||||
|
||||
def git(*args: str) -> str:
|
||||
return subprocess.check_output(
|
||||
["git", "-C", str(ROOT), *args], text=True
|
||||
).strip()
|
||||
|
||||
|
||||
def sha256(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def source_at(commit: str, path: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(ROOT), "show", f"{commit}:{path}"],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"cannot read {path} at {commit}: {completed.stderr}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def extract_function(source: str, name: str) -> str:
|
||||
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
|
||||
if match is None:
|
||||
raise SystemExit(f"function not found: {name}")
|
||||
start = source.rfind("\n\n", 0, match.start()) + 2
|
||||
brace = source.find("{", match.end())
|
||||
if brace < 0:
|
||||
raise SystemExit(f"function body not found: {name}")
|
||||
depth = 0
|
||||
state = "code"
|
||||
index = brace
|
||||
while index < len(source):
|
||||
char = source[index]
|
||||
following = source[index + 1] if index + 1 < len(source) else ""
|
||||
if state == "code":
|
||||
if char == "/" and following == "*":
|
||||
state = "block"
|
||||
index += 2
|
||||
continue
|
||||
if char == "/" and following == "/":
|
||||
state = "line"
|
||||
index += 2
|
||||
continue
|
||||
if char == '"':
|
||||
state = "string"
|
||||
elif char == "'":
|
||||
state = "character"
|
||||
elif char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1] + "\n"
|
||||
elif state == "block" and char == "*" and following == "/":
|
||||
state = "code"
|
||||
index += 2
|
||||
continue
|
||||
elif state == "line" and char == "\n":
|
||||
state = "code"
|
||||
elif state in {"string", "character"}:
|
||||
if char == "\\":
|
||||
index += 2
|
||||
continue
|
||||
if (state == "string" and char == '"') or (
|
||||
state == "character" and char == "'"
|
||||
):
|
||||
state = "code"
|
||||
index += 1
|
||||
raise SystemExit(f"unterminated function: {name}")
|
||||
|
||||
|
||||
def line_number(source: str, needle: str) -> int:
|
||||
if source.count(needle) != 1:
|
||||
raise SystemExit(
|
||||
f"source needle must occur exactly once ({source.count(needle)}): {needle}"
|
||||
)
|
||||
return source.count("\n", 0, source.index(needle)) + 1
|
||||
|
||||
|
||||
if SPEC.get("schema") != 1 or SPEC.get("candidate") != "P15-052":
|
||||
raise SystemExit("invalid P15-052 gate input")
|
||||
resolved = git("rev-parse", f"{REQUESTED_BASE}^{{commit}}")
|
||||
if resolved != SPEC["required_base"]:
|
||||
raise SystemExit(
|
||||
f"P15-052 must replay {SPEC['required_base']}, got {resolved}"
|
||||
)
|
||||
|
||||
sources = {
|
||||
path: source_at(resolved, path) for path in SPEC["source_sha256"]
|
||||
}
|
||||
source_hashes = {
|
||||
path: sha256(text.encode("utf-8")) for path, text in sources.items()
|
||||
}
|
||||
for path, expected in SPEC["source_sha256"].items():
|
||||
if source_hashes[path] != expected:
|
||||
raise SystemExit(f"frozen source identity mismatch: {path}")
|
||||
|
||||
function_bodies: dict[str, str] = {}
|
||||
for key, counts in SPEC["function_errno_counts"].items():
|
||||
path, function = key.rsplit(":", 1)
|
||||
body = extract_function(sources[path], function)
|
||||
function_bodies[key] = body
|
||||
for errno_name, expected_count in counts.items():
|
||||
actual_count = body.count(f"return ({errno_name});")
|
||||
if actual_count != expected_count:
|
||||
raise SystemExit(
|
||||
f"{key} {errno_name} count changed: {actual_count} != {expected_count}"
|
||||
)
|
||||
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", body):
|
||||
raise SystemExit(f"negative errno entered FreeBSD function: {key}")
|
||||
|
||||
param_path = FREEBSD_SRC / "sys/amd64/include/param.h"
|
||||
types_path = FREEBSD_SRC / "sys/sys/_types.h"
|
||||
geom_path = FREEBSD_SRC / "sys/geom/geom.h"
|
||||
for path in (param_path, types_path, geom_path):
|
||||
if not path.is_file():
|
||||
raise SystemExit(f"missing FreeBSD contract source: {path}")
|
||||
param_source = param_path.read_text(encoding="utf-8")
|
||||
types_source = types_path.read_text(encoding="utf-8")
|
||||
geom_source = geom_path.read_text(encoding="utf-8")
|
||||
page_match = re.search(r"^#define\s+PAGE_SHIFT\s+(\d+)\b", param_source, re.MULTILINE)
|
||||
if page_match is None:
|
||||
raise SystemExit("PAGE_SHIFT is absent from amd64 param.h")
|
||||
page_shift = int(page_match.group(1))
|
||||
if page_shift != 12:
|
||||
raise SystemExit(f"unexpected audited amd64 PAGE_SHIFT: {page_shift}")
|
||||
if "typedef\t__int64_t\t__off_t;" not in types_source:
|
||||
raise SystemExit("FreeBSD off_t source is not the audited signed 64-bit type")
|
||||
if "off_t\t\t\tmediasize;" not in geom_source:
|
||||
raise SystemExit("GEOM mediasize is not the audited off_t field")
|
||||
|
||||
freebsd_head = subprocess.check_output(
|
||||
["git", "-C", str(FREEBSD_SRC), "rev-parse", "HEAD"], text=True
|
||||
).strip()
|
||||
freebsd_contract = {
|
||||
"head": freebsd_head,
|
||||
"page_shift": page_shift,
|
||||
"off_t": "signed-64",
|
||||
"geom_mediasize": "off_t",
|
||||
"files": {
|
||||
str(path.relative_to(FREEBSD_SRC)): sha256(path.read_bytes())
|
||||
for path in (param_path, types_path, geom_path)
|
||||
},
|
||||
}
|
||||
|
||||
# These are maxima after the current super/inode decode and mount-time media
|
||||
# checks, not unconstrained C-structure values.
|
||||
inode_slot_min = 32
|
||||
inode_isize_max = 64
|
||||
xattr_isize_max = 4 + 4 * ((1 << 16) - 2)
|
||||
inode_off_max = I64_MAX - inode_slot_min
|
||||
idx_base_max = inode_off_max + inode_isize_max + xattr_isize_max
|
||||
entry_size_max = 8
|
||||
chunkbits_min = 9
|
||||
chunkbits_max = page_shift + 31
|
||||
chunk_idx_max = I64_MAX >> chunkbits_min
|
||||
blkaddr_max = U48_MAX
|
||||
physical_max = blkaddr_max << page_shift
|
||||
chunk_off_max = (1 << chunkbits_max) - 1
|
||||
prefix_record_max = 256
|
||||
prefix_off_max = U32_MAX * 4 + 255 * (3 + 2 + prefix_record_max)
|
||||
xattr_base_max = U32_MAX << page_shift
|
||||
xattr_relative_max = U32_MAX * 4
|
||||
|
||||
reachability: dict[str, tuple[bool, str, dict[str, int]]] = {
|
||||
"data.chunk.inode_plus_isize": (
|
||||
inode_off_max > U64_MAX - inode_isize_max,
|
||||
"inode_off is bounded by mounted backing size minus one compact inode",
|
||||
{"inode_off_max": inode_off_max, "inode_isize_max": inode_isize_max},
|
||||
),
|
||||
"data.chunk.isize_plus_xattr": (
|
||||
inode_off_max + inode_isize_max > U64_MAX - xattr_isize_max,
|
||||
"inode offset plus maximum 64-byte inode and 16-bit xattr body stays below UINT64_MAX",
|
||||
{"metadata_base_max": inode_off_max + inode_isize_max, "xattr_isize_max": xattr_isize_max},
|
||||
),
|
||||
"data.chunk.align": (
|
||||
idx_base_max > U64_MAX - (entry_size_max - 1),
|
||||
"validated backing and bounded inode/xattr sizes leave alignment headroom",
|
||||
{"idx_base_max": idx_base_max, "alignment_slack": entry_size_max - 1},
|
||||
),
|
||||
"data.chunk.index_multiply": (
|
||||
chunk_idx_max > (U64_MAX - idx_base_max) // entry_size_max,
|
||||
"size <= OFF_MAX and chunkbits >= 9 bound the index product",
|
||||
{"idx_base_max": idx_base_max, "chunk_idx_max": chunk_idx_max, "entry_size_max": entry_size_max},
|
||||
),
|
||||
"data.chunk.image_size_shift": (
|
||||
I64_MAX > U64_MAX,
|
||||
"mount validates blocks << blkszbits <= GEOM off_t mediasize",
|
||||
{"validated_image_bytes_max": I64_MAX},
|
||||
),
|
||||
"data.chunk.physical_shift": (
|
||||
blkaddr_max > (U64_MAX >> page_shift),
|
||||
"decoded chunk block address is at most 48 bits and blkszbits is at most PAGE_SHIFT",
|
||||
{"blkaddr_max": blkaddr_max, "blkszbits_max": page_shift},
|
||||
),
|
||||
"data.chunk.physical_plus_offset": (
|
||||
chunk_off_max > U64_MAX - physical_max,
|
||||
"48-bit block address and maximum validated chunkbits leave addition headroom",
|
||||
{"physical_max": physical_max, "chunk_off_max": chunk_off_max},
|
||||
),
|
||||
"xattr.backing_size.shift": (
|
||||
I64_MAX > U64_MAX,
|
||||
"mounted primary backing bytes are already bounded by GEOM off_t mediasize",
|
||||
{"validated_backing_bytes_max": I64_MAX},
|
||||
),
|
||||
"xattr.metadata.align": (
|
||||
prefix_off_max > U64_MAX - 3,
|
||||
"32-bit prefix start, 8-bit count, and bounded records cannot approach UINT64_MAX",
|
||||
{"prefix_off_max": prefix_off_max},
|
||||
),
|
||||
"xattr.metadata.header_add": (
|
||||
False,
|
||||
"the preceding off <= UINT64_MAX-3 check and 4-byte roundup imply aligned off <= UINT64_MAX-3, so adding uint16_t cannot overflow",
|
||||
{"accepted_input_max": U64_MAX - 3, "aligned_off_max": U64_MAX - 3, "raw_len_size": 2},
|
||||
),
|
||||
"xattr.shared.base_shift": (
|
||||
U32_MAX > (U64_MAX >> page_shift),
|
||||
"xattr_blkaddr is 32-bit and blkszbits is at most PAGE_SHIFT",
|
||||
{"xattr_blkaddr_max": U32_MAX, "blkszbits_max": page_shift},
|
||||
),
|
||||
"xattr.shared.base_plus_relative": (
|
||||
xattr_relative_max > U64_MAX - xattr_base_max,
|
||||
"32-bit xattr block address and 32-bit shared ID cannot overflow the 64-bit sum",
|
||||
{"xattr_base_max": xattr_base_max, "relative_max": xattr_relative_max},
|
||||
),
|
||||
}
|
||||
|
||||
ledger = []
|
||||
for target in SPEC["targets"]:
|
||||
target_id = target["id"]
|
||||
if target_id not in reachability:
|
||||
raise SystemExit(f"target has no reachability proof: {target_id}")
|
||||
body = function_bodies[f"{target['path']}:{target['function']}"]
|
||||
if body.count(target["needle"]) != 1:
|
||||
raise SystemExit(f"target source is absent or ambiguous: {target_id}")
|
||||
reachable, reason, bounds = reachability[target_id]
|
||||
ledger.append(
|
||||
{
|
||||
**target,
|
||||
"line": line_number(sources[target["path"]], target["needle"]),
|
||||
"source_unique": True,
|
||||
"target_marker_reached": reachable,
|
||||
"current_freebsd_return": "+EOVERFLOW",
|
||||
"candidate_freebsd_return": "+EINTEGRITY",
|
||||
"linux_semantic_return": "-EFSCORRUPTED (no direct checked counterpart)",
|
||||
"source_class": "on-disk contradiction",
|
||||
"replay_observation": reason,
|
||||
"provenance_bounds": bounds,
|
||||
}
|
||||
)
|
||||
|
||||
data_source = sources["repo-pre-15/src/data.c"]
|
||||
xattr_source = sources["repo-pre-15/src/xattr.c"]
|
||||
linux_xattr = sources["src-linux/xattr.c"]
|
||||
control_anchors = (
|
||||
(xattr_source, "if (off > backing_size || (uint64_t)len > backing_size - off)\n\t\treturn (EINTEGRITY);"),
|
||||
(xattr_source, "if (off > INT64_MAX)\n\t\t\treturn (EOVERFLOW);"),
|
||||
(xattr_source, "error = EOPNOTSUPP;"),
|
||||
(data_source, "return (ENXIO);"),
|
||||
(data_source, "return (EIO);"),
|
||||
(data_source, "if (len == 0) {\n\t\treturn (0);\n\t}"),
|
||||
(linux_xattr, "ret = -EOPNOTSUPP;"),
|
||||
(linux_xattr, "ret = -ENOMEM;"),
|
||||
(linux_xattr, "return PTR_ERR(it->kaddr);"),
|
||||
(linux_xattr, "return -EFSCORRUPTED;"),
|
||||
)
|
||||
for source, anchor in control_anchors:
|
||||
if anchor not in source:
|
||||
raise SystemExit(f"errno preservation anchor is absent: {anchor}")
|
||||
|
||||
abi_reachable = False
|
||||
abi_reason = (
|
||||
"erofs_xattr_read_backing checks off <= backing_size before off > INT64_MAX; "
|
||||
"for a mounted primary backing, backing_size <= GEOM signed off_t mediasize"
|
||||
)
|
||||
controls = []
|
||||
for control in SPEC["preservation_controls"]:
|
||||
record: dict[str, Any] = dict(control)
|
||||
record["source_anchors_verified"] = True
|
||||
if control["id"] == "abi.off_gt_int64":
|
||||
record["target_marker_reached"] = abi_reachable
|
||||
record["replay_observation"] = abi_reason
|
||||
controls.append(record)
|
||||
|
||||
unreachable = [record["id"] for record in ledger if not record["target_marker_reached"]]
|
||||
status = "GO" if not unreachable else "STOP"
|
||||
stop_reasons = [
|
||||
{
|
||||
"id": target_id,
|
||||
"reason": next(
|
||||
record["replay_observation"] for record in ledger if record["id"] == target_id
|
||||
),
|
||||
}
|
||||
for target_id in unreachable
|
||||
]
|
||||
|
||||
ledger_document = {
|
||||
"schema": 1,
|
||||
"gate": "G06",
|
||||
"candidate": "P15-052",
|
||||
"status": status,
|
||||
"resolved_base": resolved,
|
||||
"targets": ledger,
|
||||
"preservation_controls": controls,
|
||||
"freebsd_contract": freebsd_contract,
|
||||
"prototype_identity": SPEC["prototype_identity"],
|
||||
"prototype_disposition": (
|
||||
"READY arithmetic vectors inject unconstrained internal or local values; "
|
||||
"they are not independently reachable on-disk fixtures"
|
||||
),
|
||||
}
|
||||
(OUTPUT / "branch-ledger.json").write_text(
|
||||
json.dumps(ledger_document, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
||||
)
|
||||
|
||||
tsv = [
|
||||
"id\tfunction\tfield\tfreebsd_now\tfreebsd_candidate\tlinux_semantic\tmarker\tobservation"
|
||||
]
|
||||
for record in ledger:
|
||||
tsv.append(
|
||||
"\t".join(
|
||||
(
|
||||
record["id"],
|
||||
record["function"],
|
||||
record["field"],
|
||||
record["current_freebsd_return"],
|
||||
record["candidate_freebsd_return"],
|
||||
record["linux_semantic_return"],
|
||||
"reached" if record["target_marker_reached"] else "not-reached",
|
||||
record["replay_observation"],
|
||||
)
|
||||
)
|
||||
)
|
||||
(OUTPUT / "branch-ledger.tsv").write_text("\n".join(tsv) + "\n", encoding="ascii")
|
||||
|
||||
control_tsv = ["id\tfreebsd\tlinux\tresult\tmarker"]
|
||||
for record in controls:
|
||||
control_tsv.append(
|
||||
"\t".join(
|
||||
(
|
||||
record["id"],
|
||||
record["freebsd"],
|
||||
record["linux"],
|
||||
record["result"],
|
||||
(
|
||||
"not-reached"
|
||||
if record.get("target_marker_reached") is False
|
||||
else "preserved"
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
(OUTPUT / "preservation-ledger.tsv").write_text(
|
||||
"\n".join(control_tsv) + "\n", encoding="ascii"
|
||||
)
|
||||
(OUTPUT / "source-sha256.json").write_text(
|
||||
json.dumps(source_hashes, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
||||
)
|
||||
(OUTPUT / "freebsd-contract.json").write_text(
|
||||
json.dumps(freebsd_contract, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
||||
)
|
||||
|
||||
result = {
|
||||
"schema": 1,
|
||||
"gate": "G06",
|
||||
"candidate": "P15-052",
|
||||
"status": status,
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"resolved_base": resolved,
|
||||
"target_count": len(ledger),
|
||||
"target_markers_reached": len(ledger) - len(unreachable),
|
||||
"target_markers_not_reached": len(unreachable),
|
||||
"source_unique_count": sum(record["source_unique"] for record in ledger),
|
||||
"preservation_control_count": len(controls),
|
||||
"abi_marker_reached": abi_reachable,
|
||||
"stop_reasons": stop_reasons,
|
||||
"b20": "STOP-NO-SOURCE",
|
||||
"b21": "NOT_RUN",
|
||||
"qemu": "NOT_RUN",
|
||||
"qemu_reason": "host-proven target reachability failure",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
}
|
||||
(OUTPUT / "result.json").write_text(
|
||||
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
||||
)
|
||||
|
||||
hash_lines = []
|
||||
for path in sorted(OUTPUT.iterdir()):
|
||||
if path.is_file() and path.name != "SHA256SUMS":
|
||||
hash_lines.append(f"{sha256(path.read_bytes())} {path.name}")
|
||||
(OUTPUT / "SHA256SUMS").write_text("\n".join(hash_lines) + "\n", encoding="ascii")
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
if status != "GO":
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"candidate": "P15-062",
|
||||
"freebsd": {
|
||||
"head": "106727738dcfb6c001b46f25363b91cece970085",
|
||||
"sha256": {
|
||||
"sys/sys/endian.h": "3d870d499089ac84bb49debebbb75b406a5f4df6f05f1582faebc5cf0b7b8806",
|
||||
"sys/sys/errno.h": "4e615f248a900c6c240c0c87844fd60a8bdffc8a34259d876d5dfde74bd9e42c",
|
||||
"sys/sys/param.h": "cc451dd2de4d6a7cc97b928ddb9e5b19475cba4ab418153ab7f6e6baa661b917",
|
||||
"sys/sys/systm.h": "648c4ba00523c4dff1e9c0e693722d748e38312fd3cf35f2ee92c21188b62649"
|
||||
}
|
||||
},
|
||||
"gate": "G04",
|
||||
"legacy_fixture": {
|
||||
"block_size": 4096,
|
||||
"expected_image_size": 12288,
|
||||
"expected_source_sha256": "cea05c6972efce79727fe268260e8de74089b0fc0b527825beaca8de4dbacff2",
|
||||
"extents": [
|
||||
{
|
||||
"consumed_bytes": 4096,
|
||||
"decoded_sha256": "9d1d4465d9891e22a7cb79f1d5c9e8c4b7500753fbe5a7feca1117e948970017",
|
||||
"logical_length": 1038906,
|
||||
"logical_offset": 0,
|
||||
"physical_length": 4096,
|
||||
"physical_offset": 4096,
|
||||
"zero_tail_bytes": 0
|
||||
},
|
||||
{
|
||||
"consumed_bytes": 48,
|
||||
"decoded_sha256": "c5563821cb010b2bf6ba6ecd5a563f71f9d37644f086c90de4a9c44a071b4bdc",
|
||||
"logical_length": 9670,
|
||||
"logical_offset": 1038906,
|
||||
"physical_length": 4096,
|
||||
"physical_offset": 8192,
|
||||
"zero_tail_bytes": 4048
|
||||
}
|
||||
],
|
||||
"mkfs_args": [
|
||||
"-T0",
|
||||
"-U00112233-4455-6677-8899-aabbccddeeff",
|
||||
"--all-root",
|
||||
"-x-1",
|
||||
"-zlz4",
|
||||
"-C4096",
|
||||
"-Elegacy-compress"
|
||||
],
|
||||
"segments": [
|
||||
{
|
||||
"byte": 65,
|
||||
"length": 262144
|
||||
},
|
||||
{
|
||||
"byte": 66,
|
||||
"length": 262144
|
||||
},
|
||||
{
|
||||
"byte": 67,
|
||||
"length": 262144
|
||||
},
|
||||
{
|
||||
"byte": 68,
|
||||
"length": 262144
|
||||
}
|
||||
]
|
||||
},
|
||||
"partial_fixture": {
|
||||
"a_sha256": "cea05c6972efce79727fe268260e8de74089b0fc0b527825beaca8de4dbacff2",
|
||||
"b_nid": 58,
|
||||
"b_sha256": "f25f98332996139ce81bac04665df5593856430a39b6ed7e25a25f8c066e0d47",
|
||||
"block_size": 4096,
|
||||
"expected_image_sha256": "63b6ec3736b325f5e3d355eef35ac8f7e80cb7edc49e3e495a4006fffe7f7514",
|
||||
"expected_image_size": 16384,
|
||||
"full_decoded_bytes": 9670,
|
||||
"full_stream_consumed_bytes": 48,
|
||||
"mkfs_args": [
|
||||
"-T0",
|
||||
"-U00112233-4455-6677-8899-aabbccddeeff",
|
||||
"--all-root",
|
||||
"-x-1",
|
||||
"-zlz4",
|
||||
"-C4096",
|
||||
"-Ededupe"
|
||||
],
|
||||
"partial_consumed_bytes": 42,
|
||||
"partial_index_records": [
|
||||
{
|
||||
"cluster_offset": 550,
|
||||
"lcn": 254,
|
||||
"pblk": 2
|
||||
},
|
||||
{
|
||||
"cluster_offset": 2048,
|
||||
"lcn": 255,
|
||||
"pblk": 2
|
||||
}
|
||||
],
|
||||
"partial_output_bytes": [
|
||||
4096,
|
||||
5594
|
||||
],
|
||||
"prefix_pad_byte": 33,
|
||||
"prefix_pattern": "P15-062-partial-reference\n",
|
||||
"prefix_size": 2048,
|
||||
"shared_pcluster_offset": 8192,
|
||||
"stream_start": 4048
|
||||
},
|
||||
"required_base": "205a90465edb64e83ba44aaacac9bedad4cbe905",
|
||||
"schema": 1,
|
||||
"source_sha256": {
|
||||
"repo-pre-15/src/decompressor.c": "76289dcb494994f77c1ba6ff10bbbfe55e7b4baa6a1724c35af14ae1611c537c",
|
||||
"repo-pre-15/src/decompressor_lz4.c": "ca5e5bd6142f9e3c3ea66849900f05799dbc7d6d1a97e0d5a988c28edb90ea98",
|
||||
"repo-pre-15/src/zdata.c": "3eeb5dae825d7028793a2e1d19a80d24d3f78cdc2e2d5e19466d8fb1208f3242",
|
||||
"repo-pre-15/src/zmap.c": "95432d49c20f3049e0fb5af21904283494325570673dbb037767c1397c77c1f3",
|
||||
"src-linux/decompressor.c": "caf1c501d00a5a2c9cda5fc0b59d2823eaedf0161a130ca69cd5e7c455128709"
|
||||
},
|
||||
"upstream": {
|
||||
"liblz4_sha256": "67bf8b84af77e962e09be661944f7c61393c4806746734e36db9fbdaebc1de36",
|
||||
"liblz4_soname": "/lib/x86_64-linux-gnu/liblz4.so.1",
|
||||
"liblz4_version": "1.10.0",
|
||||
"url": "https://git.kernel.org/pub/scm/linux/kernel/git/xiang/erofs-utils.git",
|
||||
"versions": [
|
||||
{
|
||||
"commit": "ee97fe5fb77c737df0f77d92ab0d92edd3a11be6",
|
||||
"expected_image_sha256": "d78c1f00b930bfbf45ac2a0a903896ed5559bac43297e283b348a63926ce5576",
|
||||
"name": "v1.4"
|
||||
},
|
||||
{
|
||||
"commit": "ce36273833096f1b7e828309d9b1caa37132092d",
|
||||
"expected_image_sha256": "1f08b09b7ea40d495160699ddf7079bf5d4deb85859dbe7459a41617f25fdddf",
|
||||
"name": "v1.7"
|
||||
},
|
||||
{
|
||||
"commit": "3689cbc2349bff05807d2f939146e92eb1bfaea1",
|
||||
"expected_image_sha256": "c5ee543074d57a45adc7c666db47ca435c5d3c8504d3163d471ed53bdbdac303",
|
||||
"name": "v1.8.6"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Executable
+713
@@ -0,0 +1,713 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
gate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||
root=$(CDPATH= cd -- "$gate_dir/../../../.." && pwd -P)
|
||||
input=$gate_dir/P15-062-input.json
|
||||
freebsd_src=${FREEBSD_SRC:-/work/build/freebsd-src}
|
||||
base=
|
||||
output=
|
||||
|
||||
while test "$#" -gt 0; do
|
||||
case "$1" in
|
||||
--base)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 2; }
|
||||
base=$2
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 2; }
|
||||
output=$2
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
printf 'unknown argument: %s\n' "$1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 2; }
|
||||
test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 2; }
|
||||
test -f "$input" || { printf 'missing input: %s\n' "$input" >&2; exit 2; }
|
||||
test -d "$freebsd_src/sys" || {
|
||||
printf 'missing FreeBSD source tree: %s\n' "$freebsd_src" >&2
|
||||
exit 2
|
||||
}
|
||||
for tool in cc git make pkg-config python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
printf 'missing required host tool: %s\n' "$tool" >&2
|
||||
exit 2
|
||||
}
|
||||
done
|
||||
case "$output" in
|
||||
/*) ;;
|
||||
*) output=$PWD/$output ;;
|
||||
esac
|
||||
test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 2; }
|
||||
mkdir -p "$output"
|
||||
|
||||
python3 - "$root" "$input" "$base" "$output" "$freebsd_src" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(sys.argv[1])
|
||||
INPUT = Path(sys.argv[2])
|
||||
REQUESTED_BASE = sys.argv[3]
|
||||
OUTPUT = Path(sys.argv[4])
|
||||
FREEBSD_SRC = Path(sys.argv[5])
|
||||
SPEC = json.loads(INPUT.read_text(encoding="ascii"))
|
||||
|
||||
|
||||
class GateFailure(Exception):
|
||||
def __init__(self, status: str, reason: str):
|
||||
super().__init__(reason)
|
||||
self.status = status
|
||||
self.reason = reason
|
||||
|
||||
|
||||
def sha256_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def sha256_path(path: Path) -> str:
|
||||
return sha256_bytes(path.read_bytes())
|
||||
|
||||
|
||||
def write_json(path: Path, value: Any) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
|
||||
|
||||
def git(*args: str) -> str:
|
||||
return subprocess.check_output(["git", "-C", str(ROOT), *args], text=True).strip()
|
||||
|
||||
|
||||
def source_at(commit: str, path: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(ROOT), "show", f"{commit}:{path}"],
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise GateFailure("INFRA_BLOCKED", f"cannot read {path} at {commit}: {completed.stderr.strip()}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def run_logged(argv: list[str], cwd: Path, log: Path, timeout: int = 120) -> subprocess.CompletedProcess[str]:
|
||||
completed = subprocess.run(
|
||||
argv,
|
||||
cwd=cwd,
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
log.write_text(
|
||||
"$ " + " ".join(argv) + "\n" + completed.stdout + f"\n[exit {completed.returncode}]\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise GateFailure("INFRA_BLOCKED", f"command failed ({completed.returncode}): {' '.join(argv)}")
|
||||
return completed
|
||||
|
||||
|
||||
def raw_lz4_decode(data: bytes, target: int | None = None) -> tuple[bytes, int]:
|
||||
ip = 0
|
||||
output = bytearray()
|
||||
while ip < len(data):
|
||||
token = data[ip]
|
||||
ip += 1
|
||||
literal_length = token >> 4
|
||||
if literal_length == 15:
|
||||
while True:
|
||||
if ip >= len(data):
|
||||
raise GateFailure("INFRA_BLOCKED", "oracle saw a truncated literal length")
|
||||
value = data[ip]
|
||||
ip += 1
|
||||
literal_length += value
|
||||
if value != 255:
|
||||
break
|
||||
if ip + literal_length > len(data):
|
||||
raise GateFailure("INFRA_BLOCKED", "oracle saw truncated literals")
|
||||
output.extend(data[ip : ip + literal_length])
|
||||
ip += literal_length
|
||||
if target is not None and len(output) >= target:
|
||||
return bytes(output[:target]), ip
|
||||
if ip == len(data):
|
||||
return bytes(output), ip
|
||||
if ip + 2 > len(data):
|
||||
raise GateFailure("INFRA_BLOCKED", "oracle saw a truncated match offset")
|
||||
offset = data[ip] | data[ip + 1] << 8
|
||||
ip += 2
|
||||
if offset == 0 or offset > len(output):
|
||||
raise GateFailure("INFRA_BLOCKED", f"oracle saw invalid match offset {offset}")
|
||||
match_length = token & 15
|
||||
if match_length == 15:
|
||||
while True:
|
||||
if ip >= len(data):
|
||||
raise GateFailure("INFRA_BLOCKED", "oracle saw a truncated match length")
|
||||
value = data[ip]
|
||||
ip += 1
|
||||
match_length += value
|
||||
if value != 255:
|
||||
break
|
||||
for _ in range(match_length + 4):
|
||||
output.append(output[-offset])
|
||||
if target is not None and len(output) >= target:
|
||||
return bytes(output[:target]), ip
|
||||
raise GateFailure("INFRA_BLOCKED", "oracle input ended before a final literal sequence")
|
||||
|
||||
|
||||
def make_legacy_source(path: Path) -> bytes:
|
||||
content = b"".join(
|
||||
bytes([segment["byte"]]) * segment["length"]
|
||||
for segment in SPEC["legacy_fixture"]["segments"]
|
||||
)
|
||||
if sha256_bytes(content) != SPEC["legacy_fixture"]["expected_source_sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "legacy source generator hash drift")
|
||||
path.mkdir(parents=True)
|
||||
(path / "big.txt").write_bytes(content)
|
||||
return content
|
||||
|
||||
|
||||
def make_partial_source(path: Path) -> tuple[bytes, bytes]:
|
||||
path.mkdir(parents=True)
|
||||
data_a = b"".join(
|
||||
bytes([segment["byte"]]) * segment["length"]
|
||||
for segment in SPEC["legacy_fixture"]["segments"]
|
||||
)
|
||||
partial = SPEC["partial_fixture"]
|
||||
pattern = partial["prefix_pattern"].encode("ascii")
|
||||
repeats = (partial["prefix_size"] + len(pattern) - 1) // len(pattern)
|
||||
prefix = (pattern * repeats)[: partial["prefix_size"]]
|
||||
prefix = prefix.ljust(partial["prefix_size"], bytes([partial["prefix_pad_byte"]]))
|
||||
data_b = prefix + data_a
|
||||
if sha256_bytes(data_a) != partial["a_sha256"] or sha256_bytes(data_b) != partial["b_sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "partial source generator hash drift")
|
||||
(path / "a.dat").write_bytes(data_a)
|
||||
(path / "b.dat").write_bytes(data_b)
|
||||
return data_a, data_b
|
||||
|
||||
|
||||
def load_liblz4() -> tuple[Any, Any]:
|
||||
upstream = SPEC["upstream"]
|
||||
soname = Path(upstream["liblz4_soname"])
|
||||
version = subprocess.check_output(["pkg-config", "--modversion", "liblz4"], text=True).strip()
|
||||
if version != upstream["liblz4_version"] or sha256_path(soname) != upstream["liblz4_sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "liblz4 identity changed")
|
||||
library = ctypes.CDLL(str(soname))
|
||||
full = library.LZ4_decompress_safe
|
||||
full.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, ctypes.c_int]
|
||||
full.restype = ctypes.c_int
|
||||
partial = library.LZ4_decompress_safe_partial
|
||||
partial.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int]
|
||||
partial.restype = ctypes.c_int
|
||||
return full, partial
|
||||
|
||||
|
||||
def liblz4_call(function: Any, source: bytes, output_size: int, target: int | None = None) -> tuple[int, bytes]:
|
||||
source_buffer = ctypes.create_string_buffer(source, len(source))
|
||||
output_buffer = ctypes.create_string_buffer(output_size)
|
||||
if target is None:
|
||||
result = function(source_buffer, output_buffer, len(source), output_size)
|
||||
else:
|
||||
result = function(source_buffer, output_buffer, len(source), target, output_size)
|
||||
return result, output_buffer.raw
|
||||
|
||||
|
||||
def compile_dut_decoder(temp: Path, source: str) -> Any:
|
||||
source_dir = temp / "dut-decoder"
|
||||
sys_dir = source_dir / "sys"
|
||||
sys_dir.mkdir(parents=True)
|
||||
(source_dir / "decompressor_lz4.c").write_text(source, encoding="utf-8")
|
||||
(sys_dir / "param.h").write_text(
|
||||
"#include <stdbool.h>\n#include <stddef.h>\n#include <stdint.h>\n#include <string.h>\n"
|
||||
"#define EINTEGRITY 97\n#define MIN(a, b) ((a) < (b) ? (a) : (b))\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
(sys_dir / "endian.h").write_text(
|
||||
"#include <stdint.h>\nstatic inline uint16_t le16dec(const void *p) { "
|
||||
"const uint8_t *b = p; return (uint16_t)b[0] | (uint16_t)b[1] << 8; }\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
(sys_dir / "systm.h").write_text("#include <string.h>\n", encoding="ascii")
|
||||
(source_dir / "compress.h").write_text(
|
||||
"#include <stdbool.h>\n#include <stddef.h>\n"
|
||||
"struct z_erofs_decompress_req { void *sbi; const void *map; const void *in; "
|
||||
"size_t inputsize; void *out; size_t outputsize; bool partial_decoding; };\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
library = temp / "dut-lz4.so"
|
||||
run_logged(
|
||||
["cc", "-shared", "-fPIC", "-std=c11", "-Wall", "-Wextra", "-Werror", "-I", str(source_dir),
|
||||
"-o", str(library), str(source_dir / "decompressor_lz4.c")],
|
||||
temp,
|
||||
OUTPUT / "logs/dut-decoder-build.log",
|
||||
)
|
||||
loaded = ctypes.CDLL(str(library))
|
||||
|
||||
class Request(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("sbi", ctypes.c_void_p),
|
||||
("map", ctypes.c_void_p),
|
||||
("in", ctypes.c_void_p),
|
||||
("inputsize", ctypes.c_size_t),
|
||||
("out", ctypes.c_void_p),
|
||||
("outputsize", ctypes.c_size_t),
|
||||
("partial_decoding", ctypes.c_bool),
|
||||
]
|
||||
|
||||
function = loaded.z_erofs_lz4_decompress
|
||||
function.argtypes = [ctypes.POINTER(Request)]
|
||||
function.restype = ctypes.c_int
|
||||
return loaded, function, Request
|
||||
|
||||
|
||||
def dut_call(function: Any, request_type: Any, source: bytes, output_size: int, partial: bool) -> tuple[int, bytes, bool]:
|
||||
source_buffer = ctypes.create_string_buffer(source, len(source))
|
||||
guard_size = 64
|
||||
backing = bytearray(b"\xa5" * guard_size + b"\0" * output_size + b"\x5a" * guard_size)
|
||||
output_buffer = (ctypes.c_ubyte * len(backing)).from_buffer(backing)
|
||||
output_pointer = ctypes.cast(ctypes.byref(output_buffer, guard_size), ctypes.c_void_p)
|
||||
request = request_type(
|
||||
None,
|
||||
None,
|
||||
ctypes.cast(source_buffer, ctypes.c_void_p),
|
||||
len(source),
|
||||
output_pointer,
|
||||
output_size,
|
||||
partial,
|
||||
)
|
||||
result = function(ctypes.byref(request))
|
||||
guards_ok = backing[:guard_size] == b"\xa5" * guard_size and backing[-guard_size:] == b"\x5a" * guard_size
|
||||
return result, bytes(backing[guard_size : guard_size + output_size]), guards_ok
|
||||
|
||||
|
||||
def verify_cleanup_source(zdata_source: str) -> dict[str, Any]:
|
||||
anchors = [
|
||||
"error = z_erofs_decompress(sbi, map, input, (size_t)map->m_plen,",
|
||||
"erofs_put_metabuf(&buf);",
|
||||
"erofs_brelse(compressed);",
|
||||
"free(decoded, M_EROFS);",
|
||||
"*bufp = decoded;",
|
||||
]
|
||||
missing = [anchor for anchor in anchors if anchor not in zdata_source]
|
||||
if missing:
|
||||
raise GateFailure("INFRA_BLOCKED", f"DUT cleanup source anchors changed: {missing}")
|
||||
release_pos = min(zdata_source.index("erofs_put_metabuf(&buf);"), zdata_source.index("erofs_brelse(compressed);"))
|
||||
error_pos = zdata_source.index("if (error != 0) {", release_pos)
|
||||
free_pos = zdata_source.index("free(decoded, M_EROFS);", error_pos)
|
||||
publish_pos = zdata_source.index("*bufp = decoded;", free_pos)
|
||||
return {
|
||||
"decoded_error_free_after_input_release": release_pos < error_pos < free_pos,
|
||||
"decoded_success_publish_after_error_branch": free_pos < publish_pos,
|
||||
"metadata_release_anchor": True,
|
||||
"physical_release_anchor": True,
|
||||
}
|
||||
|
||||
|
||||
def build_versions(temp: Path, legacy_source: bytes) -> tuple[list[dict[str, Any]], dict[str, Path]]:
|
||||
upstream = SPEC["upstream"]
|
||||
clone = temp / "erofs-utils"
|
||||
run_logged(
|
||||
["git", "clone", "--no-checkout", upstream["url"], str(clone)],
|
||||
temp,
|
||||
OUTPUT / "logs/upstream-clone.log",
|
||||
timeout=90,
|
||||
)
|
||||
records = []
|
||||
worktrees: dict[str, Path] = {}
|
||||
fixture = SPEC["legacy_fixture"]
|
||||
source_dir = temp / "legacy-source"
|
||||
make_legacy_source(source_dir)
|
||||
for version in upstream["versions"]:
|
||||
name = version["name"]
|
||||
worktree = temp / f"erofs-{name}"
|
||||
worktrees[name] = worktree
|
||||
run_logged(
|
||||
["git", "-C", str(clone), "worktree", "add", "--detach", str(worktree), version["commit"]],
|
||||
temp,
|
||||
OUTPUT / f"logs/{name}-worktree.log",
|
||||
)
|
||||
resolved = subprocess.check_output(["git", "-C", str(worktree), "rev-parse", "HEAD"], text=True).strip()
|
||||
if resolved != version["commit"]:
|
||||
raise GateFailure("INFRA_BLOCKED", f"upstream commit mismatch for {name}")
|
||||
run_logged(["./autogen.sh"], worktree, OUTPUT / f"logs/{name}-autogen.log")
|
||||
run_logged(["./configure", "--disable-fuse"], worktree, OUTPUT / f"logs/{name}-configure.log")
|
||||
run_logged(["make", "-s", "-j2"], worktree, OUTPUT / f"logs/{name}-make.log")
|
||||
mkfs = worktree / "mkfs/mkfs.erofs"
|
||||
fsck = worktree / "fsck/fsck.erofs"
|
||||
dump = worktree / "dump/dump.erofs"
|
||||
images = temp / f"images-{name}"
|
||||
images.mkdir()
|
||||
generated = []
|
||||
for label in ("a", "b"):
|
||||
image = images / f"legacy-{label}.erofs"
|
||||
argv = [str(mkfs), *fixture["mkfs_args"], str(image), str(source_dir)]
|
||||
run_logged(argv, worktree, OUTPUT / f"logs/{name}-mkfs-{label}.log")
|
||||
generated.append(image)
|
||||
hashes = [sha256_path(path) for path in generated]
|
||||
if hashes[0] != hashes[1] or hashes[0] != version["expected_image_sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", f"{name} image reproducibility/hash mismatch: {hashes}")
|
||||
run_logged([str(fsck), "--extract", str(generated[0])], worktree, OUTPUT / f"logs/{name}-fsck.log")
|
||||
superblock = run_logged([str(dump), "-s", str(generated[0])], worktree, OUTPUT / f"logs/{name}-super.log").stdout
|
||||
feature_line = next((line for line in superblock.splitlines() if "features:" in line), "")
|
||||
if "0padding" in feature_line:
|
||||
raise GateFailure("INFRA_BLOCKED", f"{name} legacy fixture unexpectedly has 0padding")
|
||||
image = generated[0].read_bytes()
|
||||
if len(image) != fixture["expected_image_size"]:
|
||||
raise GateFailure("INFRA_BLOCKED", f"{name} image size changed")
|
||||
extents = []
|
||||
reconstructed = bytearray()
|
||||
for expected in fixture["extents"]:
|
||||
block = image[expected["physical_offset"] : expected["physical_offset"] + expected["physical_length"]]
|
||||
decoded, consumed = raw_lz4_decode(block, expected["logical_length"])
|
||||
tail = block[consumed:]
|
||||
if (
|
||||
consumed != expected["consumed_bytes"]
|
||||
or sha256_bytes(decoded) != expected["decoded_sha256"]
|
||||
or len(tail) != expected["zero_tail_bytes"]
|
||||
or any(tail)
|
||||
):
|
||||
raise GateFailure("INFRA_BLOCKED", f"{name} independent extent oracle drift")
|
||||
logical = legacy_source[expected["logical_offset"] : expected["logical_offset"] + expected["logical_length"]]
|
||||
if decoded != logical:
|
||||
raise GateFailure("INFRA_BLOCKED", f"{name} decoded extent differs from source")
|
||||
reconstructed.extend(decoded)
|
||||
extents.append({
|
||||
**expected,
|
||||
"input_bytes": len(block),
|
||||
"tail_all_zero": not any(tail),
|
||||
})
|
||||
if bytes(reconstructed) != legacy_source:
|
||||
raise GateFailure("INFRA_BLOCKED", f"{name} reconstructed file mismatch")
|
||||
records.append({
|
||||
"commit": resolved,
|
||||
"fsck": "PASS",
|
||||
"image_repeated_sha256": hashes,
|
||||
"image_size": len(image),
|
||||
"legacy_without_0padding": True,
|
||||
"name": name,
|
||||
"extents": extents,
|
||||
})
|
||||
return records, worktrees
|
||||
|
||||
|
||||
def verify_partial_fixture(temp: Path, worktree: Path, full: Any, partial_codec: Any, dut: Any) -> dict[str, Any]:
|
||||
partial_spec = SPEC["partial_fixture"]
|
||||
source_dir = temp / "partial-source"
|
||||
data_a, data_b = make_partial_source(source_dir)
|
||||
images = temp / "partial-images"
|
||||
images.mkdir()
|
||||
mkfs = worktree / "mkfs/mkfs.erofs"
|
||||
fsck = worktree / "fsck/fsck.erofs"
|
||||
dump = worktree / "dump/dump.erofs"
|
||||
generated = []
|
||||
for label in ("a", "b"):
|
||||
image = images / f"partial-{label}.erofs"
|
||||
run_logged(
|
||||
[str(mkfs), *partial_spec["mkfs_args"], str(image), str(source_dir)],
|
||||
worktree,
|
||||
OUTPUT / f"logs/partial-mkfs-{label}.log",
|
||||
)
|
||||
generated.append(image)
|
||||
hashes = [sha256_path(path) for path in generated]
|
||||
if hashes[0] != hashes[1] or hashes[0] != partial_spec["expected_image_sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", f"partial fixture reproducibility/hash mismatch: {hashes}")
|
||||
run_logged([str(fsck), "--extract", str(generated[0])], worktree, OUTPUT / "logs/partial-fsck.log")
|
||||
dump_b = run_logged(
|
||||
[str(dump), "--path=/b.dat", "-e", str(generated[0])],
|
||||
worktree,
|
||||
OUTPUT / "logs/partial-dump-b.log",
|
||||
).stdout
|
||||
match = re.search(r"^NID:\s+(\d+)", dump_b, re.MULTILINE)
|
||||
if match is None or int(match.group(1)) != partial_spec["b_nid"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "partial fixture b.dat NID drift")
|
||||
image = generated[0].read_bytes()
|
||||
if len(image) != partial_spec["expected_image_size"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "partial fixture image size drift")
|
||||
inode_offset = partial_spec["b_nid"] * 32
|
||||
inode_format, xattr_count, _, _, inode_size = struct.unpack_from("<HHHHI", image, inode_offset)
|
||||
inode_bytes = 64 if inode_format & 1 else 32
|
||||
datalayout = (inode_format >> 1) & 7
|
||||
if xattr_count != 0 or datalayout != 1 or inode_size != len(data_b):
|
||||
raise GateFailure("INFRA_BLOCKED", "partial fixture inode contract drift")
|
||||
index_start = ((inode_offset + inode_bytes + 7) & ~7) + 16
|
||||
index_records = []
|
||||
for expected in partial_spec["partial_index_records"]:
|
||||
position = index_start + expected["lcn"] * 8
|
||||
advise, cluster_offset, pblk = struct.unpack_from("<HHI", image, position)
|
||||
record = {
|
||||
"advise": advise,
|
||||
"cluster_offset": cluster_offset,
|
||||
"lcn": expected["lcn"],
|
||||
"partial_ref": bool(advise & 0x8000),
|
||||
"pblk": pblk,
|
||||
"position": position,
|
||||
}
|
||||
if not record["partial_ref"] or (cluster_offset, pblk) != (expected["cluster_offset"], expected["pblk"]):
|
||||
raise GateFailure("INFRA_BLOCKED", "partial-reference index oracle drift")
|
||||
index_records.append(record)
|
||||
block_offset = partial_spec["shared_pcluster_offset"]
|
||||
block = image[block_offset : block_offset + partial_spec["block_size"]]
|
||||
first_nonzero = next((index for index, value in enumerate(block) if value), len(block))
|
||||
if first_nonzero != partial_spec["stream_start"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "partial pcluster leading padding drift")
|
||||
stream = block[first_nonzero:]
|
||||
full_decoded, full_consumed = raw_lz4_decode(stream, partial_spec["full_decoded_bytes"])
|
||||
if full_consumed != partial_spec["full_stream_consumed_bytes"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "partial pcluster full consumption drift")
|
||||
expected_tail = data_a[-partial_spec["full_decoded_bytes"] :]
|
||||
if full_decoded != expected_tail:
|
||||
raise GateFailure("INFRA_BLOCKED", "partial pcluster decoded bytes drift")
|
||||
partial_records = []
|
||||
for output_size in partial_spec["partial_output_bytes"]:
|
||||
decoded, consumed = raw_lz4_decode(stream, output_size)
|
||||
if consumed != partial_spec["partial_consumed_bytes"] or decoded != expected_tail[:output_size]:
|
||||
raise GateFailure("INFRA_BLOCKED", "partial parser consumption/output drift")
|
||||
result, output, guards = dut_call(dut[1], dut[2], stream, output_size, True)
|
||||
if result != 0 or output != expected_tail[:output_size] or not guards:
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen DUT partial decode mismatch")
|
||||
partial_records.append({"consumed_bytes": consumed, "decoded_bytes": output_size, "dut_errno": result, "guards": guards})
|
||||
corrupted = bytearray(stream)
|
||||
corrupted[partial_spec["partial_consumed_bytes"] :] = b"\0" * (
|
||||
len(corrupted) - partial_spec["partial_consumed_bytes"]
|
||||
)
|
||||
partial_result, partial_output, partial_guards = dut_call(
|
||||
dut[1], dut[2], bytes(corrupted), partial_spec["partial_output_bytes"][0], True
|
||||
)
|
||||
full_result, _, full_guards = dut_call(
|
||||
dut[1], dut[2], bytes(corrupted), partial_spec["full_decoded_bytes"], False
|
||||
)
|
||||
lib_partial_result, lib_partial_output = liblz4_call(
|
||||
partial_codec, bytes(corrupted), partial_spec["partial_output_bytes"][0], partial_spec["partial_output_bytes"][0]
|
||||
)
|
||||
lib_full_result, _ = liblz4_call(full, bytes(corrupted), partial_spec["full_decoded_bytes"])
|
||||
if (
|
||||
partial_result != 0
|
||||
or partial_output != expected_tail[: partial_spec["partial_output_bytes"][0]]
|
||||
or full_result != 97
|
||||
or lib_partial_result != partial_spec["partial_output_bytes"][0]
|
||||
or lib_partial_output != expected_tail[: partial_spec["partial_output_bytes"][0]]
|
||||
or lib_full_result >= 0
|
||||
or not partial_guards
|
||||
or not full_guards
|
||||
):
|
||||
raise GateFailure("INFRA_BLOCKED", "range-after-corruption oracle mismatch")
|
||||
return {
|
||||
"fixture_repeated_sha256": hashes,
|
||||
"fsck": "PASS",
|
||||
"index_records": index_records,
|
||||
"partial_records": partial_records,
|
||||
"range_after_corruption": {
|
||||
"corruption_starts_at_consumed_byte": partial_spec["partial_consumed_bytes"],
|
||||
"dut_full_errno": full_result,
|
||||
"dut_partial_errno": partial_result,
|
||||
"full_guard_unchanged": full_guards,
|
||||
"liblz4_full_result": lib_full_result,
|
||||
"liblz4_partial_result": lib_partial_result,
|
||||
"partial_guard_unchanged": partial_guards,
|
||||
"partial_output_matches_full_slice": True,
|
||||
},
|
||||
"shared_stream": {
|
||||
"consumed_bytes": full_consumed,
|
||||
"decoded_bytes": len(full_decoded),
|
||||
"leading_zero_bytes": first_nonzero,
|
||||
"sha256": sha256_bytes(stream),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def finalize() -> None:
|
||||
lines = []
|
||||
for path in sorted(OUTPUT.rglob("*")):
|
||||
if path.is_file() and path.name != "SHA256SUMS":
|
||||
lines.append(f"{sha256_path(path)} {path.relative_to(OUTPUT)}")
|
||||
(OUTPUT / "SHA256SUMS").write_text("\n".join(lines) + "\n", encoding="ascii")
|
||||
|
||||
|
||||
result: dict[str, Any] | None = None
|
||||
exit_code = 0
|
||||
owned_temp: str | None = None
|
||||
try:
|
||||
if SPEC.get("schema") != 1 or SPEC.get("candidate") != "P15-062" or SPEC.get("gate") != "G04":
|
||||
raise GateFailure("INFRA_BLOCKED", "invalid P15-062 input schema")
|
||||
resolved = git("rev-parse", f"{REQUESTED_BASE}^{{commit}}")
|
||||
if resolved != SPEC["required_base"]:
|
||||
raise GateFailure("INFRA_BLOCKED", f"P15-062 must replay {SPEC['required_base']}, got {resolved}")
|
||||
sources = {path: source_at(resolved, path) for path in SPEC["source_sha256"]}
|
||||
source_hashes = {path: sha256_bytes(text.encode("utf-8")) for path, text in sources.items()}
|
||||
if source_hashes != SPEC["source_sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen DUT/Linux source identity changed")
|
||||
freebsd_head = subprocess.check_output(["git", "-C", str(FREEBSD_SRC), "rev-parse", "HEAD"], text=True).strip()
|
||||
freebsd_hashes = {path: sha256_path(FREEBSD_SRC / path) for path in SPEC["freebsd"]["sha256"]}
|
||||
if freebsd_head != SPEC["freebsd"]["head"] or freebsd_hashes != SPEC["freebsd"]["sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "FreeBSD source identity changed")
|
||||
if "#define\tEINTEGRITY\t97" not in (FREEBSD_SRC / "sys/sys/errno.h").read_text(encoding="utf-8"):
|
||||
raise GateFailure("INFRA_BLOCKED", "FreeBSD EINTEGRITY positive errno changed")
|
||||
lz4_source = sources["repo-pre-15/src/decompressor_lz4.c"]
|
||||
if "while (ip < iend)" not in lz4_source or "if (*ip++ != 0)" not in lz4_source:
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen DUT no longer has the zero-tail full-decode rule")
|
||||
if "m->map->m_plen = m->compressedblks << m->sbi->blkszbits;" not in sources["repo-pre-15/src/zmap.c"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen DUT no longer maps legacy pcluster input by whole blocks")
|
||||
if "ret = LZ4_decompress_safe(src + inputmargin, out," not in sources["src-linux/decompressor.c"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "Linux LZ4 comparison anchor changed")
|
||||
write_json(OUTPUT / "source-sha256.json", source_hashes)
|
||||
write_json(OUTPUT / "freebsd-source.json", {"head": freebsd_head, "sha256": freebsd_hashes})
|
||||
cleanup_contract = verify_cleanup_source(sources["repo-pre-15/src/zdata.c"])
|
||||
full_codec, partial_codec = load_liblz4()
|
||||
with tempfile.TemporaryDirectory(prefix="p15-062-g04-") as temporary:
|
||||
owned_temp = temporary
|
||||
temp = Path(temporary)
|
||||
legacy_source = make_legacy_source(temp / "legacy-source-check")
|
||||
generator_records, worktrees = build_versions(temp, legacy_source)
|
||||
dut = compile_dut_decoder(temp, lz4_source)
|
||||
dut_cases = []
|
||||
for record in generator_records:
|
||||
image_path = temp / f"images-{record['name']}/legacy-a.erofs"
|
||||
image = image_path.read_bytes()
|
||||
expected = SPEC["legacy_fixture"]["extents"][1]
|
||||
block = image[expected["physical_offset"] : expected["physical_offset"] + expected["physical_length"]]
|
||||
stream = block[: expected["consumed_bytes"]]
|
||||
zero_tail_result, zero_tail_output, zero_tail_guards = dut_call(
|
||||
dut[1], dut[2], block, expected["logical_length"], False
|
||||
)
|
||||
exact_result, exact_output, exact_guards = dut_call(
|
||||
dut[1], dut[2], stream, expected["logical_length"], False
|
||||
)
|
||||
nonzero = bytearray(block)
|
||||
nonzero[expected["consumed_bytes"]] = 1
|
||||
nonzero_result, _, nonzero_guards = dut_call(
|
||||
dut[1], dut[2], bytes(nonzero), expected["logical_length"], False
|
||||
)
|
||||
truncated_result, _, truncated_guards = dut_call(
|
||||
dut[1], dut[2], stream[:-1], expected["logical_length"], False
|
||||
)
|
||||
lib_full_result, _ = liblz4_call(full_codec, block, expected["logical_length"])
|
||||
lib_exact_result, lib_exact_output = liblz4_call(full_codec, stream, expected["logical_length"])
|
||||
expected_output = legacy_source[expected["logical_offset"] :]
|
||||
if (
|
||||
zero_tail_result != 0
|
||||
or zero_tail_output != expected_output
|
||||
or exact_result != 0
|
||||
or exact_output != expected_output
|
||||
or nonzero_result != 97
|
||||
or truncated_result != 97
|
||||
or lib_full_result >= 0
|
||||
or lib_exact_result != expected["logical_length"]
|
||||
or lib_exact_output != expected_output
|
||||
or not all((zero_tail_guards, exact_guards, nonzero_guards, truncated_guards))
|
||||
):
|
||||
raise GateFailure("INFRA_BLOCKED", f"DUT/liblz4 legacy oracle mismatch for {record['name']}")
|
||||
dut_cases.append({
|
||||
"current_dut_exact_errno": exact_result,
|
||||
"current_dut_nonzero_tail_errno": nonzero_result,
|
||||
"current_dut_truncated_errno": truncated_result,
|
||||
"current_dut_zero_tail_errno": zero_tail_result,
|
||||
"exact_policy_would_accept": expected["consumed_bytes"] == expected["physical_length"],
|
||||
"guards_unchanged": True,
|
||||
"liblz4_exact_result": lib_exact_result,
|
||||
"liblz4_physical_input_result": lib_full_result,
|
||||
"name": record["name"],
|
||||
"physical_input_bytes": expected["physical_length"],
|
||||
"stream_consumed_bytes": expected["consumed_bytes"],
|
||||
"zero_tail_bytes": expected["zero_tail_bytes"],
|
||||
})
|
||||
partial_record = verify_partial_fixture(temp, worktrees["v1.8.6"], full_codec, partial_codec, dut)
|
||||
write_json(OUTPUT / "generator-ledger.json", generator_records)
|
||||
write_json(OUTPUT / "dut-cases.json", dut_cases)
|
||||
write_json(OUTPUT / "partial-oracle.json", partial_record)
|
||||
write_json(OUTPUT / "cleanup-ledger.json", {
|
||||
**cleanup_contract,
|
||||
"decoder_allocations": 0,
|
||||
"decoder_owned_buffers": 0,
|
||||
"guards_unchanged_for_all_cases": True,
|
||||
"input_buffer_owner": "z_erofs_read_extent caller",
|
||||
"output_buffer_owner": "z_erofs_read_extent caller",
|
||||
"typed_corruption_errno": 97,
|
||||
})
|
||||
incompatible = [case["name"] for case in dut_cases if not case["exact_policy_would_accept"]]
|
||||
if incompatible != [version["name"] for version in SPEC["upstream"]["versions"]]:
|
||||
raise GateFailure("INFRA_BLOCKED", f"unexpected incompatibility set: {incompatible}")
|
||||
result = {
|
||||
"b26": "STOP-NO-SOURCE",
|
||||
"candidate": "P15-062",
|
||||
"cleanup": "PASS",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"gate": "G04",
|
||||
"historical_generations": incompatible,
|
||||
"oracle": "independent raw LZ4 parser + liblz4 1.10.0 + matching erofs-utils fsck",
|
||||
"partial_reference": "PASS",
|
||||
"qemu": "NOT_RUN",
|
||||
"qemu_reason": "host oracle proves exact-full would reject reproducible legal legacy images",
|
||||
"reason": "all three reproducible legacy mkfs generations store a short valid final raw LZ4 stream followed by zero bytes inside the block-sized m_plen",
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"resolved_base": resolved,
|
||||
"schema": 1,
|
||||
"status": "STOP",
|
||||
"typed_errno": "PASS",
|
||||
}
|
||||
write_json(OUTPUT / "result.json", result)
|
||||
exit_code = 1
|
||||
cleanup_record = {
|
||||
"owned_temp": owned_temp,
|
||||
"owned_temp_removed": owned_temp is not None and not Path(owned_temp).exists(),
|
||||
"protected_pid_touched": False,
|
||||
"protected_port_touched": False,
|
||||
"qemu_started": False,
|
||||
}
|
||||
if not cleanup_record["owned_temp_removed"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "owned gate temporary directory survived cleanup")
|
||||
write_json(OUTPUT / "owned-cleanup.json", cleanup_record)
|
||||
except GateFailure as failure:
|
||||
result = {
|
||||
"b26": "STOP-NO-SOURCE" if failure.status == "STOP" else "NOT_RUN",
|
||||
"candidate": "P15-062",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"gate": "G04",
|
||||
"qemu": "NOT_RUN",
|
||||
"reason": failure.reason,
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"schema": 1,
|
||||
"status": failure.status,
|
||||
}
|
||||
write_json(OUTPUT / "result.json", result)
|
||||
exit_code = 21 if failure.status == "INFRA_BLOCKED" else 1
|
||||
except (OSError, subprocess.SubprocessError, ValueError) as failure:
|
||||
result = {
|
||||
"b26": "NOT_RUN",
|
||||
"candidate": "P15-062",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"gate": "G04",
|
||||
"qemu": "NOT_RUN",
|
||||
"reason": f"gate infrastructure failure: {failure}",
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"schema": 1,
|
||||
"status": "INFRA_BLOCKED",
|
||||
}
|
||||
write_json(OUTPUT / "result.json", result)
|
||||
exit_code = 21
|
||||
finally:
|
||||
finalize()
|
||||
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
raise SystemExit(exit_code)
|
||||
PY
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"benchmark": {
|
||||
"iterations_per_sample": 64,
|
||||
"samples": 7
|
||||
},
|
||||
"budgets": {
|
||||
"global_cached_contexts_per_codec": 16,
|
||||
"global_resident_bytes": 8388608,
|
||||
"mount_cached_contexts_per_codec": 2,
|
||||
"mount_resident_bytes": 1048576,
|
||||
"wrapper_bytes_per_context": 128
|
||||
},
|
||||
"candidate": "P15-076",
|
||||
"fixture_assets": {
|
||||
"repo-pre-15/tests/pre15/fixtures/B28-partial-fixtures.py": "9d9ac67b09667d74e99d7f5e5e5edd3ac467f11d579196fda9a9b7599c862b30",
|
||||
"repo-pre-15/tests/pre15/fixtures/B28-partial.json": "ccdc62718942739ae9b0a60600f9958247abf005e3751bbc76a63173239f0e48"
|
||||
},
|
||||
"freebsd": {
|
||||
"head": "106727738dcfb6c001b46f25363b91cece970085",
|
||||
"sha256": {
|
||||
"sys/contrib/xz-embedded/freebsd/xz_config.h": "fdc4fcf394b4014b6c9a39a27fb6988d77c840e6e0470709acbdaf541a7dd949",
|
||||
"sys/contrib/xz-embedded/linux/include/linux/xz.h": "4255bf4d723746761816da5787ca47acf8b229d3e7a37502a12f3d532772026b",
|
||||
"sys/contrib/xz-embedded/linux/lib/xz/xz_dec_lzma2.c": "fca31a150d259f5e7e5aa82392fbe9b2ac6331f5da9e24834ece81f0e30d5c65",
|
||||
"sys/contrib/xz-embedded/linux/lib/xz/xz_private.h": "8a8483369795154db376b82700be345917050ad78c089dd289b273c5718c8177",
|
||||
"sys/contrib/zlib/inflate.c": "413abc042c18d267619441eade265505952dbcf46dc9b2c2967d2fec1281086c",
|
||||
"sys/contrib/zlib/zlib.h": "3c4f75f90589af70540aeaf23610e2b299bb498826a34c4c16ec92233dd68693",
|
||||
"sys/contrib/zstd/lib/decompress/zstd_decompress.c": "029580818b7e9cd38d9d07c63516b00ceaa943ffcfbd099fc5ccbe3628fa362f",
|
||||
"sys/contrib/zstd/lib/zstd.h": "9b4bc8245565c98ccfc61c07749928b57e7c0f6fddb0530c4f6aa1971893d88b",
|
||||
"sys/kern/vfs_init.c": "4092238096a68edbc27697fa091e11c146702fc147de0015dec882e955a715a6",
|
||||
"sys/sys/mount.h": "2bc2017d63389c39dfee52b3041970450ef1eebaa149c8fe5e84a871c0f0b738",
|
||||
"sys/vm/uma.h": "398bbc3787c363b0a71c4f6d724739018f2b54607962d5d8aea5a547011226a3"
|
||||
}
|
||||
},
|
||||
"gate": "G05",
|
||||
"libraries": {
|
||||
"liblzma": "5.8.1",
|
||||
"libzstd": "1.5.7",
|
||||
"zlib": "1.3.1"
|
||||
},
|
||||
"required_base": "3e9bc3f03ba9c39c38cc40f2f08eb6e769557f55",
|
||||
"schema": 1,
|
||||
"source_sha256": {
|
||||
"repo-pre-15/src/decompressor.c": "308c3c151fcacf423a79331818cd8f08b3093aadc4def869b03e5c932937e350",
|
||||
"repo-pre-15/src/decompressor_deflate.c": "d3efece85fee05affa0fcac5f763d886bcaebaeaf4b5278215631c5801478caa",
|
||||
"repo-pre-15/src/decompressor_lzma.c": "ebe8c7b9f70a771c5d01c03665c905982b3bfe8e088b59711ab6d8f07cf6c733",
|
||||
"repo-pre-15/src/decompressor_zstd.c": "8c1b7d551b0dfed4f1ccd3636632bd1dc06e1d3890aa8dc8f8e309fd8aa1ef5c",
|
||||
"repo-pre-15/src/internal.h": "d1884395c040e2d1dcb31473b3c007c301248b6ff9304632b6b1b51602927ca9",
|
||||
"repo-pre-15/src/super.c": "61f31906bb7d1872c99cd3cf3859a34daf29a02d91a3caa1bfc8187af562a3d2",
|
||||
"repo-pre-15/src/zdata.c": "614cd2a704716c166f4a889715129f7abcaac528651433370474b1334f2e5502",
|
||||
"src-linux/decompressor_deflate.c": "0ddb56c27cacc63aa9f7a10a55cfd76ee2ba0bab15cbac7f6d8684203fb2c67b",
|
||||
"src-linux/decompressor_lzma.c": "5a23b4455767c30c8d90e3e5b10bc3c430d94359727affae2e6f7824f106d5a8",
|
||||
"src-linux/decompressor_zstd.c": "4f8a4961fa6b4219fcfe91c04ba37179ec4dde296fcf9f91a5d5b27d8c272607"
|
||||
},
|
||||
"thresholds": {
|
||||
"minimum_baseline_allocator_share_percent": 10.0,
|
||||
"minimum_context_cost_reduction_percent": 25.0
|
||||
},
|
||||
"tools": {
|
||||
"cc": {
|
||||
"path": "/usr/bin/x86_64-linux-gnu-gcc-14",
|
||||
"sha256": "a23ecab8ff08f09ad8c80602c2c5df7f49e09c25905cb8975902e101bf72635f"
|
||||
},
|
||||
"fsck.erofs": {
|
||||
"path": "/usr/bin/fsck.erofs",
|
||||
"sha256": "09bdee6a2dc7ccdc05547470d2b1f66dc5e1007890b8c3d8f1352e5767eb6b29"
|
||||
},
|
||||
"liblzma": {
|
||||
"path": "/usr/lib/x86_64-linux-gnu/liblzma.so.5.8.1",
|
||||
"sha256": "01e05a4b5268805a10a2414a8904fb1fb45c60626dce53b5ea19c1d491049b99"
|
||||
},
|
||||
"libz": {
|
||||
"path": "/usr/lib/x86_64-linux-gnu/libz.so.1.3.1",
|
||||
"sha256": "85590dd58edf5445e18bc7193e5ebc01ac5841f1ae187e97705a662e90c6421e"
|
||||
},
|
||||
"libzstd": {
|
||||
"path": "/usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7",
|
||||
"sha256": "27f07c9a49c2c956bcfb64cd4712976586a66facbf15fc7f09bc37413b5f2b21"
|
||||
},
|
||||
"mkfs.erofs": {
|
||||
"path": "/usr/bin/mkfs.erofs",
|
||||
"sha256": "544e2e502db2302710d7ffdb115eeb26bec374fb4a1ecf12c7aec1156dacc94c"
|
||||
},
|
||||
"python3": {
|
||||
"path": "/usr/bin/python3.13",
|
||||
"sha256": "4703a3d15898c0b5d81c3f939e93bdd8ca6116342093fb160ab1e01860dd7d8b"
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+853
@@ -0,0 +1,853 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
umask 022
|
||||
|
||||
gate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||
root=$(CDPATH= cd -- "$gate_dir/../../../.." && pwd -P)
|
||||
input=$gate_dir/P15-076-input.json
|
||||
freebsd_src=${FREEBSD_SRC:-/work/build/freebsd-src}
|
||||
base=
|
||||
output=
|
||||
|
||||
while test "$#" -gt 0; do
|
||||
case "$1" in
|
||||
--base)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 2; }
|
||||
base=$2
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 2; }
|
||||
output=$2
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
printf 'unknown argument: %s\n' "$1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 2; }
|
||||
test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 2; }
|
||||
test -f "$input" || { printf 'missing input: %s\n' "$input" >&2; exit 2; }
|
||||
test -d "$freebsd_src/sys" || { printf 'missing FreeBSD source: %s\n' "$freebsd_src" >&2; exit 2; }
|
||||
case "$output" in
|
||||
/*) ;;
|
||||
*) output=$PWD/$output ;;
|
||||
esac
|
||||
test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 2; }
|
||||
mkdir -p "$output"
|
||||
|
||||
python3 -B - "$root" "$input" "$base" "$output" "$freebsd_src" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(sys.argv[1])
|
||||
INPUT = Path(sys.argv[2])
|
||||
REQUESTED_BASE = sys.argv[3]
|
||||
OUTPUT = Path(sys.argv[4])
|
||||
FREEBSD_SRC = Path(sys.argv[5])
|
||||
SPEC = json.loads(INPUT.read_text(encoding="ascii"))
|
||||
DUT = ROOT / "repo-pre-15"
|
||||
|
||||
|
||||
class GateStop(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class InfraBlocked(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class RunnerFail(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def sha256_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def sha256_path(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def write_json(path: Path, value: Any) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
|
||||
|
||||
def run(argv: list[str], cwd: Path, log: Path, timeout: int = 120) -> subprocess.CompletedProcess[str]:
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
argv, cwd=cwd, check=False, text=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=timeout,
|
||||
)
|
||||
except subprocess.TimeoutExpired as error:
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
log.write_text(f"$ {' '.join(argv)}\nTIMEOUT after {timeout}s\n", encoding="ascii")
|
||||
raise InfraBlocked(f"command timed out after {timeout}s: {' '.join(argv)}") from error
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
log.write_text(
|
||||
f"$ {' '.join(argv)}\n{completed.stdout}\n[exit {completed.returncode}]\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return completed
|
||||
|
||||
|
||||
def checked_run(argv: list[str], cwd: Path, log: Path, timeout: int = 120) -> str:
|
||||
completed = run(argv, cwd, log, timeout)
|
||||
if completed.returncode != 0:
|
||||
raise RunnerFail(f"command failed ({completed.returncode}): {' '.join(argv)}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def git(path: Path, *args: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(path), *args], check=False, text=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise InfraBlocked(f"git {' '.join(args)} failed: {completed.stdout.strip()}")
|
||||
return completed.stdout.strip()
|
||||
|
||||
|
||||
def source_at(commit: str, relative: str) -> bytes:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(ROOT), "show", f"{commit}:{relative}"],
|
||||
check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise InfraBlocked(f"cannot read frozen source {relative}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def verify_identity() -> dict[str, Any]:
|
||||
if SPEC.get("schema") != 1 or SPEC.get("candidate") != "P15-076" or SPEC.get("gate") != "G05":
|
||||
raise InfraBlocked("invalid P15-076 input identity")
|
||||
resolved = git(ROOT, "rev-parse", f"{REQUESTED_BASE}^{{commit}}")
|
||||
if resolved != SPEC["required_base"]:
|
||||
raise InfraBlocked(f"P15-076 must replay {SPEC['required_base']}, got {resolved}")
|
||||
source_hashes = {
|
||||
relative: sha256_bytes(source_at(resolved, relative))
|
||||
for relative in SPEC["source_sha256"]
|
||||
}
|
||||
if source_hashes != SPEC["source_sha256"]:
|
||||
raise InfraBlocked("frozen DUT/Linux source identity changed")
|
||||
fixture_hashes = {
|
||||
relative: sha256_path(ROOT / relative)
|
||||
for relative in SPEC["fixture_assets"]
|
||||
}
|
||||
if fixture_hashes != SPEC["fixture_assets"]:
|
||||
raise InfraBlocked("frozen B28 fixture identity changed")
|
||||
freebsd_head = git(FREEBSD_SRC, "rev-parse", "HEAD")
|
||||
freebsd_hashes = {
|
||||
relative: sha256_path(FREEBSD_SRC / relative)
|
||||
for relative in SPEC["freebsd"]["sha256"]
|
||||
}
|
||||
if freebsd_head != SPEC["freebsd"]["head"] or freebsd_hashes != SPEC["freebsd"]["sha256"]:
|
||||
raise InfraBlocked("frozen FreeBSD API/source identity changed")
|
||||
tool_hashes = {}
|
||||
for name, record in SPEC["tools"].items():
|
||||
path = Path(record["path"])
|
||||
if not path.is_file():
|
||||
raise InfraBlocked(f"missing frozen tool: {path}")
|
||||
tool_hashes[name] = sha256_path(path)
|
||||
if tool_hashes[name] != record["sha256"]:
|
||||
raise InfraBlocked(f"frozen tool changed: {name}")
|
||||
versions = {
|
||||
package: subprocess.check_output(
|
||||
["pkg-config", "--modversion", package], text=True, timeout=30
|
||||
).strip()
|
||||
for package in ("liblzma", "libzstd", "zlib")
|
||||
}
|
||||
if versions != SPEC["libraries"]:
|
||||
raise InfraBlocked(f"library versions changed: {versions}")
|
||||
return {
|
||||
"base": resolved,
|
||||
"fixture_sha256": fixture_hashes,
|
||||
"freebsd_head": freebsd_head,
|
||||
"freebsd_sha256": freebsd_hashes,
|
||||
"libraries": versions,
|
||||
"source_sha256": source_hashes,
|
||||
"tool_sha256": tool_hashes,
|
||||
}
|
||||
|
||||
|
||||
XZ_CONFIG = r'''#ifndef P15_076_XZ_CONFIG_H
|
||||
#define P15_076_XZ_CONFIG_H
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <contrib/xz-embedded/linux/include/linux/xz.h>
|
||||
void *gate_xz_malloc(size_t size);
|
||||
void gate_xz_free(void *address);
|
||||
#define XZ_PREBOOT 1
|
||||
#undef XZ_EXTERN
|
||||
#define XZ_EXTERN extern
|
||||
#define STATIC
|
||||
#define INIT
|
||||
#define bool int
|
||||
#define true 1
|
||||
#define false 0
|
||||
#define GFP_KERNEL 0
|
||||
#define kmalloc(size, flags) gate_xz_malloc(size)
|
||||
#define kfree(address) gate_xz_free(address)
|
||||
#define vmalloc(size) gate_xz_malloc(size)
|
||||
#define vfree(address) gate_xz_free(address)
|
||||
#define memeq(a, b, size) (memcmp((a), (b), (size)) == 0)
|
||||
#define memzero(buffer, size) memset((buffer), 0, (size))
|
||||
#define min(a, b) ((a) < (b) ? (a) : (b))
|
||||
#define min_t(type, a, b) min((a), (b))
|
||||
static inline uint32_t get_le32(const void *address)
|
||||
{
|
||||
const unsigned char *p = address;
|
||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
|
||||
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||||
}
|
||||
#endif
|
||||
'''
|
||||
|
||||
|
||||
BENCHMARK_SOURCE = r'''#define _POSIX_C_SOURCE 200809L
|
||||
#define ZSTD_STATIC_LINKING_ONLY
|
||||
#include <inttypes.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <zlib.h>
|
||||
#include <zstd.h>
|
||||
#include <contrib/xz-embedded/linux/include/linux/xz.h>
|
||||
|
||||
struct allocation { size_t size; };
|
||||
struct tracker {
|
||||
uint64_t alloc_calls;
|
||||
uint64_t free_calls;
|
||||
size_t live_bytes;
|
||||
size_t peak_bytes;
|
||||
};
|
||||
static struct tracker *active_tracker;
|
||||
|
||||
static void fail(const char *message)
|
||||
{
|
||||
fprintf(stderr, "%s\n", message);
|
||||
exit(2);
|
||||
}
|
||||
|
||||
static void *tracked_alloc(size_t size)
|
||||
{
|
||||
struct allocation *allocation;
|
||||
if (active_tracker == NULL || size > SIZE_MAX - sizeof(*allocation))
|
||||
return NULL;
|
||||
allocation = malloc(sizeof(*allocation) + size);
|
||||
if (allocation == NULL)
|
||||
return NULL;
|
||||
allocation->size = size;
|
||||
active_tracker->alloc_calls++;
|
||||
active_tracker->live_bytes += size;
|
||||
if (active_tracker->live_bytes > active_tracker->peak_bytes)
|
||||
active_tracker->peak_bytes = active_tracker->live_bytes;
|
||||
return allocation + 1;
|
||||
}
|
||||
|
||||
static void tracked_free(void *address)
|
||||
{
|
||||
struct allocation *allocation;
|
||||
if (address == NULL)
|
||||
return;
|
||||
allocation = (struct allocation *)address - 1;
|
||||
if (active_tracker == NULL || allocation->size > active_tracker->live_bytes)
|
||||
fail("allocation tracker underflow");
|
||||
active_tracker->free_calls++;
|
||||
active_tracker->live_bytes -= allocation->size;
|
||||
free(allocation);
|
||||
}
|
||||
|
||||
void *gate_xz_malloc(size_t size) { return tracked_alloc(size); }
|
||||
void gate_xz_free(void *address) { tracked_free(address); }
|
||||
|
||||
static voidpf zalloc_tracked(voidpf opaque, uInt items, uInt size)
|
||||
{
|
||||
(void)opaque;
|
||||
if (items != 0 && size > SIZE_MAX / items)
|
||||
return NULL;
|
||||
return tracked_alloc((size_t)items * size);
|
||||
}
|
||||
static void zfree_tracked(voidpf opaque, voidpf address)
|
||||
{
|
||||
(void)opaque;
|
||||
tracked_free(address);
|
||||
}
|
||||
static void *zstd_alloc_tracked(void *opaque, size_t size)
|
||||
{
|
||||
(void)opaque;
|
||||
return tracked_alloc(size);
|
||||
}
|
||||
static void zstd_free_tracked(void *opaque, void *address)
|
||||
{
|
||||
(void)opaque;
|
||||
tracked_free(address);
|
||||
}
|
||||
|
||||
static unsigned char *read_file(const char *path, size_t *sizep)
|
||||
{
|
||||
FILE *stream;
|
||||
unsigned char *data;
|
||||
long length;
|
||||
stream = fopen(path, "rb");
|
||||
if (stream == NULL || fseek(stream, 0, SEEK_END) != 0)
|
||||
fail("cannot open input");
|
||||
length = ftell(stream);
|
||||
if (length <= 0 || fseek(stream, 0, SEEK_SET) != 0)
|
||||
fail("invalid input size");
|
||||
data = malloc((size_t)length);
|
||||
if (data == NULL || fread(data, 1, (size_t)length, stream) != (size_t)length)
|
||||
fail("cannot read input");
|
||||
fclose(stream);
|
||||
*sizep = (size_t)length;
|
||||
return data;
|
||||
}
|
||||
|
||||
static uint64_t elapsed_ns(const struct timespec *start, const struct timespec *end)
|
||||
{
|
||||
return (uint64_t)(end->tv_sec - start->tv_sec) * 1000000000ULL +
|
||||
(uint64_t)(end->tv_nsec - start->tv_nsec);
|
||||
}
|
||||
|
||||
static int run_lzma(struct xz_dec_microlzma *state, const unsigned char *input,
|
||||
size_t input_size, unsigned char *output, size_t output_size)
|
||||
{
|
||||
struct xz_buf buffer = { 0 };
|
||||
enum xz_ret ret;
|
||||
xz_dec_microlzma_reset(state, (uint32_t)input_size,
|
||||
(uint32_t)output_size, 1);
|
||||
buffer.in = input;
|
||||
buffer.in_size = input_size;
|
||||
buffer.out = output;
|
||||
buffer.out_size = output_size;
|
||||
ret = xz_dec_microlzma_run(state, &buffer);
|
||||
return ret == XZ_STREAM_END && buffer.in_pos == input_size &&
|
||||
buffer.out_pos == output_size ? 0 : -1;
|
||||
}
|
||||
|
||||
static int run_deflate(z_stream *stream, const unsigned char *input,
|
||||
size_t input_size, unsigned char *output, size_t output_size)
|
||||
{
|
||||
int ret = Z_OK;
|
||||
stream->next_in = (Bytef *)(uintptr_t)input;
|
||||
stream->avail_in = (uInt)input_size;
|
||||
stream->next_out = output;
|
||||
stream->avail_out = (uInt)output_size;
|
||||
while (stream->avail_out != 0) {
|
||||
uInt in_before = stream->avail_in;
|
||||
uInt out_before = stream->avail_out;
|
||||
ret = inflate(stream, Z_SYNC_FLUSH);
|
||||
if (ret == Z_STREAM_END)
|
||||
break;
|
||||
if (ret != Z_OK || (stream->avail_in == in_before &&
|
||||
stream->avail_out == out_before))
|
||||
return -1;
|
||||
}
|
||||
return ret == Z_STREAM_END && stream->avail_in == 0 &&
|
||||
stream->avail_out == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
static int run_zstd(ZSTD_DCtx *context, const unsigned char *input,
|
||||
size_t input_size, unsigned char *output, size_t output_size)
|
||||
{
|
||||
ZSTD_inBuffer in_buffer = { input, input_size, 0 };
|
||||
ZSTD_outBuffer out_buffer = { output, output_size, 0 };
|
||||
size_t ret = 1;
|
||||
while (out_buffer.pos != out_buffer.size) {
|
||||
size_t in_before = in_buffer.pos;
|
||||
size_t out_before = out_buffer.pos;
|
||||
ret = ZSTD_decompressStream(context, &out_buffer, &in_buffer);
|
||||
if (ZSTD_isError(ret) || (in_buffer.pos == in_before &&
|
||||
out_buffer.pos == out_before))
|
||||
return -1;
|
||||
if (ret == 0)
|
||||
break;
|
||||
}
|
||||
return ret == 0 && in_buffer.pos == input_size &&
|
||||
out_buffer.pos == output_size ? 0 : -1;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *codec, *mode;
|
||||
unsigned char *input, *expected, *output;
|
||||
struct tracker tracker = { 0 };
|
||||
struct timespec start, end;
|
||||
size_t input_size, output_size;
|
||||
uint64_t cpu_ns;
|
||||
int iterations, pooled;
|
||||
struct xz_dec_microlzma *xz_state = NULL;
|
||||
z_stream zstream = { 0 };
|
||||
ZSTD_DCtx *zstd_context = NULL;
|
||||
ZSTD_customMem zstd_memory = {
|
||||
zstd_alloc_tracked, zstd_free_tracked, NULL
|
||||
};
|
||||
|
||||
if (argc != 6)
|
||||
fail("usage: benchmark CODEC MODE STREAM EXPECTED ITERATIONS");
|
||||
codec = argv[1];
|
||||
mode = argv[2];
|
||||
pooled = strcmp(mode, "pooled") == 0;
|
||||
if (!pooled && strcmp(mode, "baseline") != 0)
|
||||
fail("invalid mode");
|
||||
iterations = atoi(argv[5]);
|
||||
if (iterations <= 0)
|
||||
fail("invalid iterations");
|
||||
input = read_file(argv[3], &input_size);
|
||||
expected = read_file(argv[4], &output_size);
|
||||
output = malloc(output_size);
|
||||
if (output == NULL)
|
||||
fail("cannot allocate output");
|
||||
active_tracker = &tracker;
|
||||
|
||||
if (pooled && strcmp(codec, "lzma") == 0) {
|
||||
xz_state = xz_dec_microlzma_alloc(XZ_SINGLE, 65536);
|
||||
if (xz_state == NULL)
|
||||
fail("pooled lzma init failed");
|
||||
} else if (pooled && strcmp(codec, "deflate") == 0) {
|
||||
zstream.zalloc = zalloc_tracked;
|
||||
zstream.zfree = zfree_tracked;
|
||||
if (inflateInit2(&zstream, -15) != Z_OK)
|
||||
fail("pooled deflate init failed");
|
||||
} else if (pooled && strcmp(codec, "zstd") == 0) {
|
||||
zstd_context = ZSTD_createDCtx_advanced(zstd_memory);
|
||||
if (zstd_context == NULL || ZSTD_isError(ZSTD_DCtx_setParameter(
|
||||
zstd_context, ZSTD_d_windowLogMax, 16)))
|
||||
fail("pooled zstd init failed");
|
||||
}
|
||||
|
||||
if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start) != 0)
|
||||
fail("clock start failed");
|
||||
for (int iteration = 0; iteration < iterations; ++iteration) {
|
||||
int error = 0;
|
||||
memset(output, 0xa5, output_size);
|
||||
if (strcmp(codec, "lzma") == 0) {
|
||||
struct xz_dec_microlzma *state = xz_state;
|
||||
if (!pooled)
|
||||
state = xz_dec_microlzma_alloc(XZ_SINGLE, 65536);
|
||||
if (state == NULL || run_lzma(state, input, input_size,
|
||||
output, output_size) != 0)
|
||||
error = 1;
|
||||
if (!pooled && state != NULL)
|
||||
xz_dec_microlzma_end(state);
|
||||
} else if (strcmp(codec, "deflate") == 0) {
|
||||
z_stream stream = { 0 };
|
||||
z_stream *current = &zstream;
|
||||
if (!pooled) {
|
||||
stream.zalloc = zalloc_tracked;
|
||||
stream.zfree = zfree_tracked;
|
||||
current = &stream;
|
||||
if (inflateInit2(current, -15) != Z_OK)
|
||||
error = 1;
|
||||
} else if (iteration != 0 && inflateReset2(current, -15) != Z_OK) {
|
||||
error = 1;
|
||||
}
|
||||
if (!error && run_deflate(current, input, input_size,
|
||||
output, output_size) != 0)
|
||||
error = 1;
|
||||
if (!pooled && current->state != NULL && inflateEnd(current) != Z_OK)
|
||||
error = 1;
|
||||
} else if (strcmp(codec, "zstd") == 0) {
|
||||
ZSTD_DCtx *context = zstd_context;
|
||||
if (!pooled) {
|
||||
context = ZSTD_createDCtx_advanced(zstd_memory);
|
||||
if (context == NULL || ZSTD_isError(ZSTD_DCtx_setParameter(
|
||||
context, ZSTD_d_windowLogMax, 16)))
|
||||
error = 1;
|
||||
} else if (iteration != 0 && ZSTD_isError(ZSTD_DCtx_reset(
|
||||
context, ZSTD_reset_session_only))) {
|
||||
error = 1;
|
||||
}
|
||||
if (!error && run_zstd(context, input, input_size,
|
||||
output, output_size) != 0)
|
||||
error = 1;
|
||||
if (!pooled && context != NULL && ZSTD_isError(ZSTD_freeDCtx(context)))
|
||||
error = 1;
|
||||
} else {
|
||||
fail("unknown codec");
|
||||
}
|
||||
if (error || memcmp(output, expected, output_size) != 0)
|
||||
fail("decode or output comparison failed");
|
||||
}
|
||||
if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end) != 0)
|
||||
fail("clock end failed");
|
||||
|
||||
if (xz_state != NULL)
|
||||
xz_dec_microlzma_end(xz_state);
|
||||
if (zstream.state != NULL && inflateEnd(&zstream) != Z_OK)
|
||||
fail("pooled deflate fini failed");
|
||||
if (zstd_context != NULL && ZSTD_isError(ZSTD_freeDCtx(zstd_context)))
|
||||
fail("pooled zstd fini failed");
|
||||
if (tracker.live_bytes != 0 || tracker.alloc_calls != tracker.free_calls)
|
||||
fail("context cleanup mismatch");
|
||||
cpu_ns = elapsed_ns(&start, &end);
|
||||
printf("alloc_calls=%" PRIu64 " cpu_ns=%" PRIu64
|
||||
" free_calls=%" PRIu64 " iterations=%d peak_bytes=%zu\n",
|
||||
tracker.alloc_calls, cpu_ns, tracker.free_calls, iterations,
|
||||
tracker.peak_bytes);
|
||||
free(output);
|
||||
free(expected);
|
||||
free(input);
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
def compile_prototype(temp: Path) -> tuple[Path, dict[str, str]]:
|
||||
shadow = temp / "shadow/contrib/xz-embedded/freebsd"
|
||||
shadow.mkdir(parents=True)
|
||||
config = shadow / "xz_config.h"
|
||||
source = temp / "p15-076-prototype.c"
|
||||
binary = temp / "p15-076-prototype"
|
||||
config.write_text(XZ_CONFIG, encoding="ascii")
|
||||
source.write_text(BENCHMARK_SOURCE, encoding="ascii")
|
||||
checked_run(
|
||||
[
|
||||
SPEC["tools"]["cc"]["path"], "-O2", "-std=c17", "-Wall",
|
||||
"-Wextra", "-Werror", "-DXZ_DEC_MICROLZMA", "-DXZ_DEC_SINGLE",
|
||||
f"-I{temp / 'shadow'}", "-idirafter", str(FREEBSD_SRC / "sys"),
|
||||
str(source),
|
||||
str(FREEBSD_SRC / "sys/contrib/xz-embedded/linux/lib/xz/xz_dec_lzma2.c"),
|
||||
"-o", str(binary), "-lz", "-lzstd",
|
||||
],
|
||||
temp, OUTPUT / "logs/prototype-build.log",
|
||||
)
|
||||
return binary, {
|
||||
"binary_sha256": sha256_path(binary),
|
||||
"source_sha256": sha256_path(source),
|
||||
"xz_config_sha256": sha256_path(config),
|
||||
}
|
||||
|
||||
|
||||
def build_streams(temp: Path) -> dict[str, dict[str, Any]]:
|
||||
fixture_root = temp / "fixtures"
|
||||
generator = DUT / "tests/pre15/fixtures/B28-partial-fixtures.py"
|
||||
spec_path = DUT / "tests/pre15/fixtures/B28-partial.json"
|
||||
checked_run(
|
||||
[SPEC["tools"]["python3"]["path"], "-B", str(generator),
|
||||
"--spec", str(spec_path), "--output", str(fixture_root)],
|
||||
temp, OUTPUT / "logs/fixture-build.log",
|
||||
)
|
||||
fixture_spec = json.loads(spec_path.read_text(encoding="ascii"))
|
||||
logical = (fixture_root / "sources/stream/payload.bin").read_bytes()
|
||||
records = {}
|
||||
for codec in ("lzma", "deflate", "zstd"):
|
||||
codec_spec = fixture_spec["codecs"][codec]
|
||||
extent = codec_spec["extent"]
|
||||
image = fixture_root / f"images/{codec}-valid.erofs"
|
||||
with image.open("rb") as stream:
|
||||
stream.seek(extent["physical_offset"] + extent["leading_zero_bytes"])
|
||||
data = stream.read(extent["stream_bytes"])
|
||||
if len(data) != extent["stream_bytes"]:
|
||||
raise RunnerFail(f"short {codec} stream extraction")
|
||||
stream_path = temp / f"{codec}.stream"
|
||||
logical_path = temp / f"{codec}.logical"
|
||||
stream_path.write_bytes(data)
|
||||
logical_path.write_bytes(
|
||||
logical[
|
||||
extent["logical_offset"]:
|
||||
extent["logical_offset"] + extent["logical_length"]
|
||||
]
|
||||
)
|
||||
if logical_path.stat().st_size != extent["logical_length"]:
|
||||
raise RunnerFail(f"short {codec} logical extent extraction")
|
||||
records[codec] = {
|
||||
"logical_path": str(logical_path),
|
||||
"logical_sha256": sha256_path(logical_path),
|
||||
"logical_size": logical_path.stat().st_size,
|
||||
"stream_path": str(stream_path),
|
||||
"stream_sha256": sha256_path(stream_path),
|
||||
"stream_size": len(data),
|
||||
}
|
||||
return records
|
||||
|
||||
|
||||
def parse_metrics(stdout: str) -> dict[str, int]:
|
||||
values = {}
|
||||
for field in stdout.strip().split():
|
||||
key, value = field.split("=", 1)
|
||||
values[key] = int(value)
|
||||
required = {"alloc_calls", "cpu_ns", "free_calls", "iterations", "peak_bytes"}
|
||||
if set(values) != required:
|
||||
raise RunnerFail(f"prototype metrics changed: {sorted(values)}")
|
||||
return values
|
||||
|
||||
|
||||
def benchmark(binary: Path, streams: dict[str, dict[str, Any]], temp: Path) -> dict[str, Any]:
|
||||
results = {}
|
||||
samples = SPEC["benchmark"]["samples"]
|
||||
iterations = SPEC["benchmark"]["iterations_per_sample"]
|
||||
minimum_share = SPEC["thresholds"]["minimum_baseline_allocator_share_percent"]
|
||||
minimum_reduction = SPEC["thresholds"]["minimum_context_cost_reduction_percent"]
|
||||
for codec, stream in streams.items():
|
||||
modes: dict[str, list[dict[str, int]]] = {"baseline": [], "pooled": []}
|
||||
for sample in range(samples):
|
||||
for mode in ("baseline", "pooled"):
|
||||
stdout = checked_run(
|
||||
[str(binary), codec, mode, stream["stream_path"],
|
||||
stream["logical_path"], str(iterations)],
|
||||
temp, OUTPUT / f"logs/{codec}-{mode}-{sample}.log", 30,
|
||||
)
|
||||
modes[mode].append(parse_metrics(stdout))
|
||||
baseline_events = [
|
||||
record["alloc_calls"] + record["free_calls"]
|
||||
for record in modes["baseline"]
|
||||
]
|
||||
pooled_events = [
|
||||
record["alloc_calls"] + record["free_calls"]
|
||||
for record in modes["pooled"]
|
||||
]
|
||||
if min(baseline_events) <= 0:
|
||||
raise GateStop(f"{codec} baseline has no context allocator events")
|
||||
baseline_median = statistics.median(baseline_events)
|
||||
pooled_median = statistics.median(pooled_events)
|
||||
allocator_share = 100.0
|
||||
reduction = (baseline_median - pooled_median) * 100.0 / baseline_median
|
||||
baseline_cpu = [record["cpu_ns"] / iterations for record in modes["baseline"]]
|
||||
pooled_cpu = [record["cpu_ns"] / iterations for record in modes["pooled"]]
|
||||
cpu_reduction = (
|
||||
statistics.median(baseline_cpu) - statistics.median(pooled_cpu)
|
||||
) * 100.0 / statistics.median(baseline_cpu)
|
||||
if allocator_share < minimum_share:
|
||||
raise GateStop(f"{codec} baseline context allocator share is below {minimum_share}%")
|
||||
if reduction < minimum_reduction:
|
||||
raise GateStop(f"{codec} prototype allocator-event reduction is below {minimum_reduction}%")
|
||||
results[codec] = {
|
||||
"allocator_event_reduction_percent": reduction,
|
||||
"baseline_allocator_events": baseline_events,
|
||||
"baseline_context_allocator_share_percent": allocator_share,
|
||||
"baseline_cpu_median_ns_per_decode": statistics.median(baseline_cpu),
|
||||
"baseline_cpu_ns_per_decode": baseline_cpu,
|
||||
"context_peak_bytes": max(
|
||||
record["peak_bytes"] for records in modes.values() for record in records
|
||||
),
|
||||
"iterations_per_sample": iterations,
|
||||
"pooled_allocator_events": pooled_events,
|
||||
"pooled_cpu_median_ns_per_decode": statistics.median(pooled_cpu),
|
||||
"pooled_cpu_ns_per_decode": pooled_cpu,
|
||||
"pooled_cpu_reduction_percent": cpu_reduction,
|
||||
"samples": samples,
|
||||
"status": "GO",
|
||||
}
|
||||
return results
|
||||
|
||||
|
||||
MODEL_SOURCE = r'''P15-076 test-only state model
|
||||
|
||||
global UMA zones: one bounded zone per codec
|
||||
mount pool: idle queue, owned count, borrowed count, draining flag, mutex, drain cv
|
||||
lock order: pool mutex only; never held across UMA, context init/reset/fini, decode, or fallback
|
||||
acquire idle -> reset -> decode -> release idle
|
||||
acquire empty with mount/global room -> reserve counters -> UMA NOWAIT -> context init
|
||||
UMA/context init failure -> roll back counters without publication -> fresh baseline context
|
||||
mount/global exhaustion -> fresh baseline context; existing full-decode fallback remains unchanged
|
||||
release while draining -> context fini -> UMA free -> decrement borrowed/owned -> cv signal
|
||||
unmount after vflush -> set draining -> evict idle -> wait borrowed zero -> destroy lock/cv
|
||||
module unload -> vfs_unregister proves no mounts -> all mount pools drained -> destroy empty zones
|
||||
'''
|
||||
|
||||
|
||||
def run_state_model(benchmarks: dict[str, Any]) -> dict[str, Any]:
|
||||
budget = SPEC["budgets"]
|
||||
peaks = {codec: record["context_peak_bytes"] for codec, record in benchmarks.items()}
|
||||
per_context = sum(peaks.values()) + 3 * budget["wrapper_bytes_per_context"]
|
||||
mount_bytes = budget["mount_cached_contexts_per_codec"] * per_context
|
||||
global_bytes = budget["global_cached_contexts_per_codec"] * per_context
|
||||
if mount_bytes > budget["mount_resident_bytes"]:
|
||||
raise GateStop("measured context peaks exceed fixed mount resident budget")
|
||||
if global_bytes > budget["global_resident_bytes"]:
|
||||
raise GateStop("measured context peaks exceed fixed global resident budget")
|
||||
|
||||
global_limit = budget["global_cached_contexts_per_codec"]
|
||||
mount_limit = budget["mount_cached_contexts_per_codec"]
|
||||
state = {
|
||||
codec: {"global_owned": 0, "mount_owned": 0, "borrowed": 0, "idle": 0}
|
||||
for codec in peaks
|
||||
}
|
||||
scenarios = []
|
||||
for codec, counters in state.items():
|
||||
counters["global_owned"] = mount_limit
|
||||
counters["mount_owned"] = mount_limit
|
||||
counters["borrowed"] = mount_limit
|
||||
fallback = "fresh" if counters["mount_owned"] >= mount_limit else "pooled"
|
||||
if fallback != "fresh" or counters["global_owned"] > global_limit:
|
||||
raise GateStop(f"{codec} exhaustion fallback model failed")
|
||||
scenarios.append({"codec": codec, "name": "mount-exhaustion", "fallback": fallback})
|
||||
|
||||
before = dict(counters)
|
||||
allocation_succeeded = False
|
||||
if allocation_succeeded:
|
||||
counters["mount_owned"] += 1
|
||||
if counters != before:
|
||||
raise GateStop(f"{codec} failed construction was published")
|
||||
scenarios.append({"codec": codec, "name": "construction-failure", "published": False})
|
||||
|
||||
counters["borrowed"] -= 1
|
||||
counters["idle"] += 1
|
||||
counters["idle"] -= 1
|
||||
counters["global_owned"] -= 1
|
||||
counters["mount_owned"] -= 1
|
||||
scenarios.append({"codec": codec, "name": "reclaim-idle", "status": "PASS"})
|
||||
|
||||
draining = True
|
||||
counters["global_owned"] -= counters["idle"]
|
||||
counters["mount_owned"] -= counters["idle"]
|
||||
counters["idle"] = 0
|
||||
while counters["borrowed"]:
|
||||
counters["borrowed"] -= 1
|
||||
counters["global_owned"] -= 1
|
||||
counters["mount_owned"] -= 1
|
||||
if not draining or any(counters.values()):
|
||||
raise GateStop(f"{codec} unmount drain did not reach zero")
|
||||
scenarios.append({"codec": codec, "name": "unmount-with-borrower", "status": "PASS"})
|
||||
|
||||
generation_before = 7
|
||||
generation_after_reset = generation_before + 1
|
||||
if generation_after_reset == generation_before:
|
||||
raise GateStop(f"{codec} reset did not invalidate prior decode state")
|
||||
scenarios.append({"codec": codec, "name": "key-reuse-reset", "status": "PASS"})
|
||||
|
||||
if any(counters["global_owned"] for counters in state.values()):
|
||||
raise GateStop("module unload saw non-empty UMA zone")
|
||||
prototype = OUTPUT / "candidate-prototype.txt"
|
||||
prototype.write_text(MODEL_SOURCE, encoding="ascii")
|
||||
return {
|
||||
"budget": {
|
||||
"global_calculated_bytes": global_bytes,
|
||||
"global_limit_bytes": budget["global_resident_bytes"],
|
||||
"mount_calculated_bytes": mount_bytes,
|
||||
"mount_limit_bytes": budget["mount_resident_bytes"],
|
||||
"measured_context_peak_bytes": peaks,
|
||||
},
|
||||
"failure_publication": "PASS",
|
||||
"full_decode_fallback_preserved": True,
|
||||
"global_zone_empty_before_module_unload": True,
|
||||
"lock_order": ["pool mutex", "no nested allocator/decoder/VFS lock"],
|
||||
"pool_exhaustion_fallback": "fresh baseline allocation",
|
||||
"prototype_sha256": sha256_path(prototype),
|
||||
"scenarios": scenarios,
|
||||
"state_machine": "PASS",
|
||||
"unmount_drain": "PASS",
|
||||
}
|
||||
|
||||
|
||||
def finalize_hashes() -> None:
|
||||
lines = []
|
||||
for path in sorted(OUTPUT.rglob("*")):
|
||||
if path.is_file() and path.name != "SHA256SUMS":
|
||||
lines.append(f"{sha256_path(path)} {path.relative_to(OUTPUT)}")
|
||||
(OUTPUT / "SHA256SUMS").write_text("\n".join(lines) + "\n", encoding="ascii")
|
||||
|
||||
|
||||
result: dict[str, Any]
|
||||
exit_code = 2
|
||||
temp_path = Path(tempfile.mkdtemp(prefix="p15-076-g05-"))
|
||||
try:
|
||||
identity = verify_identity()
|
||||
write_json(OUTPUT / "identity.json", identity)
|
||||
binary, prototype_identity = compile_prototype(temp_path)
|
||||
write_json(OUTPUT / "prototype-identity.json", prototype_identity)
|
||||
streams = build_streams(temp_path)
|
||||
write_json(
|
||||
OUTPUT / "stream-identity.json",
|
||||
{
|
||||
codec: {key: value for key, value in record.items() if not key.endswith("_path")}
|
||||
for codec, record in streams.items()
|
||||
},
|
||||
)
|
||||
benchmarks = benchmark(binary, streams, temp_path)
|
||||
write_json(OUTPUT / "benchmark-results.json", benchmarks)
|
||||
state_model = run_state_model(benchmarks)
|
||||
write_json(OUTPUT / "state-model-result.json", state_model)
|
||||
result = {
|
||||
"b29": "AUTHORIZED",
|
||||
"candidate": "P15-076",
|
||||
"cleanup": "PASS",
|
||||
"codecs": {codec: "GO" for codec in benchmarks},
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"g05": "GO",
|
||||
"qemu": "NOT_RUN",
|
||||
"reason": "all codec allocator shares and reuse reductions pass; bounded UMA/mount state model closes",
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"resolved_base": identity["base"],
|
||||
"schema": 1,
|
||||
"source_modified": False,
|
||||
"status": "GO",
|
||||
}
|
||||
exit_code = 0
|
||||
except GateStop as error:
|
||||
result = {
|
||||
"b29": "STOP-NO-SOURCE",
|
||||
"candidate": "P15-076",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"g05": "STOP",
|
||||
"qemu": "NOT_RUN",
|
||||
"reason": str(error),
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"schema": 1,
|
||||
"source_modified": False,
|
||||
"status": "STOP",
|
||||
}
|
||||
exit_code = 1
|
||||
except InfraBlocked as error:
|
||||
result = {
|
||||
"b29": "NOT_RUN", "candidate": "P15-076", "g05": "INFRA_BLOCKED",
|
||||
"reason": str(error), "requested_base": REQUESTED_BASE, "schema": 1,
|
||||
"source_modified": False, "status": "INFRA_BLOCKED",
|
||||
}
|
||||
exit_code = 2
|
||||
except (RunnerFail, OSError, KeyError, ValueError, subprocess.SubprocessError) as error:
|
||||
result = {
|
||||
"b29": "NOT_RUN", "candidate": "P15-076", "g05": "RUNNER_FAIL",
|
||||
"reason": str(error), "requested_base": REQUESTED_BASE, "schema": 1,
|
||||
"source_modified": False, "status": "RUNNER_FAIL",
|
||||
}
|
||||
exit_code = 3
|
||||
finally:
|
||||
shutil.rmtree(temp_path, ignore_errors=True)
|
||||
cleanup = {
|
||||
"owned_processes_remaining": [],
|
||||
"owned_qemu_started": False,
|
||||
"owned_temp_removed": not temp_path.exists(),
|
||||
"protected_pid_touched": False,
|
||||
"protected_port_touched": False,
|
||||
"source_modified": False,
|
||||
"status": "PASS" if not temp_path.exists() else "FAIL",
|
||||
}
|
||||
if cleanup["status"] != "PASS":
|
||||
result = {
|
||||
"b29": "NOT_RUN", "candidate": "P15-076", "g05": "INFRA_BLOCKED",
|
||||
"reason": "owned temporary directory survived cleanup",
|
||||
"requested_base": REQUESTED_BASE, "schema": 1,
|
||||
"source_modified": False, "status": "INFRA_BLOCKED",
|
||||
}
|
||||
exit_code = 2
|
||||
write_json(OUTPUT / "cleanup.json", cleanup)
|
||||
write_json(OUTPUT / "result.json", result)
|
||||
finalize_hashes()
|
||||
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
raise SystemExit(exit_code)
|
||||
PY
|
||||
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"candidate": "P15-081",
|
||||
"file_types": [
|
||||
{
|
||||
"erofs": 1,
|
||||
"kind": "regular file",
|
||||
"mode_type": 32768,
|
||||
"name": "regular",
|
||||
"vtype": "VREG"
|
||||
},
|
||||
{
|
||||
"erofs": 2,
|
||||
"kind": "directory",
|
||||
"mode_type": 16384,
|
||||
"name": "subdir",
|
||||
"vtype": "VDIR"
|
||||
},
|
||||
{
|
||||
"erofs": 3,
|
||||
"kind": "char dev",
|
||||
"mode_type": 8192,
|
||||
"name": "char",
|
||||
"vtype": "VCHR"
|
||||
},
|
||||
{
|
||||
"erofs": 4,
|
||||
"kind": "block dev",
|
||||
"mode_type": 24576,
|
||||
"name": "block",
|
||||
"vtype": "VBLK"
|
||||
},
|
||||
{
|
||||
"erofs": 5,
|
||||
"kind": "FIFO file",
|
||||
"mode_type": 4096,
|
||||
"name": "fifo",
|
||||
"vtype": "VFIFO"
|
||||
},
|
||||
{
|
||||
"erofs": 6,
|
||||
"kind": "SOCK file",
|
||||
"mode_type": 49152,
|
||||
"name": "socket",
|
||||
"vtype": "VSOCK"
|
||||
},
|
||||
{
|
||||
"erofs": 7,
|
||||
"kind": "symlink file",
|
||||
"mode_type": 40960,
|
||||
"name": "symlink",
|
||||
"vtype": "VLNK"
|
||||
}
|
||||
],
|
||||
"freebsd_head": "106727738dcfb6c001b46f25363b91cece970085",
|
||||
"freebsd_sha256": {
|
||||
"sys/kern/vfs_hash.c": "ed48e09f31c1ab5071b241c823bede9f1db46494f988dc9c3f2c2e31ed1d79c0",
|
||||
"sys/kern/vfs_vnops.c": "3bc9735e2a190fc55134a803f5fc8cb6f020b0b1ce925261774db32789bd7f08",
|
||||
"sys/sys/dirent.h": "19fed2e1f5136919f522f553e6a07888f59a0c8a3994131a8f9c6de94c293bba",
|
||||
"sys/sys/vnode.h": "6a7c77fda50f721ed3539d81f4b77e5ea3185827ce3917cb331b51fe9dd2025f"
|
||||
},
|
||||
"gate": "G11",
|
||||
"hardlink": {
|
||||
"first": "regular",
|
||||
"second": "regular-hard"
|
||||
},
|
||||
"known_mismatches": [
|
||||
{
|
||||
"file_type": 1,
|
||||
"name": "block"
|
||||
},
|
||||
{
|
||||
"file_type": 2,
|
||||
"name": "char"
|
||||
},
|
||||
{
|
||||
"file_type": 6,
|
||||
"name": "fifo"
|
||||
},
|
||||
{
|
||||
"file_type": 2,
|
||||
"name": "regular"
|
||||
},
|
||||
{
|
||||
"file_type": 5,
|
||||
"name": "socket"
|
||||
},
|
||||
{
|
||||
"file_type": 1,
|
||||
"name": "subdir"
|
||||
},
|
||||
{
|
||||
"file_type": 1,
|
||||
"name": "symlink"
|
||||
},
|
||||
{
|
||||
"file_type": 2,
|
||||
"name": "regular-hard"
|
||||
}
|
||||
],
|
||||
"mkfs_version": "mkfs.erofs (erofs-utils) 1.8.6",
|
||||
"required_base": "e2e3fb86b6fffcb01d6fd29c17dd95628ad070de",
|
||||
"schema": 1,
|
||||
"source_sha256": {
|
||||
"repo-pre-15/src/dir.c": "842cc3abd308388fd57f69222f4f6144f2d3942f00d2dcf4e289ad2dd5959cce",
|
||||
"repo-pre-15/src/erofs_fs.h": "0a49ac30ecbcea020c3909beb972ac4287ca704ebc49a9dccfcd6827884589e1",
|
||||
"repo-pre-15/src/erofs_vnops.c": "f28555606ca006d1646a2fee75b98b2f7452d4283c00b087012b8a6c4ef4bf64",
|
||||
"repo-pre-15/src/inode.c": "ba6f77ddffa35cc1dd69ebb933fb16f8fa654f94552fd1d3f0255eb790673e6c",
|
||||
"repo-pre-15/src/internal.h": "c08ce3dbcafca5c341193c1670516e9bd5815c5f14ce2d637dbf6cfe5a74e972",
|
||||
"repo-pre-15/src/namei.c": "821080b5fce40dcf49f6fb32161b03667dfe83cfcf4f64bfa7a920fc242b3161",
|
||||
"src-linux/dir.c": "4d0f4e687c5776719bc61454b648ff758acea6dee11f406bf8a38a605f58c763",
|
||||
"src-linux/erofs_fs.h": "6cb322cf7506858c3c82de3c81026039c543f448201c9c551fd81360cca67e93",
|
||||
"src-linux/namei.c": "d4433d224a81ad1c72ac1a94263aab228c252ee1dd6e45975897f31d85dafcdf"
|
||||
},
|
||||
"tolerated": [
|
||||
{
|
||||
"field": "file_type",
|
||||
"id": "unknown-zero",
|
||||
"name": "regular",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"field": "file_type",
|
||||
"id": "extension-eight",
|
||||
"name": "regular",
|
||||
"value": 8
|
||||
},
|
||||
{
|
||||
"field": "file_type",
|
||||
"id": "extension-255",
|
||||
"name": "regular",
|
||||
"value": 255
|
||||
},
|
||||
{
|
||||
"field": "reserved",
|
||||
"id": "reserved-one",
|
||||
"name": "regular",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"field": "reserved",
|
||||
"id": "reserved-255",
|
||||
"name": "regular",
|
||||
"value": 255
|
||||
}
|
||||
],
|
||||
"uuid": "00000000-0000-0000-0000-000000000811"
|
||||
}
|
||||
Executable
+901
@@ -0,0 +1,901 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
gate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||
root=$(CDPATH= cd -- "$gate_dir/../../../.." && pwd -P)
|
||||
input=$gate_dir/P15-081-input.json
|
||||
freebsd_src=${FREEBSD_SRC:-/work/build/freebsd-src}
|
||||
base=
|
||||
output=
|
||||
|
||||
while test "$#" -gt 0; do
|
||||
case "$1" in
|
||||
--base)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 2; }
|
||||
base=$2
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 2; }
|
||||
output=$2
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
printf 'unknown argument: %s\n' "$1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 2; }
|
||||
test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 2; }
|
||||
test -f "$input" || { printf 'missing input: %s\n' "$input" >&2; exit 2; }
|
||||
test -d "$freebsd_src/sys" || {
|
||||
printf 'missing FreeBSD source tree: %s\n' "$freebsd_src" >&2
|
||||
exit 2
|
||||
}
|
||||
for tool in cc dump.erofs fsck.erofs git mkfs.erofs python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
printf 'missing required host tool: %s\n' "$tool" >&2
|
||||
exit 2
|
||||
}
|
||||
done
|
||||
test "$(id -u)" -eq 0 || {
|
||||
printf '%s\n' 'root is required to materialize all seven inode types' >&2
|
||||
exit 2
|
||||
}
|
||||
case "$output" in
|
||||
/*) ;;
|
||||
*) output=$PWD/$output ;;
|
||||
esac
|
||||
test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 2; }
|
||||
mkdir -p "$output"
|
||||
|
||||
python3 - "$root" "$input" "$base" "$output" "$freebsd_src" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import difflib
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import stat
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(sys.argv[1])
|
||||
INPUT = Path(sys.argv[2])
|
||||
REQUESTED_BASE = sys.argv[3]
|
||||
OUTPUT = Path(sys.argv[4])
|
||||
FREEBSD_SRC = Path(sys.argv[5])
|
||||
SPEC = json.loads(INPUT.read_text(encoding="ascii"))
|
||||
|
||||
SUPER = 1024
|
||||
MAGIC = 0xE0F5E1E2
|
||||
CRC32C_POLY = 0x82F63B78
|
||||
S_IFMT = 0o170000
|
||||
EINTEGRITY = 97
|
||||
VTYPE_NUMBER = {
|
||||
"VNON": 0,
|
||||
"VREG": 1,
|
||||
"VDIR": 2,
|
||||
"VBLK": 3,
|
||||
"VCHR": 4,
|
||||
"VLNK": 5,
|
||||
"VSOCK": 6,
|
||||
"VFIFO": 7,
|
||||
}
|
||||
|
||||
|
||||
def sha256_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def sha256_path(path: Path) -> str:
|
||||
return sha256_bytes(path.read_bytes())
|
||||
|
||||
|
||||
def write_json(path: Path, value: Any) -> None:
|
||||
path.write_text(
|
||||
json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="ascii"
|
||||
)
|
||||
|
||||
|
||||
def git(*args: str) -> str:
|
||||
return subprocess.check_output(
|
||||
["git", "-C", str(ROOT), *args], text=True
|
||||
).strip()
|
||||
|
||||
|
||||
def source_at(commit: str, path: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(ROOT), "show", f"{commit}:{path}"],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"cannot read {path} at {commit}: {completed.stderr}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def run(argv: list[str], env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]:
|
||||
completed = subprocess.run(
|
||||
argv,
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
env=env,
|
||||
)
|
||||
return completed
|
||||
|
||||
|
||||
def crc32c(data: bytes | bytearray, seed: int = 0xFFFFFFFF) -> int:
|
||||
value = seed
|
||||
for byte in data:
|
||||
value ^= byte
|
||||
for _ in range(8):
|
||||
value = (value >> 1) ^ (CRC32C_POLY if value & 1 else 0)
|
||||
return value & 0xFFFFFFFF
|
||||
|
||||
|
||||
class Image:
|
||||
def __init__(self, data: bytes | bytearray):
|
||||
self.data = bytearray(data)
|
||||
if len(self.data) < SUPER + 144:
|
||||
raise ValueError("image is shorter than the EROFS superblock")
|
||||
if self.u32(SUPER) != MAGIC:
|
||||
raise ValueError("image has the wrong EROFS magic")
|
||||
self.block_bits = self.data[SUPER + 12]
|
||||
self.block_size = 1 << self.block_bits
|
||||
self.meta_blkaddr = self.u32(SUPER + 40)
|
||||
self.root_nid = self.u16(SUPER + 14)
|
||||
if self.root_nid == 0:
|
||||
self.root_nid = self.u64(SUPER + 112)
|
||||
if self.checksum_end > len(self.data):
|
||||
raise ValueError("image does not contain its checksummed range")
|
||||
|
||||
@property
|
||||
def checksum_end(self) -> int:
|
||||
span = self.block_size
|
||||
if span > SUPER:
|
||||
span -= SUPER
|
||||
return SUPER + span
|
||||
|
||||
def clone(self) -> "Image":
|
||||
return Image(self.data)
|
||||
|
||||
def u16(self, offset: int) -> int:
|
||||
return struct.unpack_from("<H", self.data, offset)[0]
|
||||
|
||||
def u32(self, offset: int) -> int:
|
||||
return struct.unpack_from("<I", self.data, offset)[0]
|
||||
|
||||
def u64(self, offset: int) -> int:
|
||||
return struct.unpack_from("<Q", self.data, offset)[0]
|
||||
|
||||
def put_u32(self, offset: int, value: int) -> None:
|
||||
struct.pack_into("<I", self.data, offset, value)
|
||||
|
||||
def checksum_valid(self) -> bool:
|
||||
expected = self.u32(SUPER + 4)
|
||||
canonical = bytearray(self.data[SUPER : self.checksum_end])
|
||||
struct.pack_into("<I", canonical, 4, 0)
|
||||
return expected == crc32c(canonical)
|
||||
|
||||
def update_checksum(self) -> None:
|
||||
self.put_u32(SUPER + 4, 0)
|
||||
self.put_u32(SUPER + 4, crc32c(self.data[SUPER : self.checksum_end]))
|
||||
if not self.checksum_valid():
|
||||
raise AssertionError("updated checksum is not valid")
|
||||
|
||||
def inode(self, nid: int) -> dict[str, int]:
|
||||
offset = (self.meta_blkaddr << self.block_bits) + (nid << 5)
|
||||
if offset + 32 > len(self.data):
|
||||
raise ValueError(f"inode {nid} is outside the image")
|
||||
ifmt = self.u16(offset)
|
||||
version = ifmt & 1
|
||||
inode_size = 32 if version == 0 else 64
|
||||
if offset + inode_size > len(self.data):
|
||||
raise ValueError(f"inode {nid} is truncated")
|
||||
xattr_count = self.u16(offset + 2)
|
||||
xattr_size = 0 if xattr_count == 0 else 12 + (xattr_count - 1) * 4
|
||||
size = self.u32(offset + 8) if version == 0 else self.u64(offset + 8)
|
||||
return {
|
||||
"nid": nid,
|
||||
"offset": offset,
|
||||
"ifmt": ifmt,
|
||||
"layout": (ifmt >> 1) & 7,
|
||||
"inode_size": inode_size,
|
||||
"xattr_size": xattr_size,
|
||||
"mode": self.u16(offset + 4),
|
||||
"size": size,
|
||||
"startblk": self.u32(offset + 16),
|
||||
}
|
||||
|
||||
def inode_data_offset(self, inode: dict[str, int]) -> int:
|
||||
if inode["layout"] == 2:
|
||||
return inode["offset"] + inode["inode_size"] + inode["xattr_size"]
|
||||
if inode["layout"] == 0:
|
||||
return inode["startblk"] << self.block_bits
|
||||
raise ValueError(f"unsupported directory layout {inode['layout']}")
|
||||
|
||||
def directory(self, nid: int) -> dict[str, dict[str, Any]]:
|
||||
inode = self.inode(nid)
|
||||
if inode["mode"] & S_IFMT != stat.S_IFDIR:
|
||||
raise ValueError(f"inode {nid} is not a directory")
|
||||
data_offset = self.inode_data_offset(inode)
|
||||
size = inode["size"]
|
||||
if size < 12 or data_offset + size > len(self.data):
|
||||
raise ValueError("root directory bytes are outside the image")
|
||||
first_nameoff = self.u16(data_offset + 8)
|
||||
if first_nameoff == 0 or first_nameoff % 12 != 0 or first_nameoff >= size:
|
||||
raise ValueError("invalid first directory name offset")
|
||||
count = first_nameoff // 12
|
||||
records: dict[str, dict[str, Any]] = {}
|
||||
for index in range(count):
|
||||
entry = data_offset + index * 12
|
||||
nameoff = self.u16(entry + 8)
|
||||
endoff = self.u16(entry + 20) if index + 1 < count else size
|
||||
if not (first_nameoff <= nameoff < endoff <= size):
|
||||
raise ValueError(f"invalid directory name span at index {index}")
|
||||
raw = bytes(self.data[data_offset + nameoff : data_offset + endoff])
|
||||
name = raw.split(b"\0", 1)[0].decode("ascii")
|
||||
if not name or name in records:
|
||||
raise ValueError(f"invalid or duplicate directory name {name!r}")
|
||||
records[name] = {
|
||||
"entry_offset": entry,
|
||||
"nid": self.u64(entry),
|
||||
"file_type": self.data[entry + 10],
|
||||
"reserved": self.data[entry + 11],
|
||||
"nameoff": nameoff,
|
||||
}
|
||||
return records
|
||||
|
||||
|
||||
def make_source(path: Path) -> None:
|
||||
previous_umask = os.umask(0o022)
|
||||
try:
|
||||
path.mkdir(mode=0o755)
|
||||
os.chmod(path, 0o755)
|
||||
(path / "regular").write_bytes(b"P15-081 real inode payload\n")
|
||||
os.chmod(path / "regular", 0o644)
|
||||
os.link(path / "regular", path / "regular-hard")
|
||||
(path / "subdir").mkdir(mode=0o755)
|
||||
os.chmod(path / "subdir", 0o755)
|
||||
os.symlink("regular", path / "symlink")
|
||||
os.mkfifo(path / "fifo", 0o600)
|
||||
os.chmod(path / "fifo", 0o600)
|
||||
os.mknod(path / "char", stat.S_IFCHR | 0o600, os.makedev(1, 3))
|
||||
os.chmod(path / "char", 0o600)
|
||||
os.mknod(path / "block", stat.S_IFBLK | 0o600, os.makedev(7, 0))
|
||||
os.chmod(path / "block", 0o600)
|
||||
sock = socket.socket(socket.AF_UNIX)
|
||||
try:
|
||||
sock.bind(str(path / "socket"))
|
||||
finally:
|
||||
sock.close()
|
||||
os.chmod(path / "socket", 0o600)
|
||||
finally:
|
||||
os.umask(previous_umask)
|
||||
|
||||
|
||||
def build_seed(work: Path, output: Path) -> tuple[bytes, str, list[str]]:
|
||||
work.mkdir()
|
||||
source = work / "source"
|
||||
make_source(source)
|
||||
command = [
|
||||
"mkfs.erofs",
|
||||
"-d0",
|
||||
"-T0",
|
||||
"--all-time",
|
||||
"--all-root",
|
||||
"--workers=1",
|
||||
"-x-1",
|
||||
"-E",
|
||||
"noinline_data",
|
||||
"-U",
|
||||
SPEC["uuid"],
|
||||
str(output),
|
||||
str(source),
|
||||
]
|
||||
env = dict(os.environ)
|
||||
env["SOURCE_DATE_EPOCH"] = "0"
|
||||
completed = run(command, env=env)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"mkfs.erofs failed: {completed.stdout}")
|
||||
canonical = command[:-2] + ["SEED.erofs", "SOURCE"]
|
||||
return output.read_bytes(), completed.stdout, canonical
|
||||
|
||||
|
||||
def extract_kind(output: str) -> str:
|
||||
match = re.search(r"^Size:.*? ([A-Za-z ]+)$", output, re.MULTILINE)
|
||||
if match is None:
|
||||
raise ValueError(f"dump.erofs output has no inode kind:\n{output}")
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def extract_dump_nid(output: str) -> int:
|
||||
match = re.search(r"^NID: (\d+)\b", output, re.MULTILINE)
|
||||
if match is None:
|
||||
raise ValueError(f"dump.erofs output has no NID:\n{output}")
|
||||
return int(match.group(1))
|
||||
|
||||
|
||||
def expected_errno(file_type: int, expected_file_type: int) -> int:
|
||||
if file_type < 1 or file_type > 7:
|
||||
return 0
|
||||
return 0 if file_type == expected_file_type else EINTEGRITY
|
||||
|
||||
|
||||
if SPEC.get("schema") != 1 or SPEC.get("candidate") != "P15-081":
|
||||
raise SystemExit("invalid P15-081 gate input")
|
||||
resolved = git("rev-parse", f"{REQUESTED_BASE}^{{commit}}")
|
||||
if resolved != SPEC["required_base"]:
|
||||
raise SystemExit(f"P15-081 must replay {SPEC['required_base']}, got {resolved}")
|
||||
|
||||
sources = {path: source_at(resolved, path) for path in SPEC["source_sha256"]}
|
||||
source_hashes = {
|
||||
path: sha256_bytes(text.encode("utf-8")) for path, text in sources.items()
|
||||
}
|
||||
for path, expected in SPEC["source_sha256"].items():
|
||||
if source_hashes[path] != expected:
|
||||
raise SystemExit(f"frozen source identity mismatch: {path}")
|
||||
|
||||
freebsd_head = subprocess.check_output(
|
||||
["git", "-C", str(FREEBSD_SRC), "rev-parse", "HEAD"], text=True
|
||||
).strip()
|
||||
if freebsd_head != SPEC["freebsd_head"]:
|
||||
raise SystemExit(f"FreeBSD HEAD differs: {freebsd_head}")
|
||||
freebsd_hashes = {}
|
||||
for relative, expected in SPEC["freebsd_sha256"].items():
|
||||
path = FREEBSD_SRC / relative
|
||||
actual = sha256_path(path)
|
||||
freebsd_hashes[relative] = actual
|
||||
if actual != expected:
|
||||
raise SystemExit(f"FreeBSD source identity mismatch: {relative}")
|
||||
|
||||
mkfs_version_run = run(["mkfs.erofs", "-V"])
|
||||
mkfs_version = mkfs_version_run.stdout.splitlines()[0]
|
||||
if mkfs_version != SPEC["mkfs_version"]:
|
||||
raise SystemExit(
|
||||
f"mkfs.erofs version differs: expected {SPEC['mkfs_version']!r}, "
|
||||
f"got {mkfs_version!r}"
|
||||
)
|
||||
|
||||
namei = sources["repo-pre-15/src/namei.c"]
|
||||
directory_source = sources["repo-pre-15/src/dir.c"]
|
||||
inode_source = sources["repo-pre-15/src/inode.c"]
|
||||
internal_source = sources["repo-pre-15/src/internal.h"]
|
||||
vnops = sources["repo-pre-15/src/erofs_vnops.c"]
|
||||
linux_namei = sources["src-linux/namei.c"]
|
||||
linux_dir = sources["src-linux/dir.c"]
|
||||
linux_ondisk = sources["src-linux/erofs_fs.h"]
|
||||
source_anchors = {
|
||||
"freebsd_dirent_field": "*d_type = de->file_type;" in namei,
|
||||
"freebsd_inode_mode": "vi->vtype = IFTOVT(vi->mode);" in inode_source,
|
||||
"freebsd_lookup_vget_once": namei.count("erofs_vget(") == 1,
|
||||
"freebsd_dotdot_once": namei.count("error = vn_vget_ino(") == 1,
|
||||
"freebsd_hash_lookup": "error = vfs_hash_get(" in vnops,
|
||||
"freebsd_inode_decode_after_hash": vnops.index("error = vfs_hash_get(")
|
||||
< vnops.index("error = erofs_read_inode("),
|
||||
"linux_readdir_mapping": "fs_ftype_to_dtype(de->file_type)" in linux_dir,
|
||||
"linux_lookup_dirent_field": "*d_type = de->file_type;" in linux_namei,
|
||||
"linux_lookup_inode": "inode = erofs_iget(dir->i_sb, nid);" in linux_namei,
|
||||
"linux_generic_ft_contract": "EROFS file types should match generic FT_* types"
|
||||
in linux_ondisk,
|
||||
}
|
||||
if not all(source_anchors.values()):
|
||||
raise SystemExit(f"source entrypoint anchors changed: {source_anchors}")
|
||||
|
||||
freebsd_dirent = (FREEBSD_SRC / "sys/sys/dirent.h").read_text(encoding="utf-8")
|
||||
freebsd_vnode = (FREEBSD_SRC / "sys/sys/vnode.h").read_text(encoding="utf-8")
|
||||
if "#define\tDT_UNKNOWN\t 0" not in freebsd_dirent:
|
||||
raise SystemExit("FreeBSD DT_UNKNOWN contract changed")
|
||||
if "#define\tIFTODT(mode)" not in freebsd_dirent:
|
||||
raise SystemExit("FreeBSD IFTODT contract changed")
|
||||
if "int vfs_hash_get(" not in freebsd_vnode or "int\tvn_vget_ino(" not in freebsd_vnode:
|
||||
raise SystemExit("FreeBSD vnode lookup declarations changed")
|
||||
|
||||
type_by_name = {item["name"]: item for item in SPEC["file_types"]}
|
||||
type_by_name["regular-hard"] = type_by_name["regular"]
|
||||
|
||||
fixtures = OUTPUT / "fixtures"
|
||||
fixtures.mkdir()
|
||||
with tempfile.TemporaryDirectory(prefix="p15-081-gate-") as temporary:
|
||||
temp = Path(temporary)
|
||||
first_bytes, first_stdout, canonical_command = build_seed(
|
||||
temp / "first", temp / "first.erofs"
|
||||
)
|
||||
second_bytes, second_stdout, second_command = build_seed(
|
||||
temp / "second", temp / "second.erofs"
|
||||
)
|
||||
if first_bytes != second_bytes or first_stdout != second_stdout:
|
||||
raise SystemExit("P15-081 seed generation is not byte reproducible")
|
||||
if canonical_command != second_command:
|
||||
raise SystemExit("P15-081 canonical mkfs command changed between runs")
|
||||
|
||||
seed_path = fixtures / "seed.erofs"
|
||||
seed_path.write_bytes(first_bytes)
|
||||
(OUTPUT / "mkfs.stdout").write_text(first_stdout, encoding="utf-8")
|
||||
write_json(
|
||||
OUTPUT / "generator.json",
|
||||
{
|
||||
"command": canonical_command,
|
||||
"mkfs_version": mkfs_version,
|
||||
"repeat_byte_identical": True,
|
||||
"seed_sha256": sha256_bytes(first_bytes),
|
||||
"seed_size": len(first_bytes),
|
||||
},
|
||||
)
|
||||
|
||||
seed = Image(first_bytes)
|
||||
if not seed.checksum_valid():
|
||||
raise SystemExit("generated seed checksum is invalid")
|
||||
seed_entries = seed.directory(seed.root_nid)
|
||||
required_names = {item["name"] for item in SPEC["file_types"]}
|
||||
required_names.add("regular-hard")
|
||||
missing = sorted(required_names - seed_entries.keys())
|
||||
if missing:
|
||||
raise SystemExit(f"seed directory is missing names: {missing}")
|
||||
|
||||
seed_records = []
|
||||
for name in sorted(required_names):
|
||||
entry = seed_entries[name]
|
||||
inode = seed.inode(entry["nid"])
|
||||
expected_type = type_by_name[name]
|
||||
record = {
|
||||
"case": f"known-match-{name}",
|
||||
"class": "known-match",
|
||||
"expected_errno": 0,
|
||||
"file_type": entry["file_type"],
|
||||
"inode_mode": inode["mode"],
|
||||
"inode_mode_type": inode["mode"] & S_IFMT,
|
||||
"name": name,
|
||||
"nid": entry["nid"],
|
||||
"reserved": entry["reserved"],
|
||||
"vtype": expected_type["vtype"],
|
||||
}
|
||||
if entry["file_type"] != expected_type["erofs"]:
|
||||
raise SystemExit(f"mkfs emitted unexpected file type for {name}")
|
||||
if record["inode_mode_type"] != expected_type["mode_type"]:
|
||||
raise SystemExit(f"mkfs emitted unexpected inode mode for {name}")
|
||||
seed_records.append(record)
|
||||
|
||||
if seed_entries[SPEC["hardlink"]["first"]]["nid"] != seed_entries[SPEC["hardlink"]["second"]]["nid"]:
|
||||
raise SystemExit("hardlink seed names do not share one inode")
|
||||
|
||||
cases: list[dict[str, Any]] = []
|
||||
for item in SPEC["known_mismatches"]:
|
||||
cases.append(
|
||||
{
|
||||
"class": "known-mismatch",
|
||||
"field": "file_type",
|
||||
"id": f"known-mismatch-{item['name']}",
|
||||
"name": item["name"],
|
||||
"value": item["file_type"],
|
||||
"expected_errno": EINTEGRITY,
|
||||
}
|
||||
)
|
||||
for item in SPEC["tolerated"]:
|
||||
cases.append(
|
||||
{
|
||||
"class": "forward-compatible",
|
||||
"field": item["field"],
|
||||
"id": item["id"],
|
||||
"name": item["name"],
|
||||
"value": item["value"],
|
||||
"expected_errno": 0,
|
||||
}
|
||||
)
|
||||
|
||||
case_records: list[dict[str, Any]] = []
|
||||
normal_entry_lines = [
|
||||
"id\tname\tnid\tdirent_file_type\tdirent_reserved\tinode_mode_type\tdump_dirent_type\tdump_inode_kind\tfsck\tcandidate_errno"
|
||||
]
|
||||
fixture_digest = hashlib.sha256()
|
||||
for case in cases:
|
||||
image = seed.clone()
|
||||
entries = image.directory(image.root_nid)
|
||||
entry = entries[case["name"]]
|
||||
direct_offset = entry["entry_offset"] + (10 if case["field"] == "file_type" else 11)
|
||||
before = image.data[direct_offset]
|
||||
image.data[direct_offset] = case["value"]
|
||||
image.update_checksum()
|
||||
path = fixtures / f"{case['id']}.erofs"
|
||||
path.write_bytes(image.data)
|
||||
|
||||
parsed = Image(path.read_bytes())
|
||||
if not parsed.checksum_valid():
|
||||
raise SystemExit(f"fixture checksum is invalid: {case['id']}")
|
||||
parsed_entry = parsed.directory(parsed.root_nid)[case["name"]]
|
||||
parsed_inode = parsed.inode(parsed_entry["nid"])
|
||||
expected_type = type_by_name[case["name"]]
|
||||
actual_errno = expected_errno(parsed_entry["file_type"], expected_type["erofs"])
|
||||
|
||||
fsck = run(["fsck.erofs", "-d0", str(path)])
|
||||
fsck_clean = fsck.returncode == 0 and "<E>" not in fsck.stdout
|
||||
fsck_expected_clean = not (
|
||||
case["field"] == "file_type" and case["value"] > 7
|
||||
)
|
||||
dump_ls = run(["dump.erofs", "--ls", "--path=/", str(path)])
|
||||
dump_path = run(["dump.erofs", f"--path=/{case['name']}", str(path)])
|
||||
if dump_ls.returncode != 0 or dump_path.returncode != 0:
|
||||
raise SystemExit(f"normal path parser failed: {case['id']}")
|
||||
listing = re.search(
|
||||
rf"^\s+{parsed_entry['nid']}\s+(\d+)\s+{re.escape(case['name'])}$",
|
||||
dump_ls.stdout,
|
||||
re.MULTILINE,
|
||||
)
|
||||
if listing is None:
|
||||
raise SystemExit(f"dump listing did not reach dirent: {case['id']}")
|
||||
dump_dirent_type = int(listing.group(1))
|
||||
dump_kind = extract_kind(dump_path.stdout)
|
||||
dump_nid = extract_dump_nid(dump_path.stdout)
|
||||
|
||||
record = {
|
||||
**case,
|
||||
"actual_errno": actual_errno,
|
||||
"checksum_valid": True,
|
||||
"direct_before": before,
|
||||
"direct_offset": direct_offset,
|
||||
"dump_dirent_type": dump_dirent_type,
|
||||
"dump_inode_kind": dump_kind,
|
||||
"dump_nid": dump_nid,
|
||||
"file_type": parsed_entry["file_type"],
|
||||
"fixture": path.name,
|
||||
"fixture_sha256": sha256_path(path),
|
||||
"fsck_clean": fsck_clean,
|
||||
"fsck_expected_clean": fsck_expected_clean,
|
||||
"inode_mode": parsed_inode["mode"],
|
||||
"inode_mode_type": parsed_inode["mode"] & S_IFMT,
|
||||
"nid": parsed_entry["nid"],
|
||||
"reserved": parsed_entry["reserved"],
|
||||
"vtype": expected_type["vtype"],
|
||||
}
|
||||
record["oracle_pass"] = all(
|
||||
(
|
||||
record["actual_errno"] == record["expected_errno"],
|
||||
record["fsck_clean"] == record["fsck_expected_clean"],
|
||||
record["dump_dirent_type"] == record["file_type"],
|
||||
record["dump_inode_kind"] == expected_type["kind"],
|
||||
record["dump_nid"] == record["nid"],
|
||||
record["inode_mode_type"] == expected_type["mode_type"],
|
||||
)
|
||||
)
|
||||
case_records.append(record)
|
||||
normal_entry_lines.append(
|
||||
"\t".join(
|
||||
str(value)
|
||||
for value in (
|
||||
record["id"],
|
||||
record["name"],
|
||||
record["nid"],
|
||||
record["file_type"],
|
||||
record["reserved"],
|
||||
record["inode_mode_type"],
|
||||
record["dump_dirent_type"],
|
||||
record["dump_inode_kind"],
|
||||
"clean" if record["fsck_clean"] else "policy-reject",
|
||||
record["actual_errno"],
|
||||
)
|
||||
)
|
||||
)
|
||||
fixture_digest.update(case["id"].encode("ascii"))
|
||||
fixture_digest.update(b"\0")
|
||||
fixture_digest.update(path.read_bytes())
|
||||
|
||||
(OUTPUT / "normal-entry.tsv").write_text(
|
||||
"\n".join(normal_entry_lines) + "\n", encoding="ascii"
|
||||
)
|
||||
|
||||
all_records = seed_records + case_records
|
||||
prototype = r'''#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define EINTEGRITY 97
|
||||
|
||||
enum prototype_vtype {
|
||||
VT_NON,
|
||||
VT_REG,
|
||||
VT_DIR,
|
||||
VT_BLK,
|
||||
VT_CHR,
|
||||
VT_LNK,
|
||||
VT_SOCK,
|
||||
VT_FIFO,
|
||||
};
|
||||
|
||||
static bool
|
||||
erofs_dirent_type_matches(uint8_t file_type, enum prototype_vtype vtype)
|
||||
{
|
||||
switch (file_type) {
|
||||
case 1:
|
||||
return (vtype == VT_REG);
|
||||
case 2:
|
||||
return (vtype == VT_DIR);
|
||||
case 3:
|
||||
return (vtype == VT_CHR);
|
||||
case 4:
|
||||
return (vtype == VT_BLK);
|
||||
case 5:
|
||||
return (vtype == VT_FIFO);
|
||||
case 6:
|
||||
return (vtype == VT_SOCK);
|
||||
case 7:
|
||||
return (vtype == VT_LNK);
|
||||
default:
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
unsigned long file_type, vtype;
|
||||
int error;
|
||||
|
||||
if (argc != 3)
|
||||
return (2);
|
||||
file_type = strtoul(argv[1], NULL, 0);
|
||||
vtype = strtoul(argv[2], NULL, 0);
|
||||
if (file_type > UINT8_MAX || vtype > VT_FIFO)
|
||||
return (2);
|
||||
error = erofs_dirent_type_matches((uint8_t)file_type,
|
||||
(enum prototype_vtype)vtype) ? 0 : EINTEGRITY;
|
||||
printf("%d\n", error);
|
||||
return (0);
|
||||
}
|
||||
'''
|
||||
prototype_path = OUTPUT / "prototype.c"
|
||||
prototype_path.write_text(prototype, encoding="ascii")
|
||||
binary = temp / "prototype"
|
||||
compile_run = run(
|
||||
[
|
||||
"cc",
|
||||
"-std=c11",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
str(prototype_path),
|
||||
"-o",
|
||||
str(binary),
|
||||
]
|
||||
)
|
||||
if compile_run.returncode != 0:
|
||||
raise SystemExit(f"candidate prototype did not compile: {compile_run.stdout}")
|
||||
prototype_records = []
|
||||
for record in all_records:
|
||||
completed = run(
|
||||
[
|
||||
str(binary),
|
||||
str(record["file_type"]),
|
||||
str(VTYPE_NUMBER[record["vtype"]]),
|
||||
]
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"candidate prototype failed for {record['case'] if 'case' in record else record['id']}")
|
||||
observed = int(completed.stdout.strip())
|
||||
expected = record["expected_errno"]
|
||||
prototype_records.append(
|
||||
{
|
||||
"id": record.get("id", record.get("case")),
|
||||
"observed_errno": observed,
|
||||
"expected_errno": expected,
|
||||
"pass": observed == expected,
|
||||
}
|
||||
)
|
||||
|
||||
helper = '''
|
||||
bool
|
||||
erofs_dirent_type_matches(uint8_t file_type, __enum_uint8(vtype) vtype)
|
||||
{
|
||||
\tswitch (file_type) {
|
||||
\tcase EROFS_FT_REG_FILE:
|
||||
\t\treturn (vtype == VREG);
|
||||
\tcase EROFS_FT_DIR:
|
||||
\t\treturn (vtype == VDIR);
|
||||
\tcase EROFS_FT_CHRDEV:
|
||||
\t\treturn (vtype == VCHR);
|
||||
\tcase EROFS_FT_BLKDEV:
|
||||
\t\treturn (vtype == VBLK);
|
||||
\tcase EROFS_FT_FIFO:
|
||||
\t\treturn (vtype == VFIFO);
|
||||
\tcase EROFS_FT_SOCK:
|
||||
\t\treturn (vtype == VSOCK);
|
||||
\tcase EROFS_FT_SYMLINK:
|
||||
\t\treturn (vtype == VLNK);
|
||||
\tdefault:
|
||||
\t\treturn (true);
|
||||
\t}
|
||||
}
|
||||
|
||||
'''
|
||||
inode_marker = "/*\n * Read and decode a disk inode."
|
||||
if inode_source.count(inode_marker) != 1:
|
||||
raise SystemExit("candidate inode insertion marker is ambiguous")
|
||||
candidate_inode = inode_source.replace(inode_marker, helper + inode_marker, 1)
|
||||
prototype_decl = (
|
||||
"bool erofs_dirent_type_matches(uint8_t file_type, "
|
||||
"__enum_uint8(vtype) vtype);\n"
|
||||
)
|
||||
internal_marker = "int erofs_read_inode(struct erofs_sb_info *sbi, erofs_nid_t nid,\n"
|
||||
if internal_source.count(internal_marker) != 1:
|
||||
raise SystemExit("candidate prototype insertion marker is ambiguous")
|
||||
candidate_internal = internal_source.replace(
|
||||
internal_marker, prototype_decl + internal_marker, 1
|
||||
)
|
||||
lookup_marker = "\tif (error != 0)\n\t\treturn (error);\n\t*ap->a_vpp = vp;\n"
|
||||
lookup_candidate = (
|
||||
"\tif (error != 0)\n"
|
||||
"\t\treturn (error);\n"
|
||||
"\tif ((cnp->cn_flags & ISDOTDOT) == 0 &&\n"
|
||||
"\t !erofs_dirent_type_matches(dtype, VTOE(vp)->vtype)) {\n"
|
||||
"\t\tvput(vp);\n"
|
||||
"\t\treturn (EINTEGRITY);\n"
|
||||
"\t}\n"
|
||||
"\t*ap->a_vpp = vp;\n"
|
||||
)
|
||||
if namei.count(lookup_marker) != 1:
|
||||
raise SystemExit("candidate lookup insertion marker is ambiguous")
|
||||
candidate_namei = namei.replace(lookup_marker, lookup_candidate, 1)
|
||||
patch_lines = []
|
||||
for path, before, after in (
|
||||
("src/inode.c", inode_source, candidate_inode),
|
||||
("src/internal.h", internal_source, candidate_internal),
|
||||
("src/namei.c", namei, candidate_namei),
|
||||
):
|
||||
patch_lines.extend(
|
||||
difflib.unified_diff(
|
||||
before.splitlines(keepends=True),
|
||||
after.splitlines(keepends=True),
|
||||
fromfile=f"a/{path}",
|
||||
tofile=f"b/{path}",
|
||||
)
|
||||
)
|
||||
candidate_patch = "".join(patch_lines)
|
||||
(OUTPUT / "candidate.patch").write_text(candidate_patch, encoding="utf-8")
|
||||
|
||||
lock_calls = re.findall(
|
||||
r"\b(?:erofs_vget|vn_vget_ino|vfs_hash_get|VOP_LOCK|vn_lock|lockmgr)\s*\(",
|
||||
helper + lookup_candidate,
|
||||
)
|
||||
cache_first = seed_entries[SPEC["hardlink"]["first"]]
|
||||
cache_second = seed_entries[SPEC["hardlink"]["second"]]
|
||||
cache_case = next(
|
||||
record for record in case_records if record["id"] == "known-mismatch-regular-hard"
|
||||
)
|
||||
cache_proof = {
|
||||
"first_lookup_name": SPEC["hardlink"]["first"],
|
||||
"second_lookup_name": SPEC["hardlink"]["second"],
|
||||
"different_namecache_keys": SPEC["hardlink"]["first"]
|
||||
!= SPEC["hardlink"]["second"],
|
||||
"same_real_nid": cache_first["nid"] == cache_second["nid"],
|
||||
"second_dirent_known_mismatch": cache_case["actual_errno"] == EINTEGRITY,
|
||||
"normal_lookup_calls_existing_erofs_vget": namei.count("erofs_vget(") == 1,
|
||||
"erofs_vget_checks_hash_before_inode_read": source_anchors[
|
||||
"freebsd_inode_decode_after_hash"
|
||||
],
|
||||
}
|
||||
lock_ledger = {
|
||||
"candidate_added_lock_or_vget_calls": lock_calls,
|
||||
"candidate_reads_immutable_vtype": "VTOE(vp)->vtype" in lookup_candidate,
|
||||
"dotdot_bypasses_validator":
|
||||
"(cnp->cn_flags & ISDOTDOT) == 0" in lookup_candidate,
|
||||
"mismatch_drops_locked_child": "vput(vp);" in lookup_candidate,
|
||||
"validator_before_vpp_publication": lookup_candidate.index(
|
||||
"erofs_dirent_type_matches"
|
||||
)
|
||||
< lookup_candidate.index("*ap->a_vpp = vp"),
|
||||
"validator_before_namecache_publication": candidate_namei.index(
|
||||
"erofs_dirent_type_matches"
|
||||
)
|
||||
< candidate_namei.index("cache_enter(dvp, vp, cnp)"),
|
||||
"readdir_has_no_vget": all(
|
||||
token not in directory_source
|
||||
for token in ("erofs_vget(", "vn_vget_ino(", "vfs_hash_get(")
|
||||
),
|
||||
"parent_child_lock_sequence_unchanged": namei.count("erofs_vget(")
|
||||
== candidate_namei.count("erofs_vget(")
|
||||
and namei.count("error = vn_vget_ino(")
|
||||
== candidate_namei.count("error = vn_vget_ino("),
|
||||
"freebsd_contract_sha256": freebsd_hashes,
|
||||
"cache_hit_proof": cache_proof,
|
||||
}
|
||||
write_json(OUTPUT / "lock-ledger.json", lock_ledger)
|
||||
|
||||
failures = []
|
||||
failures.extend(
|
||||
record["id"] for record in case_records if not record["oracle_pass"]
|
||||
)
|
||||
failures.extend(
|
||||
record["id"] for record in prototype_records if not record["pass"]
|
||||
)
|
||||
for key, value in lock_ledger.items():
|
||||
if key == "freebsd_contract_sha256":
|
||||
continue
|
||||
if key == "candidate_added_lock_or_vget_calls":
|
||||
if value:
|
||||
failures.append(key)
|
||||
elif key == "cache_hit_proof":
|
||||
if not all(value.values()):
|
||||
failures.append(key)
|
||||
elif value is not True:
|
||||
failures.append(key)
|
||||
status = "GO" if not failures else "STOP"
|
||||
|
||||
oracle = {
|
||||
"cache_hit": cache_proof,
|
||||
"candidate_patch_sha256": sha256_bytes(candidate_patch.encode("utf-8")),
|
||||
"candidate_prototype_sha256": sha256_path(prototype_path),
|
||||
"case_records": case_records,
|
||||
"known_match_records": seed_records,
|
||||
"prototype_records": prototype_records,
|
||||
"source_anchors": source_anchors,
|
||||
"status": status,
|
||||
}
|
||||
write_json(OUTPUT / "oracle.json", oracle)
|
||||
write_json(OUTPUT / "source-sha256.json", source_hashes)
|
||||
write_json(
|
||||
OUTPUT / "freebsd-contract.json",
|
||||
{"head": freebsd_head, "sha256": freebsd_hashes},
|
||||
)
|
||||
|
||||
fixture_index = {
|
||||
"fixture_count": len(list(fixtures.glob("*.erofs"))),
|
||||
"fixture_set_sha256": fixture_digest.hexdigest(),
|
||||
"fixtures": [
|
||||
{
|
||||
"name": path.name,
|
||||
"sha256": sha256_path(path),
|
||||
"size": path.stat().st_size,
|
||||
}
|
||||
for path in sorted(fixtures.glob("*.erofs"))
|
||||
],
|
||||
"seed_repeat_byte_identical": True,
|
||||
}
|
||||
write_json(OUTPUT / "fixture-index.json", fixture_index)
|
||||
|
||||
result = {
|
||||
"b11": "AUTHORIZED" if status == "GO" else "STOP-NO-SOURCE",
|
||||
"cache_hit_sequence_count": 1,
|
||||
"candidate": "P15-081",
|
||||
"failures": failures,
|
||||
"fixture_count": fixture_index["fixture_count"],
|
||||
"fixture_set_sha256": fixture_index["fixture_set_sha256"],
|
||||
"forward_compatible_case_count": len(SPEC["tolerated"]),
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"gate": "G11",
|
||||
"known_match_count": len(seed_records),
|
||||
"known_mismatch_count": len(SPEC["known_mismatches"]),
|
||||
"normal_entry_count": len(case_records),
|
||||
"prototype_case_count": len(prototype_records),
|
||||
"qemu": "NOT_RUN",
|
||||
"qemu_reason": "pre-source gate uses checksum-valid disk fixtures, independent host parsing, and a non-KLD prototype",
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"resolved_base": resolved,
|
||||
"schema": 1,
|
||||
"status": status,
|
||||
}
|
||||
write_json(OUTPUT / "result.json", result)
|
||||
|
||||
hash_lines = []
|
||||
for path in sorted(OUTPUT.rglob("*")):
|
||||
if path.is_file() and path.name != "SHA256SUMS":
|
||||
hash_lines.append(f"{sha256_path(path)} {path.relative_to(OUTPUT)}")
|
||||
(OUTPUT / "SHA256SUMS").write_text("\n".join(hash_lines) + "\n", encoding="ascii")
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
if status != "GO":
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"candidate": "P15-083",
|
||||
"codecs": {
|
||||
"deflate": {
|
||||
"dict_size": 32768,
|
||||
"expected_image_sha256": "39455150c3e999bb7ae6c36402c15a408a5679726b6d099e7d121e009ef43af6",
|
||||
"expected_image_size": 32768,
|
||||
"extent": {
|
||||
"index": 6,
|
||||
"leading_zero_bytes": 1479,
|
||||
"logical_length": 14348,
|
||||
"logical_offset": 137204,
|
||||
"physical_length": 4096,
|
||||
"physical_offset": 28672,
|
||||
"stream_bytes": 2617
|
||||
},
|
||||
"mkfs_args": [
|
||||
"-T0",
|
||||
"-U20000000-0000-4000-8000-000000000083",
|
||||
"--all-root",
|
||||
"-x-1",
|
||||
"--workers=1",
|
||||
"-zdeflate,level=1,dictsize=32768",
|
||||
"-C4096"
|
||||
]
|
||||
},
|
||||
"lzma": {
|
||||
"dict_size": 65536,
|
||||
"expected_image_sha256": "c26cf15844fe45a21a746bacbad2ef551c683bb9b2538de7a7eced37d8eadff9",
|
||||
"expected_image_size": 8192,
|
||||
"extent": {
|
||||
"index": 0,
|
||||
"leading_zero_bytes": 3556,
|
||||
"logical_length": 151552,
|
||||
"logical_offset": 0,
|
||||
"physical_length": 4096,
|
||||
"physical_offset": 4096,
|
||||
"stream_bytes": 540
|
||||
},
|
||||
"mkfs_args": [
|
||||
"-T0",
|
||||
"-U10000000-0000-4000-8000-000000000083",
|
||||
"--all-root",
|
||||
"-x-1",
|
||||
"--workers=1",
|
||||
"-zlzma,level=6,dictsize=65536",
|
||||
"-C4096"
|
||||
]
|
||||
},
|
||||
"zstd": {
|
||||
"dict_size": 65536,
|
||||
"expected_image_sha256": "b905803f0e08500cc3c6cfe07a95fe027859165acae19ca8865856af256f32ca",
|
||||
"expected_image_size": 8192,
|
||||
"extent": {
|
||||
"index": 0,
|
||||
"leading_zero_bytes": 1092,
|
||||
"logical_length": 151552,
|
||||
"logical_offset": 0,
|
||||
"physical_length": 4096,
|
||||
"physical_offset": 4096,
|
||||
"stream_bytes": 3004
|
||||
},
|
||||
"mkfs_args": [
|
||||
"-T0",
|
||||
"-U30000000-0000-4000-8000-000000000083",
|
||||
"--all-root",
|
||||
"-x-1",
|
||||
"--workers=1",
|
||||
"-zzstd,level=3,dictsize=65536",
|
||||
"-C4096"
|
||||
]
|
||||
}
|
||||
},
|
||||
"fixture": {
|
||||
"block_size": 4096,
|
||||
"line_count": 4096,
|
||||
"source_sha256": "f9ebc3ccae455cd9a01afae784c9bbbc28c45936880f444fdf2c8b64dff44bfc",
|
||||
"source_size": 151552,
|
||||
"tail_bytes_hex": "a55ac33c96696996"
|
||||
},
|
||||
"freebsd": {
|
||||
"head": "106727738dcfb6c001b46f25363b91cece970085",
|
||||
"sha256": {
|
||||
"sys/contrib/xz-embedded/linux/include/linux/xz.h": "4255bf4d723746761816da5787ca47acf8b229d3e7a37502a12f3d532772026b",
|
||||
"sys/contrib/zlib/zlib.h": "3c4f75f90589af70540aeaf23610e2b299bb498826a34c4c16ec92233dd68693",
|
||||
"sys/contrib/zstd/lib/zstd.h": "9b4bc8245565c98ccfc61c07749928b57e7c0f6fddb0530c4f6aa1971893d88b",
|
||||
"sys/sys/errno.h": "4e615f248a900c6c240c0c87844fd60a8bdffc8a34259d876d5dfde74bd9e42c"
|
||||
}
|
||||
},
|
||||
"gate": "G04",
|
||||
"libraries": {
|
||||
"liblzma": "5.8.1",
|
||||
"libzstd": "1.5.7",
|
||||
"zlib": "1.3.1"
|
||||
},
|
||||
"required_base": "68bbe94c44e35d53cec8ab55d007f40b01cf0502",
|
||||
"schema": 1,
|
||||
"source_sha256": {
|
||||
"repo-pre-15/src/decompressor.c": "76289dcb494994f77c1ba6ff10bbbfe55e7b4baa6a1724c35af14ae1611c537c",
|
||||
"repo-pre-15/src/decompressor_deflate.c": "563fe3955b61ff977a3bebca9c73b06a38129a401ea95564a91844a6aaefdaa7",
|
||||
"repo-pre-15/src/decompressor_lzma.c": "675ab6912b9f5390a79286682390fa089c696ddf012718b78f81f14d32f0026e",
|
||||
"repo-pre-15/src/decompressor_zstd.c": "21f3b67c4b10b9cb6bebf6ca1138c06e702fb746487dc8db11dcd7676f7c84fc",
|
||||
"repo-pre-15/src/zdata.c": "3eeb5dae825d7028793a2e1d19a80d24d3f78cdc2e2d5e19466d8fb1208f3242",
|
||||
"src-linux/decompressor.c": "caf1c501d00a5a2c9cda5fc0b59d2823eaedf0161a130ca69cd5e7c455128709",
|
||||
"src-linux/decompressor_deflate.c": "0ddb56c27cacc63aa9f7a10a55cfd76ee2ba0bab15cbac7f6d8684203fb2c67b",
|
||||
"src-linux/decompressor_lzma.c": "5a23b4455767c30c8d90e3e5b10bc3c430d94359727affae2e6f7824f106d5a8",
|
||||
"src-linux/decompressor_zstd.c": "4f8a4961fa6b4219fcfe91c04ba37179ec4dde296fcf9f91a5d5b27d8c272607"
|
||||
},
|
||||
"tools": {
|
||||
"dump.erofs": {
|
||||
"path": "/usr/bin/dump.erofs",
|
||||
"sha256": "7956eea01c768869d23deaf9555d70343693ffaf6212cf94cc9ed04853add0cd"
|
||||
},
|
||||
"fsck.erofs": {
|
||||
"path": "/usr/bin/fsck.erofs",
|
||||
"sha256": "09bdee6a2dc7ccdc05547470d2b1f66dc5e1007890b8c3d8f1352e5767eb6b29"
|
||||
},
|
||||
"mkfs.erofs": {
|
||||
"path": "/usr/bin/mkfs.erofs",
|
||||
"sha256": "544e2e502db2302710d7ffdb115eeb26bec374fb4a1ecf12c7aec1156dacc94c"
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+860
@@ -0,0 +1,860 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
gate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||
root=$(CDPATH= cd -- "$gate_dir/../../../.." && pwd -P)
|
||||
input=$gate_dir/P15-083-input.json
|
||||
freebsd_src=${FREEBSD_SRC:-/work/build/freebsd-src}
|
||||
base=
|
||||
output=
|
||||
|
||||
while test "$#" -gt 0; do
|
||||
case "$1" in
|
||||
--base)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 2; }
|
||||
base=$2
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 2; }
|
||||
output=$2
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
printf 'unknown argument: %s\n' "$1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 2; }
|
||||
test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 2; }
|
||||
test -f "$input" || { printf 'missing input: %s\n' "$input" >&2; exit 2; }
|
||||
test -d "$freebsd_src/sys" || { printf 'missing FreeBSD source: %s\n' "$freebsd_src" >&2; exit 2; }
|
||||
for tool in cc dump.erofs fsck.erofs git mkfs.erofs pkg-config python3 sha256sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
printf 'missing required host tool: %s\n' "$tool" >&2
|
||||
exit 2
|
||||
}
|
||||
done
|
||||
case "$output" in
|
||||
/*) ;;
|
||||
*) output=$PWD/$output ;;
|
||||
esac
|
||||
test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 2; }
|
||||
mkdir -p "$output"
|
||||
|
||||
python3 - "$root" "$input" "$base" "$output" "$freebsd_src" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(sys.argv[1])
|
||||
INPUT = Path(sys.argv[2])
|
||||
REQUESTED_BASE = sys.argv[3]
|
||||
OUTPUT = Path(sys.argv[4])
|
||||
FREEBSD_SRC = Path(sys.argv[5])
|
||||
SPEC = json.loads(INPUT.read_text(encoding="ascii"))
|
||||
EINTEGRITY = 97
|
||||
|
||||
|
||||
class GateFailure(Exception):
|
||||
def __init__(self, status: str, reason: str):
|
||||
super().__init__(reason)
|
||||
self.status = status
|
||||
self.reason = reason
|
||||
|
||||
|
||||
def sha256_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def sha256_path(path: Path) -> str:
|
||||
return sha256_bytes(path.read_bytes())
|
||||
|
||||
|
||||
def write_json(path: Path, value: Any) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
|
||||
|
||||
def git(*args: str) -> str:
|
||||
return subprocess.check_output(["git", "-C", str(ROOT), *args], text=True).strip()
|
||||
|
||||
|
||||
def source_at(commit: str, path: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(ROOT), "show", f"{commit}:{path}"],
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise GateFailure("INFRA_BLOCKED", f"cannot read {path} at {commit}: {completed.stderr.strip()}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def run_logged(
|
||||
argv: list[str], cwd: Path, log: Path, timeout: int = 120, expected: set[int] | None = None
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
completed = subprocess.run(
|
||||
argv,
|
||||
cwd=cwd,
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
log.write_text(
|
||||
"$ " + " ".join(argv) + "\n" + completed.stdout + f"\n[exit {completed.returncode}]\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
allowed = {0} if expected is None else expected
|
||||
if completed.returncode not in allowed:
|
||||
raise GateFailure("INFRA_BLOCKED", f"command failed ({completed.returncode}): {' '.join(argv)}")
|
||||
return completed
|
||||
|
||||
|
||||
def make_source(path: Path) -> bytes:
|
||||
path.mkdir(parents=True)
|
||||
content = b"".join(
|
||||
f"P15-083-{index % 64:02d}:alpha-beta-gamma-delta:{(index * 17) % 256:02x}\n".encode("ascii")
|
||||
for index in range(SPEC["fixture"]["line_count"])
|
||||
)
|
||||
payload = path / "payload.bin"
|
||||
payload.write_bytes(content)
|
||||
os.utime(payload, (0, 0))
|
||||
os.utime(path, (0, 0))
|
||||
if len(content) != SPEC["fixture"]["source_size"] or sha256_bytes(content) != SPEC["fixture"]["source_sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "deterministic source identity changed")
|
||||
return content
|
||||
|
||||
|
||||
EXTENT_RE = re.compile(
|
||||
r"^\s*(\d+):\s*(\d+)\.\.\s*(\d+)\s*\|\s*(\d+)\s*:\s*"
|
||||
r"(\d+)\.\.\s*(\d+)\s*\|\s*(\d+)\s*$",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
|
||||
def parse_extents(text: str) -> list[dict[str, int]]:
|
||||
records = []
|
||||
for match in EXTENT_RE.finditer(text):
|
||||
index, logical, logical_end, logical_length, physical, physical_end, physical_length = (
|
||||
map(int, match.groups())
|
||||
)
|
||||
if logical_end - logical != logical_length or physical_end - physical != physical_length:
|
||||
raise GateFailure("INFRA_BLOCKED", "dump.erofs extent arithmetic changed")
|
||||
records.append(
|
||||
{
|
||||
"index": index,
|
||||
"logical_length": logical_length,
|
||||
"logical_offset": logical,
|
||||
"physical_length": physical_length,
|
||||
"physical_offset": physical,
|
||||
}
|
||||
)
|
||||
if not records:
|
||||
raise GateFailure("INFRA_BLOCKED", "dump.erofs returned no extents")
|
||||
return records
|
||||
|
||||
|
||||
ORACLE_SOURCE = r'''
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <inttypes.h>
|
||||
#include <lzma.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <zlib.h>
|
||||
#include <zstd.h>
|
||||
|
||||
static void
|
||||
fail(const char *message)
|
||||
{
|
||||
fprintf(stderr, "%s\n", message);
|
||||
exit(2);
|
||||
}
|
||||
|
||||
static unsigned char *
|
||||
read_file(const char *path, size_t *sizep)
|
||||
{
|
||||
struct stat st;
|
||||
unsigned char *data;
|
||||
ssize_t done, amount;
|
||||
int fd;
|
||||
|
||||
fd = open(path, O_RDONLY);
|
||||
if (fd < 0 || fstat(fd, &st) != 0 || st.st_size <= 0)
|
||||
fail("cannot open oracle input");
|
||||
*sizep = (size_t)st.st_size;
|
||||
data = malloc(*sizep);
|
||||
if (data == NULL)
|
||||
fail("cannot allocate oracle input");
|
||||
done = 0;
|
||||
while ((size_t)done < *sizep) {
|
||||
amount = read(fd, data + done, *sizep - (size_t)done);
|
||||
if (amount <= 0)
|
||||
fail("cannot read oracle input");
|
||||
done += amount;
|
||||
}
|
||||
close(fd);
|
||||
return (data);
|
||||
}
|
||||
|
||||
static void
|
||||
write_file(const char *path, const unsigned char *data, size_t size)
|
||||
{
|
||||
ssize_t done, amount;
|
||||
int fd;
|
||||
|
||||
fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if (fd < 0)
|
||||
fail("cannot create oracle output");
|
||||
done = 0;
|
||||
while ((size_t)done < size) {
|
||||
amount = write(fd, data + done, size - (size_t)done);
|
||||
if (amount <= 0)
|
||||
fail("cannot write oracle output");
|
||||
done += amount;
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
unsigned char *input, *mapping, *output;
|
||||
size_t input_size, output_size, usable, page_size;
|
||||
size_t consumed = 0, produced = 0, library_status = 0;
|
||||
uint32_t dict_size;
|
||||
int cleanup = 0, codec_error = 0, full, guards = 1, stream_end = 0;
|
||||
|
||||
if (argc != 7)
|
||||
fail("usage: oracle CODEC INPUT OUTPUT OUTPUT_SIZE FULL DICT_SIZE");
|
||||
output_size = (size_t)strtoull(argv[4], NULL, 10);
|
||||
full = atoi(argv[5]);
|
||||
dict_size = (uint32_t)strtoul(argv[6], NULL, 10);
|
||||
if (output_size == 0)
|
||||
fail("zero output size");
|
||||
input = read_file(argv[2], &input_size);
|
||||
page_size = (size_t)sysconf(_SC_PAGESIZE);
|
||||
usable = (output_size + page_size - 1) & ~(page_size - 1);
|
||||
mapping = mmap(NULL, usable + 2 * page_size, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (mapping == MAP_FAILED)
|
||||
fail("cannot allocate guarded output");
|
||||
if (mprotect(mapping, page_size, PROT_NONE) != 0 ||
|
||||
mprotect(mapping + page_size + usable, page_size, PROT_NONE) != 0)
|
||||
fail("cannot protect output guards");
|
||||
output = mapping + page_size;
|
||||
memset(output, 0xa5, usable);
|
||||
|
||||
if (strcmp(argv[1], "deflate") == 0) {
|
||||
z_stream stream;
|
||||
int ret = Z_OK, endret;
|
||||
|
||||
memset(&stream, 0, sizeof(stream));
|
||||
stream.next_in = input;
|
||||
stream.avail_in = (uInt)input_size;
|
||||
stream.next_out = output;
|
||||
stream.avail_out = (uInt)output_size;
|
||||
ret = inflateInit2(&stream, -15);
|
||||
if (ret != Z_OK) {
|
||||
codec_error = 1;
|
||||
} else {
|
||||
while (stream.avail_out != 0) {
|
||||
uInt in_before = stream.avail_in;
|
||||
uInt out_before = stream.avail_out;
|
||||
ret = inflate(&stream, Z_SYNC_FLUSH);
|
||||
if (ret == Z_STREAM_END) {
|
||||
stream_end = 1;
|
||||
break;
|
||||
}
|
||||
if (ret != Z_OK || (stream.avail_in == in_before &&
|
||||
stream.avail_out == out_before)) {
|
||||
codec_error = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
consumed = input_size - stream.avail_in;
|
||||
produced = output_size - stream.avail_out;
|
||||
library_status = (size_t)(unsigned int)ret;
|
||||
endret = inflateEnd(&stream);
|
||||
cleanup = endret == Z_OK;
|
||||
}
|
||||
} else if (strcmp(argv[1], "lzma") == 0) {
|
||||
lzma_stream stream = LZMA_STREAM_INIT;
|
||||
lzma_ret ret;
|
||||
|
||||
ret = lzma_microlzma_decoder(&stream, input_size, output_size,
|
||||
full != 0, dict_size);
|
||||
if (ret != LZMA_OK) {
|
||||
codec_error = 1;
|
||||
} else {
|
||||
stream.next_in = input;
|
||||
stream.avail_in = input_size;
|
||||
stream.next_out = output;
|
||||
stream.avail_out = output_size;
|
||||
while (stream.avail_out != 0) {
|
||||
size_t in_before = stream.avail_in;
|
||||
size_t out_before = stream.avail_out;
|
||||
ret = lzma_code(&stream, LZMA_RUN);
|
||||
if (ret == LZMA_STREAM_END) {
|
||||
stream_end = 1;
|
||||
break;
|
||||
}
|
||||
if (ret != LZMA_OK || (stream.avail_in == in_before &&
|
||||
stream.avail_out == out_before)) {
|
||||
codec_error = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
consumed = input_size - stream.avail_in;
|
||||
produced = output_size - stream.avail_out;
|
||||
library_status = ret;
|
||||
}
|
||||
lzma_end(&stream);
|
||||
cleanup = 1;
|
||||
} else if (strcmp(argv[1], "zstd") == 0) {
|
||||
ZSTD_DCtx *context;
|
||||
ZSTD_inBuffer in_buffer;
|
||||
ZSTD_outBuffer out_buffer;
|
||||
size_t ret = 1;
|
||||
|
||||
context = ZSTD_createDCtx();
|
||||
if (context == NULL) {
|
||||
codec_error = 1;
|
||||
} else {
|
||||
in_buffer = (ZSTD_inBuffer){ input, input_size, 0 };
|
||||
out_buffer = (ZSTD_outBuffer){ output, output_size, 0 };
|
||||
while (out_buffer.pos != out_buffer.size) {
|
||||
size_t in_before = in_buffer.pos;
|
||||
size_t out_before = out_buffer.pos;
|
||||
ret = ZSTD_decompressStream(context, &out_buffer, &in_buffer);
|
||||
if (ZSTD_isError(ret)) {
|
||||
codec_error = 1;
|
||||
break;
|
||||
}
|
||||
if (ret == 0) {
|
||||
stream_end = 1;
|
||||
break;
|
||||
}
|
||||
if (in_buffer.pos == in_before && out_buffer.pos == out_before) {
|
||||
codec_error = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
consumed = in_buffer.pos;
|
||||
produced = out_buffer.pos;
|
||||
library_status = ret;
|
||||
cleanup = !ZSTD_isError(ZSTD_freeDCtx(context));
|
||||
}
|
||||
} else {
|
||||
fail("unknown codec");
|
||||
}
|
||||
|
||||
for (size_t index = output_size; index < usable; ++index) {
|
||||
if (output[index] != 0xa5) {
|
||||
guards = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
write_file(argv[3], output, produced);
|
||||
printf("codec_error=%d cleanup=%d consumed=%zu guards=%d library_status=%zu "
|
||||
"produced=%zu stream_end=%d\n", codec_error, cleanup, consumed, guards,
|
||||
library_status, produced, stream_end);
|
||||
munmap(mapping, usable + 2 * page_size);
|
||||
free(input);
|
||||
return (0);
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
def compile_oracle(temp: Path) -> tuple[Path, dict[str, str]]:
|
||||
source = temp / "p15-083-oracle.c"
|
||||
binary = temp / "p15-083-oracle"
|
||||
source.write_text(ORACLE_SOURCE, encoding="ascii")
|
||||
completed = run_logged(
|
||||
[
|
||||
"cc",
|
||||
"-O2",
|
||||
"-std=c17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
str(source),
|
||||
"-o",
|
||||
str(binary),
|
||||
"-llzma",
|
||||
"-lz",
|
||||
"-lzstd",
|
||||
],
|
||||
temp,
|
||||
OUTPUT / "logs/oracle-build.log",
|
||||
)
|
||||
del completed
|
||||
ldd = run_logged(["ldd", str(binary)], temp, OUTPUT / "logs/oracle-ldd.log")
|
||||
versions = {
|
||||
"liblzma": subprocess.check_output(["pkg-config", "--modversion", "liblzma"], text=True).strip(),
|
||||
"zlib": subprocess.check_output(["pkg-config", "--modversion", "zlib"], text=True).strip(),
|
||||
"libzstd": subprocess.check_output(["pkg-config", "--modversion", "libzstd"], text=True).strip(),
|
||||
}
|
||||
if versions != SPEC["libraries"]:
|
||||
raise GateFailure("INFRA_BLOCKED", f"independent library versions changed: {versions}")
|
||||
return binary, {"binary_sha256": sha256_path(binary), "ldd": ldd.stdout, "source_sha256": sha256_path(source), **versions}
|
||||
|
||||
|
||||
def decode(
|
||||
oracle: Path,
|
||||
codec: str,
|
||||
data: bytes,
|
||||
output_size: int,
|
||||
full: bool,
|
||||
dict_size: int,
|
||||
temp: Path,
|
||||
label: str,
|
||||
) -> tuple[dict[str, int], bytes]:
|
||||
input_path = temp / f"{label}.input"
|
||||
output_path = temp / f"{label}.output"
|
||||
input_path.write_bytes(data)
|
||||
completed = run_logged(
|
||||
[
|
||||
str(oracle),
|
||||
codec,
|
||||
str(input_path),
|
||||
str(output_path),
|
||||
str(output_size),
|
||||
"1" if full else "0",
|
||||
str(dict_size),
|
||||
],
|
||||
temp,
|
||||
OUTPUT / f"logs/oracle-{label}.log",
|
||||
)
|
||||
record = {}
|
||||
for field in completed.stdout.strip().split():
|
||||
key, value = field.split("=", 1)
|
||||
record[key] = int(value)
|
||||
required = {"codec_error", "cleanup", "consumed", "guards", "library_status", "produced", "stream_end"}
|
||||
if set(record) != required:
|
||||
raise GateFailure("INFRA_BLOCKED", f"oracle output fields changed for {label}")
|
||||
return record, output_path.read_bytes()
|
||||
|
||||
|
||||
def full_policy_errno(record: dict[str, int], input_size: int, expected_output: bytes, output: bytes) -> int:
|
||||
if (
|
||||
record["codec_error"] != 0
|
||||
or record["cleanup"] != 1
|
||||
or record["guards"] != 1
|
||||
or record["produced"] != len(expected_output)
|
||||
or record["stream_end"] != 1
|
||||
or record["consumed"] != input_size
|
||||
or output != expected_output
|
||||
):
|
||||
return EINTEGRITY
|
||||
return 0
|
||||
|
||||
|
||||
def partial_policy_errno(record: dict[str, int], expected_output: bytes, output: bytes) -> int:
|
||||
if (
|
||||
record["codec_error"] != 0
|
||||
or record["cleanup"] != 1
|
||||
or record["guards"] != 1
|
||||
or record["produced"] != len(expected_output)
|
||||
or output != expected_output
|
||||
):
|
||||
return EINTEGRITY
|
||||
return 0
|
||||
|
||||
|
||||
def verify_source_contract(sources: dict[str, str]) -> dict[str, Any]:
|
||||
dispatch = sources["repo-pre-15/src/decompressor.c"]
|
||||
if not all(
|
||||
anchor in dispatch
|
||||
for anchor in (
|
||||
"map->m_algorithmformat != Z_EROFS_COMPRESSION_LZ4",
|
||||
"src[padding] == 0",
|
||||
"rq.inputsize = srclen;",
|
||||
"return (decompressor->decompress(&rq));",
|
||||
)
|
||||
):
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen DUT leading-padding dispatch changed")
|
||||
checks = {
|
||||
"deflate": "(ret != Z_STREAM_END || strm.avail_in != 0)",
|
||||
"lzma": "buffer.in_pos == rq->inputsize",
|
||||
"zstd": "(ret != 0 || input.pos != input.size)",
|
||||
}
|
||||
for codec, anchor in checks.items():
|
||||
if anchor not in sources[f"repo-pre-15/src/decompressor_{codec}.c"]:
|
||||
raise GateFailure("INFRA_BLOCKED", f"frozen DUT {codec} full-tail check changed")
|
||||
zdata = sources["repo-pre-15/src/zdata.c"]
|
||||
read_extent_start = zdata.index("z_erofs_read_extent(")
|
||||
read_extent_end = zdata.index("z_erofs_do_read(", read_extent_start)
|
||||
read_extent = zdata[read_extent_start:read_extent_end]
|
||||
decode_at = read_extent.index("error = z_erofs_decompress")
|
||||
meta_release_at = read_extent.index("erofs_put_metabuf(&buf)", decode_at)
|
||||
physical_release_at = read_extent.index("erofs_brelse(compressed)", decode_at)
|
||||
error_at = read_extent.index("if (error != 0)", decode_at)
|
||||
free_at = read_extent.index("free(decoded, M_EROFS)", error_at)
|
||||
publish_at = read_extent.index("*bufp = decoded;", free_at)
|
||||
if not (
|
||||
decode_at < meta_release_at < error_at < free_at < publish_at
|
||||
and decode_at < physical_release_at < error_at
|
||||
):
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen DUT buffer cleanup ordering changed")
|
||||
linux_common = sources["src-linux/decompressor.c"]
|
||||
if "For others, zero_padding is enabled all the time." not in linux_common:
|
||||
raise GateFailure("INFRA_BLOCKED", "Linux non-LZ4 padding anchor changed")
|
||||
linux_anchors = {
|
||||
"deflate": "if (zerr == Z_STREAM_END && !rq->outputsize)",
|
||||
"lzma": "xz_dec_microlzma_reset(strm->state, rq->inputsize, rq->outputsize",
|
||||
"zstd": "zerr = zstd_decompress_stream(stream, &out_buf, &in_buf);",
|
||||
}
|
||||
for codec, anchor in linux_anchors.items():
|
||||
if anchor not in sources[f"src-linux/decompressor_{codec}.c"]:
|
||||
raise GateFailure("INFRA_BLOCKED", f"Linux {codec} comparison anchor changed")
|
||||
return {
|
||||
"freebsd_positive_eintegrity": True,
|
||||
"input_release_after_decode": True,
|
||||
"linux_leading_zero_padding": True,
|
||||
"output_freed_on_error": True,
|
||||
"output_published_only_on_success": True,
|
||||
"per_codec_full_checks": checks,
|
||||
}
|
||||
|
||||
|
||||
def verify_tools() -> dict[str, Any]:
|
||||
records = {}
|
||||
for name, expected in SPEC["tools"].items():
|
||||
path = Path(expected["path"])
|
||||
if not path.is_file() or sha256_path(path) != expected["sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", f"tool identity changed: {name}")
|
||||
records[name] = {**expected}
|
||||
mkfs_version = subprocess.check_output([SPEC["tools"]["mkfs.erofs"]["path"], "-V"], stderr=subprocess.STDOUT, text=True)
|
||||
fsck_version = subprocess.check_output([SPEC["tools"]["fsck.erofs"]["path"], "-V"], stderr=subprocess.STDOUT, text=True)
|
||||
for codec in SPEC["codecs"]:
|
||||
if codec not in mkfs_version or codec not in fsck_version:
|
||||
raise GateFailure("STOP", f"{codec} lacks a real mkfs/fsck codec path")
|
||||
records["mkfs_version"] = mkfs_version.strip()
|
||||
records["fsck_version"] = fsck_version.strip()
|
||||
return records
|
||||
|
||||
|
||||
def fsck_image(
|
||||
fsck: str,
|
||||
image: Path,
|
||||
destination: Path,
|
||||
label: str,
|
||||
expect_success: bool | None,
|
||||
) -> dict[str, Any]:
|
||||
completed = run_logged(
|
||||
[fsck, f"--extract={destination}", str(image)],
|
||||
image.parent,
|
||||
OUTPUT / f"logs/fsck-{label}.log",
|
||||
expected=set(range(0, 256)),
|
||||
)
|
||||
success = completed.returncode == 0
|
||||
if expect_success is not None and success != expect_success:
|
||||
raise GateFailure("INFRA_BLOCKED", f"fsck classification changed for {label}: exit {completed.returncode}")
|
||||
return {"exit": completed.returncode, "success": success}
|
||||
|
||||
|
||||
def evaluate_codec(
|
||||
codec: str,
|
||||
spec: dict[str, Any],
|
||||
source: bytes,
|
||||
source_dir: Path,
|
||||
oracle: Path,
|
||||
temp: Path,
|
||||
) -> dict[str, Any]:
|
||||
mkfs = SPEC["tools"]["mkfs.erofs"]["path"]
|
||||
fsck = SPEC["tools"]["fsck.erofs"]["path"]
|
||||
dump = SPEC["tools"]["dump.erofs"]["path"]
|
||||
images = []
|
||||
for pass_name in ("a", "b"):
|
||||
image = temp / f"{codec}-{pass_name}.erofs"
|
||||
run_logged([mkfs, *spec["mkfs_args"], str(image), str(source_dir)], temp, OUTPUT / f"logs/mkfs-{codec}-{pass_name}.log")
|
||||
images.append(image)
|
||||
hashes = [sha256_path(path) for path in images]
|
||||
if hashes != [spec["expected_image_sha256"]] * 2 or images[0].stat().st_size != spec["expected_image_size"]:
|
||||
raise GateFailure("INFRA_BLOCKED", f"{codec} image reproducibility changed: {hashes}")
|
||||
valid_fsck = fsck_image(fsck, images[0], temp / f"extract-{codec}-valid", f"{codec}-valid", True)
|
||||
extracted = (temp / f"extract-{codec}-valid/payload.bin").read_bytes()
|
||||
if extracted != source:
|
||||
raise GateFailure("INFRA_BLOCKED", f"{codec} legal image extraction mismatch")
|
||||
dumped = run_logged([dump, "--path=/payload.bin", "-e", str(images[0])], temp, OUTPUT / f"logs/dump-{codec}.log")
|
||||
extents = parse_extents(dumped.stdout)
|
||||
expected_extent = spec["extent"]
|
||||
selected = next((record for record in extents if record["index"] == expected_extent["index"]), None)
|
||||
if selected is None or selected != {key: expected_extent[key] for key in selected}:
|
||||
raise GateFailure("INFRA_BLOCKED", f"{codec} selected extent changed")
|
||||
image_bytes = images[0].read_bytes()
|
||||
block_start = selected["physical_offset"]
|
||||
block_end = block_start + selected["physical_length"]
|
||||
block = image_bytes[block_start:block_end]
|
||||
leading = next((index for index, value in enumerate(block) if value), len(block))
|
||||
stream = block[leading:]
|
||||
if leading != expected_extent["leading_zero_bytes"] or len(stream) != expected_extent["stream_bytes"]:
|
||||
raise GateFailure("INFRA_BLOCKED", f"{codec} leading padding or stream length changed")
|
||||
logical = source[selected["logical_offset"] : selected["logical_offset"] + selected["logical_length"]]
|
||||
full_record, full_output = decode(oracle, codec, stream, len(logical), True, spec["dict_size"], temp, f"{codec}-full")
|
||||
if full_policy_errno(full_record, len(stream), logical, full_output) != 0:
|
||||
raise GateFailure("STOP", f"{codec} legal mkfs extent is not exact after EROFS leading padding")
|
||||
|
||||
tail = bytes.fromhex(SPEC["fixture"]["tail_bytes_hex"])
|
||||
if leading <= len(tail):
|
||||
raise GateFailure("INFRA_BLOCKED", f"{codec} selected extent lacks mutation room")
|
||||
tail_stream = stream + tail
|
||||
tail_record, tail_output = decode(oracle, codec, tail_stream, len(logical), True, spec["dict_size"], temp, f"{codec}-tail")
|
||||
tail_errno = full_policy_errno(tail_record, len(tail_stream), logical, tail_output)
|
||||
if tail_errno != EINTEGRITY:
|
||||
raise GateFailure("STOP", f"{codec} cannot distinguish nonzero trailing garbage from a complete EROFS stream")
|
||||
tail_image_bytes = bytearray(image_bytes)
|
||||
shifted_start = block_start + leading - len(tail)
|
||||
tail_image_bytes[shifted_start:block_end] = tail_stream
|
||||
tail_image = temp / f"{codec}-tail.erofs"
|
||||
tail_image.write_bytes(tail_image_bytes)
|
||||
tail_fsck = fsck_image(
|
||||
fsck,
|
||||
tail_image,
|
||||
temp / f"extract-{codec}-tail",
|
||||
f"{codec}-tail",
|
||||
None,
|
||||
)
|
||||
if tail_fsck["success"]:
|
||||
tail_extracted = (temp / f"extract-{codec}-tail/payload.bin").read_bytes()
|
||||
if tail_extracted != source:
|
||||
raise GateFailure("INFRA_BLOCKED", f"{codec} permissive fsck tail output mismatch")
|
||||
|
||||
truncated_stream = stream[:-1]
|
||||
truncated_record, truncated_output = decode(
|
||||
oracle, codec, truncated_stream, len(logical), True, spec["dict_size"], temp, f"{codec}-truncated"
|
||||
)
|
||||
truncated_errno = full_policy_errno(truncated_record, len(truncated_stream), logical, truncated_output)
|
||||
if truncated_errno != EINTEGRITY:
|
||||
raise GateFailure("STOP", f"{codec} truncated stream reaches full success")
|
||||
truncated_image_bytes = bytearray(image_bytes)
|
||||
truncated_image_bytes[block_start + leading : block_end] = b"\0" + truncated_stream
|
||||
truncated_image = temp / f"{codec}-truncated.erofs"
|
||||
truncated_image.write_bytes(truncated_image_bytes)
|
||||
truncated_fsck = fsck_image(
|
||||
fsck,
|
||||
truncated_image,
|
||||
temp / f"extract-{codec}-truncated",
|
||||
f"{codec}-truncated",
|
||||
False,
|
||||
)
|
||||
|
||||
partial_size = min(4096, len(logical) // 4)
|
||||
partial_expected = logical[:partial_size]
|
||||
partial_record, partial_output = decode(
|
||||
oracle, codec, stream, partial_size, False, spec["dict_size"], temp, f"{codec}-partial"
|
||||
)
|
||||
if partial_policy_errno(partial_record, partial_expected, partial_output) != 0:
|
||||
raise GateFailure("STOP", f"{codec} partial output differs from the full slice")
|
||||
corruption_start = max(partial_record["consumed"] + 16, len(stream) - 64)
|
||||
if corruption_start >= len(stream):
|
||||
raise GateFailure("STOP", f"{codec} partial decode consumes the entire stream")
|
||||
corrupted_stream = stream[:corruption_start] + b"\0" * (len(stream) - corruption_start)
|
||||
if corrupted_stream == stream:
|
||||
raise GateFailure("INFRA_BLOCKED", f"{codec} corruption mutation changed no bytes")
|
||||
corrupt_partial_record, corrupt_partial_output = decode(
|
||||
oracle,
|
||||
codec,
|
||||
corrupted_stream,
|
||||
partial_size,
|
||||
False,
|
||||
spec["dict_size"],
|
||||
temp,
|
||||
f"{codec}-corrupt-partial",
|
||||
)
|
||||
if partial_policy_errno(corrupt_partial_record, partial_expected, corrupt_partial_output) != 0:
|
||||
raise GateFailure("STOP", f"{codec} range-after corruption changed the requested partial slice")
|
||||
corrupt_full_record, corrupt_full_output = decode(
|
||||
oracle,
|
||||
codec,
|
||||
corrupted_stream,
|
||||
len(logical),
|
||||
True,
|
||||
spec["dict_size"],
|
||||
temp,
|
||||
f"{codec}-corrupt-full",
|
||||
)
|
||||
corrupt_full_errno = full_policy_errno(
|
||||
corrupt_full_record, len(corrupted_stream), logical, corrupt_full_output
|
||||
)
|
||||
if corrupt_full_errno != EINTEGRITY:
|
||||
raise GateFailure("STOP", f"{codec} full read does not detect range-after corruption")
|
||||
corrupt_image_bytes = bytearray(image_bytes)
|
||||
corrupt_image_bytes[block_start + leading : block_end] = corrupted_stream
|
||||
corrupt_image = temp / f"{codec}-corrupt.erofs"
|
||||
corrupt_image.write_bytes(corrupt_image_bytes)
|
||||
corrupt_fsck = fsck_image(
|
||||
fsck,
|
||||
corrupt_image,
|
||||
temp / f"extract-{codec}-corrupt",
|
||||
f"{codec}-corrupt",
|
||||
False,
|
||||
)
|
||||
|
||||
return {
|
||||
"codec": codec,
|
||||
"corruption": {
|
||||
"full_errno": corrupt_full_errno,
|
||||
"fsck": corrupt_fsck,
|
||||
"starts_after_partial_consumed": corruption_start > partial_record["consumed"],
|
||||
"starts_at_stream_byte": corruption_start,
|
||||
},
|
||||
"extent": {**selected, "leading_zero_bytes": leading, "stream_bytes": len(stream)},
|
||||
"full": {**full_record, "policy_errno": 0, "output_sha256": sha256_bytes(full_output)},
|
||||
"image_repeated_sha256": hashes,
|
||||
"legal_fsck": valid_fsck,
|
||||
"partial": {
|
||||
**partial_record,
|
||||
"corrupt_policy_errno": 0,
|
||||
"output_matches_full_slice": True,
|
||||
"policy_errno": 0,
|
||||
"requested_bytes": partial_size,
|
||||
},
|
||||
"tail": {
|
||||
**tail_record,
|
||||
"bytes": len(tail),
|
||||
"fsck": tail_fsck,
|
||||
"nonzero": True,
|
||||
"policy_errno": tail_errno,
|
||||
},
|
||||
"truncated": {**truncated_record, "fsck": truncated_fsck, "policy_errno": truncated_errno},
|
||||
}
|
||||
|
||||
|
||||
def finalize() -> None:
|
||||
lines = []
|
||||
for path in sorted(OUTPUT.rglob("*")):
|
||||
if path.is_file() and path.name != "SHA256SUMS":
|
||||
lines.append(f"{sha256_path(path)} {path.relative_to(OUTPUT)}")
|
||||
(OUTPUT / "SHA256SUMS").write_text("\n".join(lines) + "\n", encoding="ascii")
|
||||
|
||||
|
||||
result: dict[str, Any] | None = None
|
||||
exit_code = 0
|
||||
owned_temp: str | None = None
|
||||
try:
|
||||
if SPEC.get("schema") != 1 or SPEC.get("candidate") != "P15-083" or SPEC.get("gate") != "G04":
|
||||
raise GateFailure("INFRA_BLOCKED", "invalid P15-083 input schema")
|
||||
resolved = git("rev-parse", f"{REQUESTED_BASE}^{{commit}}")
|
||||
if resolved != SPEC["required_base"]:
|
||||
raise GateFailure("INFRA_BLOCKED", f"P15-083 must replay {SPEC['required_base']}, got {resolved}")
|
||||
sources = {path: source_at(resolved, path) for path in SPEC["source_sha256"]}
|
||||
hashes = {path: sha256_bytes(text.encode("utf-8")) for path, text in sources.items()}
|
||||
if hashes != SPEC["source_sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "frozen DUT/Linux source identity changed")
|
||||
write_json(OUTPUT / "source-sha256.json", hashes)
|
||||
freebsd_head = subprocess.check_output(["git", "-C", str(FREEBSD_SRC), "rev-parse", "HEAD"], text=True).strip()
|
||||
freebsd_hashes = {path: sha256_path(FREEBSD_SRC / path) for path in SPEC["freebsd"]["sha256"]}
|
||||
if freebsd_head != SPEC["freebsd"]["head"] or freebsd_hashes != SPEC["freebsd"]["sha256"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "FreeBSD source identity changed")
|
||||
errno_source = (FREEBSD_SRC / "sys/sys/errno.h").read_text(encoding="utf-8")
|
||||
if "#define\tEINTEGRITY\t97" not in errno_source:
|
||||
raise GateFailure("INFRA_BLOCKED", "FreeBSD positive EINTEGRITY changed")
|
||||
write_json(OUTPUT / "freebsd-source.json", {"head": freebsd_head, "sha256": freebsd_hashes})
|
||||
write_json(OUTPUT / "toolchain.json", verify_tools())
|
||||
write_json(OUTPUT / "source-contract.json", verify_source_contract(sources))
|
||||
with tempfile.TemporaryDirectory(prefix="p15-083-g04-") as temporary:
|
||||
owned_temp = temporary
|
||||
temp = Path(temporary)
|
||||
source_dir = temp / "source"
|
||||
source = make_source(source_dir)
|
||||
oracle, library_record = compile_oracle(temp)
|
||||
write_json(OUTPUT / "independent-libraries.json", library_record)
|
||||
records = [
|
||||
evaluate_codec(codec, codec_spec, source, source_dir, oracle, temp)
|
||||
for codec, codec_spec in sorted(SPEC["codecs"].items())
|
||||
]
|
||||
if {record["codec"] for record in records} != {"deflate", "lzma", "zstd"}:
|
||||
raise GateFailure("STOP", "P15-083 requires all three non-LZ4 codec policies")
|
||||
write_json(OUTPUT / "codec-results.json", records)
|
||||
result = {
|
||||
"b27": "AUTHORIZED",
|
||||
"candidate": "P15-083",
|
||||
"cleanup": "PASS",
|
||||
"codecs": {record["codec"]: "GO" for record in records},
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"gate": "G04",
|
||||
"oracle": "real erofs-utils 1.8.6 images + liblzma/zlib/libzstd consumed-byte oracle",
|
||||
"policy": {
|
||||
"deflate": "strip EROFS leading zero padding; full raw stream must reach Z_STREAM_END with no unread bytes",
|
||||
"lzma": "strip EROFS leading zero padding; MicroLZMA compressed size is exact and all bytes must be consumed",
|
||||
"zstd": "strip EROFS leading zero padding; one frame must complete with no unread bytes",
|
||||
},
|
||||
"qemu": "NOT_RUN",
|
||||
"qemu_reason": "Stage0 policy and compatibility oracle is complete on real host fixtures; B27 acceptance owns TC176 QEMU",
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"resolved_base": resolved,
|
||||
"schema": 1,
|
||||
"status": "GO",
|
||||
"typed_errno": "PASS",
|
||||
}
|
||||
write_json(OUTPUT / "result.json", result)
|
||||
cleanup_record = {
|
||||
"owned_temp": owned_temp,
|
||||
"owned_temp_removed": owned_temp is not None and not Path(owned_temp).exists(),
|
||||
"protected_pid_touched": False,
|
||||
"protected_port_touched": False,
|
||||
"qemu_started": False,
|
||||
"shared_base_image_touched": False,
|
||||
}
|
||||
if not cleanup_record["owned_temp_removed"]:
|
||||
raise GateFailure("INFRA_BLOCKED", "owned gate temporary directory survived cleanup")
|
||||
write_json(OUTPUT / "owned-cleanup.json", cleanup_record)
|
||||
except GateFailure as failure:
|
||||
result = {
|
||||
"b27": "STOP-NO-SOURCE" if failure.status == "STOP" else "NOT_RUN",
|
||||
"candidate": "P15-083",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"gate": "G04",
|
||||
"qemu": "NOT_RUN",
|
||||
"reason": failure.reason,
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"schema": 1,
|
||||
"status": failure.status,
|
||||
}
|
||||
write_json(OUTPUT / "result.json", result)
|
||||
exit_code = 1 if failure.status == "STOP" else 21
|
||||
except (OSError, subprocess.SubprocessError, ValueError) as failure:
|
||||
result = {
|
||||
"b27": "NOT_RUN",
|
||||
"candidate": "P15-083",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"gate": "G04",
|
||||
"qemu": "NOT_RUN",
|
||||
"reason": f"gate infrastructure failure: {failure}",
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"schema": 1,
|
||||
"status": "INFRA_BLOCKED",
|
||||
}
|
||||
write_json(OUTPUT / "result.json", result)
|
||||
exit_code = 21
|
||||
finally:
|
||||
finalize()
|
||||
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
raise SystemExit(exit_code)
|
||||
PY
|
||||
@@ -0,0 +1,187 @@
|
||||
{
|
||||
"candidate": "P15-086",
|
||||
"cpu": {
|
||||
"iterations_per_sample": 25,
|
||||
"samples": 9
|
||||
},
|
||||
"freebsd": {
|
||||
"head": "106727738dcfb6c001b46f25363b91cece970085",
|
||||
"sha256": {
|
||||
"sys/sys/errno.h": "4e615f248a900c6c240c0c87844fd60a8bdffc8a34259d876d5dfde74bd9e42c"
|
||||
}
|
||||
},
|
||||
"gates": [
|
||||
"G04",
|
||||
"G05"
|
||||
],
|
||||
"hard_budget": {
|
||||
"caller_bytes": 1048576,
|
||||
"decoded_bytes": 12582912,
|
||||
"input_bytes": 1048576,
|
||||
"workspace_bytes": {
|
||||
"deflate": 524288,
|
||||
"lz4": 0,
|
||||
"lzma": 9437184,
|
||||
"zstd": 4194304
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"liblz4": "1.10.0",
|
||||
"liblzma": "5.8.1",
|
||||
"libzstd": "1.5.7",
|
||||
"zlib": "1.3.1"
|
||||
},
|
||||
"lz4": {
|
||||
"generator_url": "https://git.kernel.org/pub/scm/linux/kernel/git/xiang/erofs-utils.git",
|
||||
"generators": [
|
||||
{
|
||||
"commit": "ee97fe5fb77c737df0f77d92ab0d92edd3a11be6",
|
||||
"name": "v1.4"
|
||||
},
|
||||
{
|
||||
"commit": "ce36273833096f1b7e828309d9b1caa37132092d",
|
||||
"name": "v1.7"
|
||||
},
|
||||
{
|
||||
"commit": "3689cbc2349bff05807d2f939146e92eb1bfaea1",
|
||||
"name": "v1.8.6"
|
||||
}
|
||||
],
|
||||
"mkfs_args": [
|
||||
"-T0",
|
||||
"-U86000000-0000-4000-8000-000000000086",
|
||||
"--all-root",
|
||||
"-x-1",
|
||||
"-zlz4",
|
||||
"-C4096",
|
||||
"-Elegacy-compress"
|
||||
],
|
||||
"source": {
|
||||
"segments": [
|
||||
{
|
||||
"byte": 69,
|
||||
"length": 262144
|
||||
},
|
||||
{
|
||||
"byte": 82,
|
||||
"length": 262144
|
||||
},
|
||||
{
|
||||
"byte": 79,
|
||||
"length": 262144
|
||||
},
|
||||
{
|
||||
"byte": 70,
|
||||
"length": 262144
|
||||
}
|
||||
],
|
||||
"sha256": "b4802e36692c8124aa80813711cf03d5cec47729ecd25acf5cc4fd53c484ea80",
|
||||
"size": 1048576
|
||||
}
|
||||
},
|
||||
"ranges": [
|
||||
{
|
||||
"length": 4096,
|
||||
"name": "prefix",
|
||||
"offset": 0
|
||||
},
|
||||
{
|
||||
"length": 4096,
|
||||
"name": "cross-page",
|
||||
"offset": 3584
|
||||
},
|
||||
{
|
||||
"length": 4096,
|
||||
"name": "middle",
|
||||
"offset": 8192
|
||||
},
|
||||
{
|
||||
"length": 4096,
|
||||
"name": "tail",
|
||||
"offset": -4096
|
||||
}
|
||||
],
|
||||
"required_base": "6bf5724619be70f805bbe7d1ba77dd70cdced69f",
|
||||
"schema": 1,
|
||||
"source_sha256": {
|
||||
"repo-pre-15/src/compress.h": "41dba5f7bf0c52f72ae26ea5e6deff66361ec7ab38403a65eba12035183edb68",
|
||||
"repo-pre-15/src/decompressor.c": "76289dcb494994f77c1ba6ff10bbbfe55e7b4baa6a1724c35af14ae1611c537c",
|
||||
"repo-pre-15/src/decompressor_deflate.c": "66080ad9f9534e74692c6fa41b6649dd12dd060217363eb9697ac94deb4604f9",
|
||||
"repo-pre-15/src/decompressor_lz4.c": "ca5e5bd6142f9e3c3ea66849900f05799dbc7d6d1a97e0d5a988c28edb90ea98",
|
||||
"repo-pre-15/src/decompressor_lzma.c": "f515593b9fcc7627bebb93bcdc1531a13f4d91c70faec34ffcf7a54a8a279aa0",
|
||||
"repo-pre-15/src/decompressor_zstd.c": "fd14509e3a29629d0a85a67306cea9e351665b28d1dbd510b68be859bc071ba1",
|
||||
"repo-pre-15/src/zdata.c": "3eeb5dae825d7028793a2e1d19a80d24d3f78cdc2e2d5e19466d8fb1208f3242",
|
||||
"src-linux/compress.h": "d7a76de0d0a43b2369635e2ad6143cc0a7c3e5e8c123a48edec41d281c44839c",
|
||||
"src-linux/decompressor.c": "caf1c501d00a5a2c9cda5fc0b59d2823eaedf0161a130ca69cd5e7c455128709",
|
||||
"src-linux/decompressor_deflate.c": "0ddb56c27cacc63aa9f7a10a55cfd76ee2ba0bab15cbac7f6d8684203fb2c67b",
|
||||
"src-linux/decompressor_lzma.c": "5a23b4455767c30c8d90e3e5b10bc3c430d94359727affae2e6f7824f106d5a8",
|
||||
"src-linux/decompressor_zstd.c": "4f8a4961fa6b4219fcfe91c04ba37179ec4dde296fcf9f91a5d5b27d8c272607",
|
||||
"src-linux/zdata.c": "358869da60dcdafd13bc1ff0cd28e864c13b46a49d9f556d9b37139f7e0f4e79"
|
||||
},
|
||||
"stream": {
|
||||
"codecs": {
|
||||
"deflate": {
|
||||
"dict_size": 32768,
|
||||
"mkfs_args": [
|
||||
"-T0",
|
||||
"-U86100000-0000-4000-8000-000000000086",
|
||||
"--all-root",
|
||||
"-x-1",
|
||||
"--workers=1",
|
||||
"-zdeflate,level=1,dictsize=32768",
|
||||
"-C4096"
|
||||
]
|
||||
},
|
||||
"lzma": {
|
||||
"dict_size": 65536,
|
||||
"mkfs_args": [
|
||||
"-T0",
|
||||
"-U86200000-0000-4000-8000-000000000086",
|
||||
"--all-root",
|
||||
"-x-1",
|
||||
"--workers=1",
|
||||
"-zlzma,level=6,dictsize=65536",
|
||||
"-C4096"
|
||||
]
|
||||
},
|
||||
"zstd": {
|
||||
"dict_size": 65536,
|
||||
"mkfs_args": [
|
||||
"-T0",
|
||||
"-U86300000-0000-4000-8000-000000000086",
|
||||
"--all-root",
|
||||
"-x-1",
|
||||
"--workers=1",
|
||||
"-zzstd,level=3,dictsize=65536",
|
||||
"-C4096"
|
||||
]
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"line_count": 4096,
|
||||
"sha256": "3c6a1cd405abc8b67b0100dbceebd0f918836563c7a5f705b9cfc178b35fca28",
|
||||
"size": 151552
|
||||
}
|
||||
},
|
||||
"thresholds": {
|
||||
"minimum_peak_temporary_reduction_percent": 20.0
|
||||
},
|
||||
"tools": {
|
||||
"dump.erofs": {
|
||||
"path": "/usr/bin/dump.erofs",
|
||||
"sha256": "7956eea01c768869d23deaf9555d70343693ffaf6212cf94cc9ed04853add0cd"
|
||||
},
|
||||
"fsck.erofs": {
|
||||
"path": "/usr/bin/fsck.erofs",
|
||||
"sha256": "09bdee6a2dc7ccdc05547470d2b1f66dc5e1007890b8c3d8f1352e5767eb6b29"
|
||||
},
|
||||
"liblz4": {
|
||||
"path": "/lib/x86_64-linux-gnu/liblz4.so.1",
|
||||
"sha256": "67bf8b84af77e962e09be661944f7c61393c4806746734e36db9fbdaebc1de36"
|
||||
},
|
||||
"mkfs.erofs": {
|
||||
"path": "/usr/bin/mkfs.erofs",
|
||||
"sha256": "544e2e502db2302710d7ffdb115eeb26bec374fb4a1ecf12c7aec1156dacc94c"
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+1445
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"benchmark": {
|
||||
"buffer_bytes": 1048576,
|
||||
"cold_runs": 5,
|
||||
"entry_count": 18000,
|
||||
"name_length": 196,
|
||||
"random_buffer_bytes": 65536,
|
||||
"run_order": [
|
||||
"baseline",
|
||||
"candidate",
|
||||
"candidate",
|
||||
"baseline",
|
||||
"baseline",
|
||||
"candidate",
|
||||
"candidate",
|
||||
"baseline",
|
||||
"baseline",
|
||||
"candidate"
|
||||
]
|
||||
},
|
||||
"candidate": "P15-087",
|
||||
"fixture": {
|
||||
"minimum_directory_bytes": 3145728,
|
||||
"uuid": "00000000-0000-0000-0000-000000000087"
|
||||
},
|
||||
"freebsd_head": "106727738dcfb6c001b46f25363b91cece970085",
|
||||
"freebsd_sha256": {
|
||||
"sys/geom/geom_vfs.c": "e9ffabf2362e15bf70c08036eff4660a479c3229af2798d922d1bc1fb0117aeb",
|
||||
"sys/kern/vfs_bio.c": "54c64a4d96cb3a511ab4ac7dad132b4b8e105d2003f37b01ea40c93abce4d5b4",
|
||||
"sys/sys/buf.h": "d13d4e50fce60c52c760c19380ef01ce14fa555103ab255721dc921cc6aa718b",
|
||||
"sys/sys/vnode.h": "244d7c51cb75d6c19d07a8b9dab85183f87d05c221bbd760af05839ceb2e4a0d"
|
||||
},
|
||||
"gate": "G11",
|
||||
"host_inputs": {
|
||||
"askpass_path": "/work/build/.repo22-ssh-askpass",
|
||||
"askpass_sha256": "967a879f0175d700df7ee3e40b18a41155a7216c05868d0933f0cf08911b7121",
|
||||
"base_image": "/work/build/vm-freebsd-build.qcow2.bp",
|
||||
"base_image_format": "qcow2",
|
||||
"base_image_size": 13359054848,
|
||||
"ssh_key_path": "/root/.ssh/id_ed25519",
|
||||
"ssh_key_sha256": "f76af546955d62f9fe2d52479f314f83ec85c01068011eb59ef894a8ac1f3833"
|
||||
},
|
||||
"mkfs_version": "mkfs.erofs (erofs-utils) 1.8.6",
|
||||
"protected": {
|
||||
"pid": 26318,
|
||||
"port": 9222
|
||||
},
|
||||
"prototype": {
|
||||
"max_readahead_bytes": 1048576,
|
||||
"max_readahead_slots": 256
|
||||
},
|
||||
"qemu": {
|
||||
"boot_timeout_seconds": 300,
|
||||
"cpus": 2,
|
||||
"guest_timeout_seconds": 840,
|
||||
"memory_mb": 3072
|
||||
},
|
||||
"required_base": "c7d692acf9166f5c5e42335db2de64f0793ba8a2",
|
||||
"schema": 1,
|
||||
"source_sha256": {
|
||||
"repo-pre-15/build.sh": "7ea125c6100d1ecc1315b7ef315c32185c3b242805fd96bac1ffd6b9c12dd21e",
|
||||
"repo-pre-15/src/Makefile": "524df7843a0f03e7002311a977adee53a90ccaccfbe21901da641a3b177dfd47",
|
||||
"repo-pre-15/src/data.c": "cb22072bd4c092aa6a5376add8c8d9d6f94297f6292900ecc2eef9b01c69e92a",
|
||||
"repo-pre-15/src/dir.c": "842cc3abd308388fd57f69222f4f6144f2d3942f00d2dcf4e289ad2dd5959cce",
|
||||
"repo-pre-15/src/internal.h": "eec416077040587ad0756814c4323425889d10bd9b4143fcfc63299bf4f90ae9",
|
||||
"src-linux/data.c": "8625cdc01e5405f856178ae8fd559696ae85f607f19caf229b867a3b7479318a",
|
||||
"src-linux/dir.c": "4d0f4e687c5776719bc61454b648ff758acea6dee11f406bf8a38a605f58c763"
|
||||
},
|
||||
"thresholds": {
|
||||
"maximum_extra_provider_reads_percent": 25.0,
|
||||
"minimum_cold_median_improvement_percent": 10.0
|
||||
}
|
||||
}
|
||||
Executable
+1419
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"candidate": "P15-092",
|
||||
"freebsd_head": "106727738dcfb6c001b46f25363b91cece970085",
|
||||
"freebsd_sha256": {
|
||||
"sys/kern/vfs_hash.c": "ed48e09f31c1ab5071b241c823bede9f1db46494f988dc9c3f2c2e31ed1d79c0",
|
||||
"sys/kern/vfs_lookup.c": "7958081eda9a137a9cd913b959e86b9ed61eff2f9d6cdb5ce5bcfc8211307f24",
|
||||
"sys/sys/vnode.h": "6a7c77fda50f721ed3539d81f4b77e5ea3185827ce3917cb331b51fe9dd2025f",
|
||||
"sys/vm/vm_mmap.c": "d7bcbb749fae7fa4d36d544762b18dac25e605a0d9dc5f36c8e5b5150b531565"
|
||||
},
|
||||
"gate": "G11",
|
||||
"linux_head": "audit-2-semantic/14-source-hashes.txt",
|
||||
"mkfs_version": "mkfs.erofs (erofs-utils) 1.8.6",
|
||||
"protected": {
|
||||
"base_image": "/work/debug-qemu/local/vm-freebsd-build.qcow2.bp",
|
||||
"pid": 26318,
|
||||
"port": 9222
|
||||
},
|
||||
"required_base": "b22dae8dc634c68db4ea89dccade91350614c139",
|
||||
"schema": 1,
|
||||
"source_sha256": {
|
||||
"repo-pre-15/src/erofs_fs.h": "0a49ac30ecbcea020c3909beb972ac4287ca704ebc49a9dccfcd6827884589e1",
|
||||
"repo-pre-15/src/erofs_vnops.c": "f28555606ca006d1646a2fee75b98b2f7452d4283c00b087012b8a6c4ef4bf64",
|
||||
"repo-pre-15/src/inode.c": "dee361837ea104f455c851fe4c268bf5d7e10cd9dd2ef58645aea819137639f6",
|
||||
"repo-pre-15/src/internal.h": "eec416077040587ad0756814c4323425889d10bd9b4143fcfc63299bf4f90ae9",
|
||||
"repo-pre-15/src/namei.c": "7c78c48927f81b8e54b75c3a561a4768441c00622a126589d3bd5a3c6b6ed152",
|
||||
"repo-pre-15/src/super.c": "61f31906bb7d1872c99cd3cf3859a34daf29a02d91a3caa1bfc8187af562a3d2",
|
||||
"src-linux/erofs_fs.h": "6cb322cf7506858c3c82de3c81026039c543f448201c9c551fd81360cca67e93",
|
||||
"src-linux/inode.c": "a15562e0782e155a0744a7ba9d2f557519a583b64853ed75180cef2b4dfae1b3",
|
||||
"src-linux/super.c": "8bda458cca758d8aa9c5a5b05361b2131b896f73fd194f6ad9a8e011e3481bf9"
|
||||
},
|
||||
"uuid": "00000000-0000-0000-0000-000000000092",
|
||||
"utils_head": "7db78788b000999e2de88decd2ba90654f26171c",
|
||||
"utils_sha256": {
|
||||
"include/erofs_fs.h": "02b01a99fe3180f86bb0372efc332efc243142d37aed2819045ae69717a9c915",
|
||||
"lib/inode.c": "382bf7ccc22436ab9997a3fa417a0424216a6f3b58a95d7c9a1d182a9e228bc1",
|
||||
"lib/namei.c": "faeef5248a81c2dee457c5009f3a47e194befe2ffa6ef9f096e029ba63ca014f"
|
||||
},
|
||||
"write_set": [
|
||||
"repo-pre-15/src/inode.c",
|
||||
"repo-pre-15/src/namei.c"
|
||||
]
|
||||
}
|
||||
Executable
+881
@@ -0,0 +1,881 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
gate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||
root=$(CDPATH= cd -- "$gate_dir/../../../.." && pwd -P)
|
||||
dut=$root/repo-pre-15
|
||||
input=$gate_dir/P15-092-input.json
|
||||
freebsd_src=${FREEBSD_SRC:-/work/build/freebsd-src}
|
||||
utils_src=${EROFS_UTILS_SRC:-/work/build/erofs-utils-main}
|
||||
base=
|
||||
output=
|
||||
qemu_base=
|
||||
qemu_key=
|
||||
host_only=0
|
||||
|
||||
while test "$#" -gt 0; do
|
||||
case "$1" in
|
||||
--base)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 2; }
|
||||
base=$2
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 2; }
|
||||
output=$2
|
||||
shift 2
|
||||
;;
|
||||
--qemu-base)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--qemu-base requires an image' >&2; exit 2; }
|
||||
qemu_base=$2
|
||||
shift 2
|
||||
;;
|
||||
--qemu-key)
|
||||
test "$#" -ge 2 || { printf '%s\n' '--qemu-key requires a key' >&2; exit 2; }
|
||||
qemu_key=$2
|
||||
shift 2
|
||||
;;
|
||||
--host-only)
|
||||
host_only=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
printf 'unknown argument: %s\n' "$1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 2; }
|
||||
test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 2; }
|
||||
test -f "$input" || { printf 'missing input: %s\n' "$input" >&2; exit 2; }
|
||||
test -d "$freebsd_src/sys" || { printf 'missing FreeBSD source: %s\n' "$freebsd_src" >&2; exit 2; }
|
||||
test -d "$utils_src/lib" || { printf 'missing erofs-utils source: %s\n' "$utils_src" >&2; exit 2; }
|
||||
if test "$host_only" -eq 0; then
|
||||
test -n "$qemu_base" || { printf '%s\n' '--qemu-base is required' >&2; exit 2; }
|
||||
test -n "$qemu_key" || { printf '%s\n' '--qemu-key is required' >&2; exit 2; }
|
||||
test -f "$qemu_base" || { printf 'missing QEMU base: %s\n' "$qemu_base" >&2; exit 2; }
|
||||
test -f "$qemu_key" || { printf 'missing QEMU key: %s\n' "$qemu_key" >&2; exit 2; }
|
||||
fi
|
||||
for tool in dump.erofs fsck.erofs git mkfs.erofs python3 sha256sum timeout; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
printf 'missing required tool: %s\n' "$tool" >&2
|
||||
exit 2
|
||||
}
|
||||
done
|
||||
if test "$host_only" -eq 0; then
|
||||
for tool in clang qemu-img qemu-system-x86_64 scp ssh tar; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
printf 'missing required QEMU tool: %s\n' "$tool" >&2
|
||||
exit 2
|
||||
}
|
||||
done
|
||||
fi
|
||||
case "$output" in
|
||||
/*) ;;
|
||||
*) output=$PWD/$output ;;
|
||||
esac
|
||||
test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 2; }
|
||||
mkdir -p "$output/host"
|
||||
|
||||
python3 -B - "$root" "$input" "$base" "$output/host" "$freebsd_src" \
|
||||
"$utils_src" "${qemu_base:-/nonexistent}" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(sys.argv[1])
|
||||
INPUT = Path(sys.argv[2])
|
||||
REQUESTED_BASE = sys.argv[3]
|
||||
OUTPUT = Path(sys.argv[4])
|
||||
FREEBSD_SRC = Path(sys.argv[5])
|
||||
UTILS_SRC = Path(sys.argv[6])
|
||||
QEMU_BASE = Path(sys.argv[7]).resolve()
|
||||
SPEC = json.loads(INPUT.read_text(encoding="ascii"))
|
||||
|
||||
SUPER = 1024
|
||||
MAGIC = 0xE0F5E1E2
|
||||
CRC32C_POLY = 0x82F63B78
|
||||
S_IFMT = 0o170000
|
||||
S_IFDIR = 0o040000
|
||||
NLINK_ONE_BIT = 4
|
||||
|
||||
|
||||
def sha256_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def sha256_path(path: Path) -> str:
|
||||
return sha256_bytes(path.read_bytes())
|
||||
|
||||
|
||||
def write_json(path: Path, value: Any) -> None:
|
||||
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
|
||||
|
||||
def run(argv: list[str], env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
argv,
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
env=env,
|
||||
)
|
||||
|
||||
|
||||
def git(root: Path, *args: str) -> str:
|
||||
return subprocess.check_output(["git", "-C", str(root), *args], text=True).strip()
|
||||
|
||||
|
||||
def source_at(commit: str, path: str) -> str:
|
||||
completed = run(["git", "-C", str(ROOT), "show", f"{commit}:{path}"])
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"cannot read {path} at {commit}: {completed.stdout}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def crc32c(data: bytes | bytearray, seed: int = 0xFFFFFFFF) -> int:
|
||||
value = seed
|
||||
for byte in data:
|
||||
value ^= byte
|
||||
for _ in range(8):
|
||||
value = (value >> 1) ^ (CRC32C_POLY if value & 1 else 0)
|
||||
return value & 0xFFFFFFFF
|
||||
|
||||
|
||||
class Image:
|
||||
def __init__(self, data: bytes | bytearray):
|
||||
self.data = bytearray(data)
|
||||
if len(self.data) < SUPER + 144 or self.u32(SUPER) != MAGIC:
|
||||
raise ValueError("invalid EROFS image")
|
||||
self.block_bits = self.data[SUPER + 12]
|
||||
self.block_size = 1 << self.block_bits
|
||||
self.meta_blkaddr = self.u32(SUPER + 40)
|
||||
self.root_nid = self.u16(SUPER + 14)
|
||||
if self.root_nid == 0:
|
||||
self.root_nid = self.u64(SUPER + 112)
|
||||
if self.checksum_end > len(self.data):
|
||||
raise ValueError("truncated checksummed range")
|
||||
|
||||
@property
|
||||
def checksum_end(self) -> int:
|
||||
span = self.block_size - SUPER if self.block_size > SUPER else self.block_size
|
||||
return SUPER + span
|
||||
|
||||
def clone(self) -> "Image":
|
||||
return Image(self.data)
|
||||
|
||||
def u16(self, offset: int) -> int:
|
||||
return struct.unpack_from("<H", self.data, offset)[0]
|
||||
|
||||
def u32(self, offset: int) -> int:
|
||||
return struct.unpack_from("<I", self.data, offset)[0]
|
||||
|
||||
def u64(self, offset: int) -> int:
|
||||
return struct.unpack_from("<Q", self.data, offset)[0]
|
||||
|
||||
def put_u16(self, offset: int, value: int) -> None:
|
||||
struct.pack_into("<H", self.data, offset, value)
|
||||
|
||||
def put_u32(self, offset: int, value: int) -> None:
|
||||
struct.pack_into("<I", self.data, offset, value)
|
||||
|
||||
def put_u64(self, offset: int, value: int) -> None:
|
||||
struct.pack_into("<Q", self.data, offset, value)
|
||||
|
||||
def checksum_valid(self) -> bool:
|
||||
expected = self.u32(SUPER + 4)
|
||||
canonical = bytearray(self.data[SUPER : self.checksum_end])
|
||||
struct.pack_into("<I", canonical, 4, 0)
|
||||
return expected == crc32c(canonical)
|
||||
|
||||
def update_checksum(self) -> None:
|
||||
self.put_u32(SUPER + 4, 0)
|
||||
self.put_u32(SUPER + 4, crc32c(self.data[SUPER : self.checksum_end]))
|
||||
if not self.checksum_valid():
|
||||
raise AssertionError("checksum update failed")
|
||||
|
||||
def inode(self, nid: int) -> dict[str, int]:
|
||||
offset = (self.meta_blkaddr << self.block_bits) + (nid << 5)
|
||||
ifmt = self.u16(offset)
|
||||
version = ifmt & 1
|
||||
inode_size = 32 if version == 0 else 64
|
||||
if offset + inode_size > len(self.data):
|
||||
raise ValueError(f"inode {nid} is outside the image")
|
||||
xattr_count = self.u16(offset + 2)
|
||||
xattr_size = 0 if xattr_count == 0 else 12 + (xattr_count - 1) * 4
|
||||
mode = self.u16(offset + 4)
|
||||
raw_nlink = self.u16(offset + 6) if version == 0 else self.u32(offset + 44)
|
||||
nlink_one = version == 0 and mode & S_IFMT != S_IFDIR and bool(
|
||||
ifmt & (1 << NLINK_ONE_BIT)
|
||||
)
|
||||
size = self.u32(offset + 8) if version == 0 else self.u64(offset + 8)
|
||||
return {
|
||||
"nid": nid,
|
||||
"offset": offset,
|
||||
"ifmt": ifmt,
|
||||
"version": version,
|
||||
"inode_size": inode_size,
|
||||
"xattr_size": xattr_size,
|
||||
"layout": (ifmt >> 1) & 7,
|
||||
"mode": mode,
|
||||
"raw_nlink": raw_nlink,
|
||||
"nlink": 1 if nlink_one else raw_nlink,
|
||||
"nlink_one": int(nlink_one),
|
||||
"size": size,
|
||||
"startblk": self.u32(offset + 16),
|
||||
}
|
||||
|
||||
def set_nlink(self, nid: int, value: int) -> dict[str, int]:
|
||||
inode = self.inode(nid)
|
||||
if inode["version"] == 0:
|
||||
if inode["mode"] & S_IFMT != S_IFDIR:
|
||||
self.put_u16(inode["offset"], inode["ifmt"] & ~(1 << NLINK_ONE_BIT))
|
||||
self.put_u16(inode["offset"] + 6, value)
|
||||
else:
|
||||
self.put_u32(inode["offset"] + 44, value)
|
||||
return self.inode(nid)
|
||||
|
||||
def data_offset(self, inode: dict[str, int]) -> int:
|
||||
if inode["layout"] == 2:
|
||||
return inode["offset"] + inode["inode_size"] + inode["xattr_size"]
|
||||
if inode["layout"] == 0:
|
||||
return inode["startblk"] << self.block_bits
|
||||
raise ValueError(f"unsupported directory layout {inode['layout']}")
|
||||
|
||||
def directory(self, nid: int) -> dict[str, dict[str, int]]:
|
||||
inode = self.inode(nid)
|
||||
if inode["mode"] & S_IFMT != S_IFDIR:
|
||||
raise ValueError(f"inode {nid} is not a directory")
|
||||
data_offset = self.data_offset(inode)
|
||||
size = inode["size"]
|
||||
first_nameoff = self.u16(data_offset + 8)
|
||||
if first_nameoff == 0 or first_nameoff % 12 != 0 or first_nameoff >= size:
|
||||
raise ValueError("invalid first name offset")
|
||||
count = first_nameoff // 12
|
||||
records: dict[str, dict[str, int]] = {}
|
||||
for index in range(count):
|
||||
entry = data_offset + index * 12
|
||||
nameoff = self.u16(entry + 8)
|
||||
endoff = self.u16(entry + 20) if index + 1 < count else size
|
||||
raw = bytes(self.data[data_offset + nameoff : data_offset + endoff])
|
||||
name = raw.split(b"\0", 1)[0].decode("ascii")
|
||||
if not name or name in records:
|
||||
raise ValueError(f"bad directory name {name!r}")
|
||||
records[name] = {
|
||||
"entry_offset": entry,
|
||||
"nid": self.u64(entry),
|
||||
"file_type": self.data[entry + 10],
|
||||
}
|
||||
return records
|
||||
|
||||
|
||||
def make_source(path: Path, extended_uid: bool) -> None:
|
||||
previous_umask = os.umask(0o022)
|
||||
try:
|
||||
path.mkdir(mode=0o755)
|
||||
(path / "compact").write_bytes(b"compact zero-nlink target\n")
|
||||
(path / "extended").write_bytes(b"extended zero-nlink target\n")
|
||||
if extended_uid:
|
||||
os.chown(path / "extended", 70000, 0)
|
||||
(path / "hard-a").write_bytes(b"hardlink control\n")
|
||||
os.link(path / "hard-a", path / "hard-b")
|
||||
(path / "orphan").write_bytes(b"orphan raw-vget control\n")
|
||||
(path / "subdir").mkdir(mode=0o755)
|
||||
(path / "subdir" / "child").write_bytes(b"directory child\n")
|
||||
for regular in ("compact", "extended", "hard-a", "hard-b", "orphan"):
|
||||
os.chmod(path / regular, 0o644)
|
||||
os.chmod(path / "subdir" / "child", 0o644)
|
||||
finally:
|
||||
os.umask(previous_umask)
|
||||
|
||||
|
||||
def build_seed(work: Path, output: Path, inode_mode: str) -> tuple[bytes, str, list[str]]:
|
||||
work.mkdir()
|
||||
source = work / "source"
|
||||
make_source(source, inode_mode == "extended")
|
||||
command = [
|
||||
"mkfs.erofs",
|
||||
"-d0",
|
||||
"-T0",
|
||||
"--all-time",
|
||||
"--ignore-mtime",
|
||||
"--workers=1",
|
||||
"-x-1",
|
||||
"-E",
|
||||
"noinline_data",
|
||||
"-E",
|
||||
f"force-inode-{inode_mode}",
|
||||
"-U",
|
||||
SPEC["uuid"],
|
||||
str(output),
|
||||
str(source),
|
||||
]
|
||||
env = dict(os.environ)
|
||||
env.pop("SOURCE_DATE_EPOCH", None)
|
||||
completed = run(command, env=env)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit(f"mkfs.erofs failed: {completed.stdout}")
|
||||
canonical = command[:-2] + [f"SEED-{inode_mode}.erofs", "SOURCE"]
|
||||
return output.read_bytes(), completed.stdout, canonical
|
||||
|
||||
|
||||
resolved = git(ROOT, "rev-parse", f"{REQUESTED_BASE}^{{commit}}")
|
||||
if resolved != SPEC["required_base"]:
|
||||
raise SystemExit(f"wrong BASE: expected {SPEC['required_base']}, got {resolved}")
|
||||
if git(FREEBSD_SRC, "rev-parse", "HEAD") != SPEC["freebsd_head"]:
|
||||
raise SystemExit("FreeBSD source HEAD changed")
|
||||
if git(UTILS_SRC, "rev-parse", "HEAD") != SPEC["utils_head"]:
|
||||
raise SystemExit("erofs-utils source HEAD changed")
|
||||
if QEMU_BASE == Path(SPEC["protected"]["base_image"]):
|
||||
raise SystemExit("refusing the protected base bp")
|
||||
if SPEC["protected"]["port"] >= 32768:
|
||||
raise SystemExit("protected port unexpectedly overlaps ephemeral ports")
|
||||
|
||||
sources: dict[str, str] = {}
|
||||
source_hashes: dict[str, str] = {}
|
||||
for path, expected in SPEC["source_sha256"].items():
|
||||
content = source_at(resolved, path)
|
||||
actual = sha256_bytes(content.encode("utf-8"))
|
||||
if actual != expected:
|
||||
raise SystemExit(f"source hash changed for {path}: {actual}")
|
||||
sources[path] = content
|
||||
source_hashes[path] = actual
|
||||
for path, expected in SPEC["freebsd_sha256"].items():
|
||||
actual = sha256_path(FREEBSD_SRC / path)
|
||||
if actual != expected:
|
||||
raise SystemExit(f"FreeBSD hash changed for {path}: {actual}")
|
||||
for path, expected in SPEC["utils_sha256"].items():
|
||||
actual = sha256_path(UTILS_SRC / path)
|
||||
if actual != expected:
|
||||
raise SystemExit(f"erofs-utils hash changed for {path}: {actual}")
|
||||
|
||||
mkfs_version = run(["mkfs.erofs", "-V"]).stdout.splitlines()[0]
|
||||
if mkfs_version != SPEC["mkfs_version"]:
|
||||
raise SystemExit(f"mkfs version changed: {mkfs_version!r}")
|
||||
|
||||
namei = sources["repo-pre-15/src/namei.c"]
|
||||
inode_source = sources["repo-pre-15/src/inode.c"]
|
||||
vnops = sources["repo-pre-15/src/erofs_vnops.c"]
|
||||
super_source = sources["repo-pre-15/src/super.c"]
|
||||
linux_inode = sources["src-linux/inode.c"]
|
||||
linux_super = sources["src-linux/super.c"]
|
||||
freebsd_vm = (FREEBSD_SRC / "sys/vm/vm_mmap.c").read_text(encoding="utf-8")
|
||||
|
||||
source_anchors = {
|
||||
"freebsd_disk_compact_nlink": "vi->nlink = le16toh(dic->i_nb.nlink);" in inode_source,
|
||||
"freebsd_disk_extended_nlink": "vi->nlink = le32toh(die->i_nlink);" in inode_source,
|
||||
"freebsd_inode_decode_has_no_zero_reject": "vi->nlink == 0" not in inode_source,
|
||||
"freebsd_lookup_has_namespace_edge": "error = erofs_namei(sbi, dir, &qname, &nid, &dtype);" in namei,
|
||||
"freebsd_lookup_calls_raw_vget": "error = erofs_vget(dvp->v_mount, nid, cnp->cn_lkflags, &vp);" in namei,
|
||||
"freebsd_lookup_publishes_namecache": "cache_enter(dvp, vp, cnp);" in namei,
|
||||
"freebsd_raw_vget_registered": ".vfs_vget = erofs_vget," in super_source,
|
||||
"freebsd_root_uses_raw_vget": "error = erofs_vget(mp, MTOE(mp)->root_nid, flags, vpp);" in super_source,
|
||||
"freebsd_fhtovp_filters_zero": "vi->mode == 0 || vi->nlink == 0 || vi->nid != nid" in super_source,
|
||||
"freebsd_hash_insert_before_decode": vnops.index("error = vfs_hash_insert(") < vnops.index("error = erofs_read_inode("),
|
||||
"freebsd_constructed_before_return": vnops.index("vn_set_state(vp, VSTATE_CONSTRUCTED);") < vnops.index("*vpp = vp;"),
|
||||
"freebsd_vget_has_no_hit_indicator": "bool shared;" in vnops and "bool created" not in vnops,
|
||||
"freebsd_vput_does_not_imply_vgone": "vgone(vp);\n\t\tvput(vp);" in vnops,
|
||||
"freebsd_vfs_allows_unlinked_vnode": "if (va.va_nlink == 0)" in freebsd_vm,
|
||||
"linux_compact_sets_disk_nlink": "set_nlink(inode, le16_to_cpu(dic->i_nb.nlink));" in linux_inode,
|
||||
"linux_extended_sets_disk_nlink": "set_nlink(inode, le32_to_cpu(die->i_nlink));" in linux_inode,
|
||||
"linux_inode_has_no_zero_reject": "i_nlink == 0" not in linux_inode,
|
||||
"linux_super_has_no_zero_reject": "i_nlink == 0" not in linux_super,
|
||||
}
|
||||
if not all(source_anchors.values()):
|
||||
raise SystemExit(f"source anchors changed: {source_anchors}")
|
||||
|
||||
fixtures = OUTPUT / "fixtures"
|
||||
fixtures.mkdir()
|
||||
with tempfile.TemporaryDirectory(prefix="p15-092-gate-") as temporary:
|
||||
temp = Path(temporary)
|
||||
first_bytes, first_stdout, compact_command = build_seed(
|
||||
temp / "compact-first", temp / "compact-first.erofs", "compact"
|
||||
)
|
||||
second_bytes, second_stdout, compact_repeat_command = build_seed(
|
||||
temp / "compact-second", temp / "compact-second.erofs", "compact"
|
||||
)
|
||||
extended_bytes, extended_stdout, extended_command = build_seed(
|
||||
temp / "extended-first", temp / "extended-first.erofs", "extended"
|
||||
)
|
||||
extended_repeat_bytes, extended_repeat_stdout, extended_repeat_command = build_seed(
|
||||
temp / "extended-second", temp / "extended-second.erofs", "extended"
|
||||
)
|
||||
write_json(
|
||||
OUTPUT / "generator-attempts.json",
|
||||
{
|
||||
"commands": [compact_command, extended_command],
|
||||
"compact_source": [
|
||||
{
|
||||
"gid": path.lstat().st_gid,
|
||||
"mode": stat.S_IMODE(path.lstat().st_mode),
|
||||
"path": str(path.relative_to(temp / "compact-first/source")),
|
||||
"uid": path.lstat().st_uid,
|
||||
}
|
||||
for path in sorted((temp / "compact-first/source").rglob("*"))
|
||||
],
|
||||
},
|
||||
)
|
||||
if (
|
||||
first_bytes != second_bytes
|
||||
or first_stdout != second_stdout
|
||||
or compact_command != compact_repeat_command
|
||||
or extended_bytes != extended_repeat_bytes
|
||||
or extended_stdout != extended_repeat_stdout
|
||||
or extended_command != extended_repeat_command
|
||||
):
|
||||
raise SystemExit("seed generation is not byte reproducible")
|
||||
seed_path = fixtures / "seed.erofs"
|
||||
seed_path.write_bytes(first_bytes)
|
||||
extended_seed_path = fixtures / "seed-extended.erofs"
|
||||
extended_seed_path.write_bytes(extended_bytes)
|
||||
seed = Image(first_bytes)
|
||||
extended_seed = Image(extended_bytes)
|
||||
if not seed.checksum_valid() or not extended_seed.checksum_valid():
|
||||
raise SystemExit("seed checksum is invalid")
|
||||
entries = seed.directory(seed.root_nid)
|
||||
required = {".", "..", "compact", "extended", "hard-a", "hard-b", "orphan", "subdir"}
|
||||
if not required.issubset(entries):
|
||||
raise SystemExit(f"seed entries missing: {sorted(required - entries.keys())}")
|
||||
if entries["hard-a"]["nid"] != entries["hard-b"]["nid"]:
|
||||
raise SystemExit("hardlink names do not share one NID")
|
||||
|
||||
extended_entries = extended_seed.directory(extended_seed.root_nid)
|
||||
seed_inodes = {name: seed.inode(record["nid"]) for name, record in entries.items()}
|
||||
extended_inodes = {
|
||||
name: extended_seed.inode(record["nid"])
|
||||
for name, record in extended_entries.items()
|
||||
}
|
||||
if seed_inodes["compact"]["version"] != 0:
|
||||
raise SystemExit("compact control is not compact")
|
||||
if extended_inodes["extended"]["version"] != 1:
|
||||
raise SystemExit("extended control is not extended")
|
||||
if seed_inodes["hard-a"]["nlink"] != 2:
|
||||
raise SystemExit("hardlink control does not have nlink 2")
|
||||
|
||||
mutations = []
|
||||
|
||||
def emit_zero(case_id: str, name: str, source: Image = seed) -> None:
|
||||
image = source.clone()
|
||||
nid = image.directory(image.root_nid)[name]["nid"]
|
||||
before = image.inode(nid)
|
||||
after = image.set_nlink(nid, 0)
|
||||
image.update_checksum()
|
||||
path = fixtures / f"{case_id}.erofs"
|
||||
path.write_bytes(image.data)
|
||||
mutations.append({
|
||||
"case": case_id,
|
||||
"class": "namespace-reachable-zero",
|
||||
"name": name,
|
||||
"nid": nid,
|
||||
"before": before,
|
||||
"after": after,
|
||||
"fixture": path.name,
|
||||
})
|
||||
|
||||
emit_zero("reachable-compact-zero", "compact")
|
||||
emit_zero("reachable-extended-zero", "extended", extended_seed)
|
||||
emit_zero("reachable-directory-zero", "subdir")
|
||||
|
||||
root_zero = seed.clone()
|
||||
root_before = root_zero.inode(root_zero.root_nid)
|
||||
root_after = root_zero.set_nlink(root_zero.root_nid, 0)
|
||||
root_zero.update_checksum()
|
||||
root_path = fixtures / "root-zero.erofs"
|
||||
root_path.write_bytes(root_zero.data)
|
||||
mutations.append({
|
||||
"case": "root-zero",
|
||||
"class": "raw-root-zero",
|
||||
"name": "/",
|
||||
"nid": root_zero.root_nid,
|
||||
"before": root_before,
|
||||
"after": root_after,
|
||||
"fixture": root_path.name,
|
||||
})
|
||||
|
||||
orphan = seed.clone()
|
||||
orphan_entries = orphan.directory(orphan.root_nid)
|
||||
orphan_nid = orphan_entries["orphan"]["nid"]
|
||||
hard_nid = orphan_entries["hard-a"]["nid"]
|
||||
orphan.put_u64(orphan_entries["orphan"]["entry_offset"], hard_nid)
|
||||
orphan.set_nlink(orphan_nid, 0)
|
||||
orphan.set_nlink(hard_nid, 3)
|
||||
orphan.update_checksum()
|
||||
orphan_path = fixtures / "orphan-zero.erofs"
|
||||
orphan_path.write_bytes(orphan.data)
|
||||
orphan_names = orphan.directory(orphan.root_nid)
|
||||
if any(record["nid"] == orphan_nid for record in orphan_names.values()):
|
||||
raise SystemExit("orphan fixture still has a namespace edge")
|
||||
mutations.append({
|
||||
"case": "orphan-zero",
|
||||
"class": "unreachable-zero",
|
||||
"name": None,
|
||||
"nid": orphan_nid,
|
||||
"before": seed.inode(orphan_nid),
|
||||
"after": orphan.inode(orphan_nid),
|
||||
"replacement_nid": hard_nid,
|
||||
"replacement_nlink": orphan.inode(hard_nid)["nlink"],
|
||||
"fixture": orphan_path.name,
|
||||
})
|
||||
|
||||
records = []
|
||||
fixture_digest = hashlib.sha256()
|
||||
for path in sorted(fixtures.glob("*.erofs")):
|
||||
image = Image(path.read_bytes())
|
||||
if not image.checksum_valid():
|
||||
raise SystemExit(f"fixture checksum invalid: {path.name}")
|
||||
fsck = run(["fsck.erofs", "-d0", str(path)])
|
||||
dump_root = run(["dump.erofs", "--path=/", str(path)])
|
||||
if dump_root.returncode != 0:
|
||||
raise SystemExit(f"dump root failed: {path.name}")
|
||||
normal = {}
|
||||
for name in ("compact", "extended", "hard-a", "hard-b", "orphan", "subdir"):
|
||||
completed = run(["dump.erofs", f"--path=/{name}", str(path)])
|
||||
normal[name] = {
|
||||
"exit": completed.returncode,
|
||||
"nid": int(match.group(1)) if (match := re.search(r"^NID: (\d+)\b", completed.stdout, re.MULTILINE)) else None,
|
||||
}
|
||||
record = {
|
||||
"fixture": path.name,
|
||||
"sha256": sha256_path(path),
|
||||
"size": path.stat().st_size,
|
||||
"checksum_valid": True,
|
||||
"fsck_exit": fsck.returncode,
|
||||
"fsck_error_marker": "<E>" in fsck.stdout,
|
||||
"normal_namespace": normal,
|
||||
}
|
||||
records.append(record)
|
||||
fixture_digest.update(path.name.encode("ascii"))
|
||||
fixture_digest.update(b"\0")
|
||||
fixture_digest.update(path.read_bytes())
|
||||
|
||||
write_json(
|
||||
OUTPUT / "generator.json",
|
||||
{
|
||||
"commands": [compact_command, extended_command],
|
||||
"mkfs_version": mkfs_version,
|
||||
"repeat_byte_identical": True,
|
||||
"seed_sha256": {
|
||||
"compact": sha256_bytes(first_bytes),
|
||||
"extended": sha256_bytes(extended_bytes),
|
||||
},
|
||||
},
|
||||
)
|
||||
(OUTPUT / "mkfs.stdout").write_text(
|
||||
first_stdout + extended_stdout, encoding="utf-8"
|
||||
)
|
||||
write_json(OUTPUT / "disk-records.json", {"mutations": mutations, "records": records})
|
||||
write_json(
|
||||
OUTPUT / "fixture-index.json",
|
||||
{
|
||||
"fixture_count": len(records),
|
||||
"fixture_set_sha256": fixture_digest.hexdigest(),
|
||||
"fixtures": records,
|
||||
},
|
||||
)
|
||||
|
||||
fsck_reachable_zero_rejects = []
|
||||
for record in records:
|
||||
if record["fixture"].startswith("reachable-"):
|
||||
fsck_reachable_zero_rejects.append(
|
||||
record["fsck_exit"] != 0 or record["fsck_error_marker"]
|
||||
)
|
||||
|
||||
boundary = {
|
||||
"b15_write_set": SPEC["write_set"],
|
||||
"vnode_constructor_owner": "repo-pre-15/src/erofs_vnops.c",
|
||||
"constructor_inside_b15_write_set": "repo-pre-15/src/erofs_vnops.c" in SPEC["write_set"],
|
||||
"post_vget_check_is_before_constructed_publication": False,
|
||||
"vput_only_performs_vgone_cleanup": False,
|
||||
"post_vget_vgone_preserves_cached_raw_vget": False,
|
||||
"pre_vget_full_decode_avoids_duplicate_inode_decode": False,
|
||||
"mount_wide_scan_required": False,
|
||||
"namespace_and_raw_entrypoints_distinct": True,
|
||||
}
|
||||
format_semantics = {
|
||||
"mkfs_emits_nonzero_namespace_links": all(
|
||||
seed_inodes[name]["nlink"] > 0
|
||||
for name in ("compact", "extended", "hard-a", "hard-b", "orphan", "subdir", ".")
|
||||
),
|
||||
"fsck_rejects_all_reachable_zero": all(fsck_reachable_zero_rejects),
|
||||
"linux_rejects_zero_during_inode_decode": False,
|
||||
"linux_copies_disk_nlink_into_inode": True,
|
||||
"freebsd_vfs_permits_live_unlinked_vnode": True,
|
||||
"freebsd_fhtovp_treats_zero_as_stale": True,
|
||||
}
|
||||
go_requirements = {
|
||||
"format_or_cross_kernel_rule_requires_rejection": (
|
||||
format_semantics["fsck_rejects_all_reachable_zero"]
|
||||
and format_semantics["linux_rejects_zero_during_inode_decode"]
|
||||
),
|
||||
"reject_before_vnode_publication_within_write_set": (
|
||||
boundary["constructor_inside_b15_write_set"]
|
||||
and boundary["post_vget_check_is_before_constructed_publication"]
|
||||
),
|
||||
"cleanup_preserves_raw_vget": boundary["post_vget_vgone_preserves_cached_raw_vget"],
|
||||
"no_duplicate_full_inode_decode": boundary["pre_vget_full_decode_avoids_duplicate_inode_decode"],
|
||||
"orphan_needs_no_mount_scan": not boundary["mount_wide_scan_required"],
|
||||
"namespace_raw_split_proven": boundary["namespace_and_raw_entrypoints_distinct"],
|
||||
}
|
||||
status = "GO" if all(go_requirements.values()) else "STOP"
|
||||
reasons = [key for key, value in go_requirements.items() if not value]
|
||||
write_json(OUTPUT / "source-anchors.json", source_anchors)
|
||||
write_json(OUTPUT / "source-sha256.json", source_hashes)
|
||||
write_json(OUTPUT / "boundary.json", boundary)
|
||||
write_json(OUTPUT / "format-semantics.json", format_semantics)
|
||||
write_json(
|
||||
OUTPUT / "host-result.json",
|
||||
{
|
||||
"candidate": SPEC["candidate"],
|
||||
"gate": SPEC["gate"],
|
||||
"fixture_count": len(records),
|
||||
"fixture_set_sha256": fixture_digest.hexdigest(),
|
||||
"go_requirements": go_requirements,
|
||||
"reasons": reasons,
|
||||
"requested_base": REQUESTED_BASE,
|
||||
"resolved_base": resolved,
|
||||
"schema": 1,
|
||||
"status": status,
|
||||
},
|
||||
)
|
||||
PY
|
||||
|
||||
if test "$host_only" -eq 1; then
|
||||
cat "$output/host/host-result.json"
|
||||
exit 22
|
||||
fi
|
||||
|
||||
runtime_tmp=$(mktemp -d "${TMPDIR:-/tmp}/p15-092-runtime.XXXXXX")
|
||||
runtime_dut=$runtime_tmp/repo-pre-15
|
||||
runtime_case=$runtime_dut/tests/pre15/cases/P15-092-gate-runtime.sh
|
||||
runtime_cleanup()
|
||||
{
|
||||
rm -rf "$runtime_tmp"
|
||||
}
|
||||
trap runtime_cleanup EXIT HUP INT TERM
|
||||
mkdir -p "$runtime_dut/tests/pre15/cases" "$runtime_dut/tests/pre15/fixtures"
|
||||
git -C "$root" archive --format=tar --output="$runtime_tmp/frozen-src.tar" \
|
||||
"$base" repo-pre-15/src
|
||||
tar -C "$runtime_tmp" -xf "$runtime_tmp/frozen-src.tar"
|
||||
ln -s "$dut/tests/pre15/fixtures/B14-build-kld.sh" \
|
||||
"$runtime_dut/tests/pre15/fixtures/B14-build-kld.sh"
|
||||
|
||||
python3 -B - "$runtime_case" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
Path(sys.argv[1]).write_text(r'''#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
|
||||
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
|
||||
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
|
||||
: "${P15_092_GATE_DUT:?P15_092_GATE_DUT is required}"
|
||||
: "${P15_092_GATE_HOST:?P15_092_GATE_HOST is required}"
|
||||
. "$PRE15_LIB_DIR/runner.sh"
|
||||
|
||||
artifacts=$PRE15_RUN_DIR/artifacts
|
||||
fixtures=$P15_092_GATE_HOST/fixtures
|
||||
builder=$P15_092_GATE_DUT/tests/pre15/fixtures/B14-build-kld.sh
|
||||
module=$PRE15_CASE_TMP/P15-092-baseline-erofs.ko
|
||||
archive=$PRE15_CASE_TMP/P15-092-fixtures.tar.gz
|
||||
mkdir -p "$artifacts"
|
||||
|
||||
pre15_record_fixture p15-092-host-result "$P15_092_GATE_HOST/host-result.json"
|
||||
pre15_record_fixture p15-092-disk-records "$P15_092_GATE_HOST/disk-records.json"
|
||||
pre15_record_fixture p15-092-boundary "$P15_092_GATE_HOST/boundary.json"
|
||||
pre15_record_fixture p15-092-source-namei "$P15_092_GATE_DUT/src/namei.c"
|
||||
pre15_record_fixture p15-092-source-inode "$P15_092_GATE_DUT/src/inode.c"
|
||||
pre15_record_fixture p15-092-source-vnops "$P15_092_GATE_DUT/src/erofs_vnops.c"
|
||||
|
||||
if ! /bin/sh "$builder" "$P15_092_GATE_DUT" "$PRE15_FREEBSD_SRC" "$module" \
|
||||
"$PRE15_CASE_TMP/kld-work" >"$artifacts/kld-build.stdout" \
|
||||
2>"$artifacts/kld-build.stderr"; then
|
||||
pre15_dut_fail 'baseline cross-target KLD build failed'
|
||||
fi
|
||||
pre15_record_module "$module"
|
||||
tar -C "$fixtures" -czf "$archive" .
|
||||
|
||||
pre15_scp()
|
||||
{
|
||||
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
|
||||
-o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
|
||||
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
|
||||
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
|
||||
"$1" "[email protected]:$2"
|
||||
}
|
||||
|
||||
pre15_scp "$module" /root/P15-092-baseline-erofs.ko || \
|
||||
pre15_infra_blocked 'could not transfer baseline KLD'
|
||||
pre15_scp "$archive" /root/P15-092-fixtures.tar.gz || \
|
||||
pre15_infra_blocked 'could not transfer zero-nlink fixtures'
|
||||
pre15_guest_ssh_bounded \
|
||||
'rm -rf /root/P15-092-fixtures && mkdir /root/P15-092-fixtures && tar -xzf /root/P15-092-fixtures.tar.gz -C /root/P15-092-fixtures' || \
|
||||
pre15_infra_blocked 'could not prepare guest fixtures'
|
||||
if pre15_guest_ssh_bounded kldstat -n erofs >/dev/null 2>&1; then
|
||||
pre15_infra_blocked 'guest already has an EROFS module loaded'
|
||||
fi
|
||||
pre15_guest_ssh_bounded kldload /root/P15-092-baseline-erofs.ko || \
|
||||
pre15_dut_fail 'baseline KLD failed to load'
|
||||
pre15_own_guest_kld erofs 'P15-092 baseline KLD'
|
||||
pre15_target_reached
|
||||
|
||||
attach_mount()
|
||||
{
|
||||
image=$1
|
||||
mountpoint=$2
|
||||
label=$3
|
||||
md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
|
||||
-f "/root/P15-092-fixtures/$image") || \
|
||||
pre15_dut_fail "$label md attach failed"
|
||||
case "$md" in
|
||||
md[0-9]*) ;;
|
||||
*) pre15_runner_fail "unexpected md unit: $md" ;;
|
||||
esac
|
||||
pre15_own_guest_md "$md" "$label md"
|
||||
pre15_guest_ssh_bounded mkdir -p "$mountpoint"
|
||||
pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$md" "$mountpoint" || \
|
||||
pre15_dut_fail "$label mount failed"
|
||||
pre15_own_guest_mount "$mountpoint" "$label mount"
|
||||
}
|
||||
|
||||
expect_nlink()
|
||||
{
|
||||
path=$1
|
||||
expected=$2
|
||||
label=$3
|
||||
actual=$(pre15_guest_ssh_bounded stat -f '%l' "$path") || \
|
||||
pre15_dut_fail "$label normal stat failed"
|
||||
test "$actual" = "$expected" || \
|
||||
pre15_dut_fail "$label nlink expected $expected got $actual"
|
||||
printf '%s\t%s\t%s\n' "$label" "$path" "$actual" >>"$artifacts/normal-vnode.tsv"
|
||||
}
|
||||
|
||||
attach_mount seed.erofs /mnt/p15-092-seed seed
|
||||
expect_nlink /mnt/p15-092-seed/compact 1 seed-compact
|
||||
expect_nlink /mnt/p15-092-seed/extended 1 seed-extended
|
||||
expect_nlink /mnt/p15-092-seed/hard-a 2 seed-hard-a
|
||||
hard_a=$(pre15_guest_ssh_bounded stat -f '%i' /mnt/p15-092-seed/hard-a)
|
||||
hard_b=$(pre15_guest_ssh_bounded stat -f '%i' /mnt/p15-092-seed/hard-b)
|
||||
test "$hard_a" = "$hard_b" || pre15_dut_fail 'hardlink NIDs differ'
|
||||
|
||||
attach_mount reachable-compact-zero.erofs /mnt/p15-092-compact compact-zero
|
||||
expect_nlink /mnt/p15-092-compact/compact 0 reachable-compact-zero
|
||||
attach_mount reachable-extended-zero.erofs /mnt/p15-092-extended extended-zero
|
||||
expect_nlink /mnt/p15-092-extended/extended 0 reachable-extended-zero
|
||||
attach_mount reachable-directory-zero.erofs /mnt/p15-092-directory directory-zero
|
||||
expect_nlink /mnt/p15-092-directory/subdir 0 reachable-directory-zero
|
||||
attach_mount root-zero.erofs /mnt/p15-092-root root-zero
|
||||
expect_nlink /mnt/p15-092-root 0 root-zero
|
||||
attach_mount orphan-zero.erofs /mnt/p15-092-orphan orphan-zero
|
||||
expect_nlink /mnt/p15-092-orphan/orphan 3 orphan-replacement
|
||||
pre15_guest_ssh_bounded cat /mnt/p15-092-orphan/orphan >"$artifacts/orphan-replacement.txt"
|
||||
pre15_guest_ssh_bounded dmesg >"$artifacts/dmesg.txt"
|
||||
printf '%s\n' 'P15-092 baseline normal vnode runtime PASS'
|
||||
''', encoding="ascii")
|
||||
PY
|
||||
chmod 0555 "$runtime_case"
|
||||
|
||||
qemu_stdout=$output/qemu.stdout
|
||||
qemu_stderr=$output/qemu.stderr
|
||||
qemu_evidence=$output/qemu-evidence
|
||||
mkdir "$qemu_evidence"
|
||||
if PRE15_DUT="$runtime_dut" PRE15_ROOT="$root" \
|
||||
PRE15_SCHEMA="$dut/tests/pre15/EVIDENCE-SCHEMA.json" \
|
||||
PRE15_EVIDENCE_ROOT="$qemu_evidence" PRE15_FREEBSD_SRC="$freebsd_src" \
|
||||
PRE15_QEMU_BASE_IMAGE="$qemu_base" PRE15_QEMU_BASE_FORMAT=qcow2 \
|
||||
PRE15_QEMU_SSH_KEY="$qemu_key" PRE15_QEMU_SSH_USER=root \
|
||||
PRE15_QEMU_MEMORY_MB=2048 PRE15_QEMU_CPUS=2 PRE15_QEMU_BOOT_TIMEOUT=180 \
|
||||
PRE15_QEMU_TIMEOUT=900 PRE15_GUEST_COMMAND_TIMEOUT=60 \
|
||||
P15_092_GATE_DUT="$runtime_dut" P15_092_GATE_HOST="$output/host" \
|
||||
timeout -k 30 1000 "$dut/tests/pre15/run-qemu.sh" P15-092-gate-runtime \
|
||||
>"$qemu_stdout" 2>"$qemu_stderr"; then
|
||||
qemu_rc=0
|
||||
else
|
||||
qemu_rc=$?
|
||||
fi
|
||||
printf '%s\n' "$qemu_rc" >"$output/qemu.exit"
|
||||
|
||||
python3 -B - "$output" "$qemu_rc" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
OUTPUT = Path(sys.argv[1])
|
||||
QEMU_RC = int(sys.argv[2])
|
||||
|
||||
|
||||
def sha256_path(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def write_json(path: Path, value: object) -> None:
|
||||
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
|
||||
|
||||
host = json.loads((OUTPUT / "host/host-result.json").read_text(encoding="ascii"))
|
||||
stdout = (OUTPUT / "qemu.stdout").read_text(encoding="utf-8")
|
||||
match = re.search(r"PRE15_RESULT status=(\S+) cleanup=(\S+) run_id=(\S+) evidence=(\S+)", stdout)
|
||||
if match is None:
|
||||
runtime = {
|
||||
"cleanup": "UNKNOWN",
|
||||
"evidence": None,
|
||||
"exit": QEMU_RC,
|
||||
"run_id": None,
|
||||
"status": "RUNNER_FAIL",
|
||||
}
|
||||
else:
|
||||
runtime = {
|
||||
"status": match.group(1),
|
||||
"cleanup": match.group(2),
|
||||
"run_id": match.group(3),
|
||||
"evidence": match.group(4),
|
||||
"exit": QEMU_RC,
|
||||
}
|
||||
|
||||
status = "STOP"
|
||||
reasons = list(host["reasons"])
|
||||
if runtime["status"] != "PASS" or runtime["cleanup"] != "PASS":
|
||||
reasons.append("normal_vnode_runtime_not_pass")
|
||||
result = {
|
||||
"b15": "STOP-NO-SOURCE",
|
||||
"candidate": "P15-092",
|
||||
"full_feature_suite": "NOT_RUN",
|
||||
"gate": "G11",
|
||||
"host_status": host["status"],
|
||||
"qemu": runtime,
|
||||
"reasons": reasons,
|
||||
"schema": 1,
|
||||
"status": status,
|
||||
}
|
||||
write_json(OUTPUT / "result.json", result)
|
||||
|
||||
hash_lines = []
|
||||
for path in sorted(OUTPUT.rglob("*")):
|
||||
if path.is_file() and path.name != "SHA256SUMS":
|
||||
hash_lines.append(f"{sha256_path(path)} {path.relative_to(OUTPUT)}")
|
||||
(OUTPUT / "SHA256SUMS").write_text("\n".join(hash_lines) + "\n", encoding="ascii")
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
PY
|
||||
|
||||
cat "$output/result.json"
|
||||
exit 22
|
||||
Reference in New Issue
Block a user