Commit 85b669d497

Luuk de Gram <luuk@degram.dev>
2022-10-19 21:48:21
wasm-linker: validate feature compatibility
Verifies disallowed and used/required features. After verifying, all errors will be emit to notify the user about incompatible features. When the user did not define any featureset, we infer the features from the linked objects instead.
1 parent 55c5da1
Changed files (2)
src
src/link/Wasm/types.zig
@@ -183,18 +183,7 @@ pub const Feature = struct {
     /// Type of the feature, must be unique in the sequence of features.
     tag: Tag,
 
-    pub const Tag = enum {
-        atomics,
-        bulk_memory,
-        exception_handling,
-        multivalue,
-        mutable_globals,
-        nontrapping_fptoint,
-        sign_ext,
-        simd128,
-        tail_call,
-        shared_mem,
-    };
+    pub const Tag = std.Target.wasm.Feature;
 
     pub const Prefix = enum(u8) {
         used = '+',
@@ -204,13 +193,18 @@ pub const Feature = struct {
 
     pub fn toString(feature: Feature) []const u8 {
         return switch (feature.tag) {
+            .atomics => "atomics",
             .bulk_memory => "bulk-memory",
             .exception_handling => "exception-handling",
+            .extended_const => "extended-const",
+            .multivalue => "multivalue",
             .mutable_globals => "mutable-globals",
             .nontrapping_fptoint => "nontrapping-fptoint",
+            .reference_types => "reference-types",
+            .relaxed_simd => "relaxed-simd",
             .sign_ext => "sign-ext",
+            .simd128 => "simd128",
             .tail_call => "tail-call",
-            else => @tagName(feature),
         };
     }
 
@@ -225,11 +219,13 @@ pub const known_features = std.ComptimeStringMap(Feature.Tag, .{
     .{ "atomics", .atomics },
     .{ "bulk-memory", .bulk_memory },
     .{ "exception-handling", .exception_handling },
+    .{ "extended-const", .extended_const },
     .{ "multivalue", .multivalue },
     .{ "mutable-globals", .mutable_globals },
     .{ "nontrapping-fptoint", .nontrapping_fptoint },
+    .{ "reference-types", .reference_types },
+    .{ "relaxed-simd", .relaxed_simd },
     .{ "sign-ext", .sign_ext },
     .{ "simd128", .simd128 },
     .{ "tail-call", .tail_call },
-    .{ "shared-mem", .shared_mem },
 });
src/link/Wasm.zig
@@ -651,6 +651,112 @@ fn resolveSymbolsInArchives(wasm: *Wasm) !void {
     }
 }
 
+fn validateFeatures(wasm: *const Wasm, arena: Allocator) !void {
+    const cpu_features = wasm.base.options.target.cpu.features;
+    const infer = cpu_features.isEmpty(); // when the user did not define any features, we infer them from linked objects.
+    var allowed = std.AutoHashMap(std.Target.wasm.Feature, void).init(arena);
+    var used = std.AutoArrayHashMap(std.Target.wasm.Feature, []const u8).init(arena);
+    var disallowed = std.AutoHashMap(std.Target.wasm.Feature, []const u8).init(arena);
+    var required = std.AutoHashMap(std.Target.wasm.Feature, []const u8).init(arena);
+
+    // when false, we fail linking. We only verify this after a loop to catch all invalid features.
+    var valid_feature_set = true;
+
+    // When the user has given an explicit list of features to enable,
+    // we extract them and insert each into the 'allowed' list.
+    if (!infer) {
+        try allowed.ensureUnusedCapacity(std.Target.wasm.all_features.len);
+        // std.builtin.Type.EnumField
+        inline for (@typeInfo(std.Target.wasm.Feature).Enum.fields) |feature_field| {
+            if (cpu_features.isEnabled(feature_field.value)) {
+                allowed.putAssumeCapacityNoClobber(@intToEnum(std.Target.wasm.Feature, feature_field.value), {});
+            }
+        }
+    }
+
+    // extract all the used, disallowed and required features from each
+    // linked object file so we can test them.
+    for (wasm.objects.items) |object| {
+        for (object.features) |feature| {
+            switch (feature.prefix) {
+                .used => {
+                    const gop = try used.getOrPut(feature.tag);
+                    if (!gop.found_existing) {
+                        gop.value_ptr.* = object.name;
+                    }
+                },
+                .disallowed => {
+                    const gop = try disallowed.getOrPut(feature.tag);
+                    if (!gop.found_existing) {
+                        gop.value_ptr.* = object.name;
+                    }
+                },
+                .required => {
+                    const gop = try required.getOrPut(feature.tag);
+                    if (!gop.found_existing) {
+                        gop.value_ptr.* = object.name;
+                    }
+                    const used_gop = try used.getOrPut(feature.tag);
+                    if (!used_gop.found_existing) {
+                        used_gop.value_ptr.* = object.name;
+                    }
+                },
+            }
+        }
+    }
+
+    // when we infer the features, we allow each feature found in the 'used' set
+    // and insert it into the 'allowed' set. When features are not inferred,
+    // we validate that a used feature is allowed.
+    if (infer) try allowed.ensureUnusedCapacity(@intCast(u32, used.count()));
+    for (used.keys()) |used_feature, used_index| {
+        if (infer) {
+            allowed.putAssumeCapacityNoClobber(used_feature, {});
+        } else if (!allowed.contains(used_feature)) {
+            log.err("feature '{s}' not allowed, but used by linked object", .{@tagName(used_feature)});
+            log.err("  defined in '{s}'", .{used.values()[used_index]});
+            valid_feature_set = false;
+        }
+    }
+
+    if (!valid_feature_set) {
+        return error.InvalidFeatureSet;
+    }
+
+    // For each linked object, validate the required and disallowed features
+    for (wasm.objects.items) |object| {
+        var object_used_features = std.AutoHashMap(std.Target.wasm.Feature, void).init(arena);
+        try object_used_features.ensureTotalCapacity(@intCast(u32, object.features.len));
+        for (object.features) |feature| {
+            if (feature.prefix == .disallowed) continue; // already defined in 'disallowed' set.
+            // from here a feature is always used
+            if (disallowed.get(feature.tag)) |disallowed_object_name| {
+                log.err("feature '{s}' is disallowed, but used by linked object", .{@tagName(feature.tag)});
+                log.err("  disallowed by '{s}'", .{disallowed_object_name});
+                log.err("  used in '{s}'", .{object.name});
+                valid_feature_set = false;
+            }
+
+            object_used_features.putAssumeCapacity(feature.tag, {});
+        }
+
+        // validate the linked object file has each required feature
+        var required_it = required.iterator();
+        while (required_it.next()) |required_feature| {
+            if (!object_used_features.contains(required_feature.key_ptr.*)) {
+                log.err("feature '{s}' is required but not used in linked object", .{@tagName(required_feature.key_ptr.*)});
+                log.err("  required by '{s}'", .{required_feature.value_ptr.*});
+                log.err("  missing in '{s}'", .{object.name});
+                valid_feature_set = false;
+            }
+        }
+    }
+
+    if (!valid_feature_set) {
+        return error.InvalidFeatureSet;
+    }
+}
+
 fn checkUndefinedSymbols(wasm: *const Wasm) !void {
     if (wasm.base.options.output_mode == .Obj) return;
 
@@ -2158,6 +2264,7 @@ pub fn flushModule(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Nod
         try wasm.resolveSymbolsInObject(@intCast(u16, object_index));
     }
 
+    try wasm.validateFeatures(arena);
     try wasm.resolveSymbolsInArchives();
     try wasm.checkUndefinedSymbols();