| // SPDX-License-Identifier: GPL-2.0-only | |
| /* Copyright (c) 2010-2020 NVIDIA Corporation */ | |
| #include "drm.h" | |
| #include "submit.h" | |
| #include "uapi.h" | |
| struct tegra_drm_firewall { | |
| struct tegra_drm_submit_data *submit; | |
| struct tegra_drm_client *client; | |
| u32 *data; | |
| u32 pos; | |
| u32 end; | |
| u32 class; | |
| }; | |
| static int fw_next(struct tegra_drm_firewall *fw, u32 *word) | |
| { | |
| if (fw->pos == fw->end) | |
| return -EINVAL; | |
| *word = fw->data[fw->pos++]; | |
| return 0; | |
| } | |
| static bool fw_check_addr_valid(struct tegra_drm_firewall *fw, u32 offset) | |
| { | |
| u32 i; | |
| for (i = 0; i < fw->submit->num_used_mappings; i++) { | |
| struct tegra_drm_mapping *m = fw->submit->used_mappings[i].mapping; | |
| if (offset >= m->iova && offset <= m->iova_end) | |
| return true; | |
| } | |
| return false; | |
| } | |
| static int fw_check_reg(struct tegra_drm_firewall *fw, u32 offset) | |
| { | |
| bool is_addr; | |
| u32 word; | |
| int err; | |
| err = fw_next(fw, &word); | |
| if (err) | |
| return err; | |
| if (!fw->client->ops->is_addr_reg) | |
| return 0; | |
| is_addr = fw->client->ops->is_addr_reg(fw->client->base.dev, fw->class, | |
| offset); | |
| if (!is_addr) | |
| return 0; | |
| if (!fw_check_addr_valid(fw, word)) | |
| return -EINVAL; | |
| return 0; | |
| } | |
| static int fw_check_regs_seq(struct tegra_drm_firewall *fw, u32 offset, | |
| u32 count, bool incr) | |
| { | |
| u32 i; | |
| for (i = 0; i < count; i++) { | |
| if (fw_check_reg(fw, offset)) | |
| return -EINVAL; | |
| if (incr) | |
| offset++; | |
| } | |
| return 0; | |
| } | |
| static int fw_check_regs_mask(struct tegra_drm_firewall *fw, u32 offset, | |
| u16 mask) | |
| { | |
| unsigned long bmask = mask; | |
| unsigned int bit; | |
| for_each_set_bit(bit, &bmask, 16) { | |
| if (fw_check_reg(fw, offset+bit)) | |
| return -EINVAL; | |
| } | |
| return 0; | |
| } | |
| static int fw_check_regs_imm(struct tegra_drm_firewall *fw, u32 offset) | |
| { | |
| bool is_addr; | |
| if (!fw->client->ops->is_addr_reg) | |
| return 0; | |
| is_addr = fw->client->ops->is_addr_reg(fw->client->base.dev, fw->class, | |
| offset); | |
| if (is_addr) | |
| return -EINVAL; | |
| return 0; | |
| } | |
| static int fw_check_class(struct tegra_drm_firewall *fw, u32 class) | |
| { | |
| if (!fw->client->ops->is_valid_class) { | |
| if (class == fw->client->base.class) | |
| return 0; | |
| else | |
| return -EINVAL; | |
| } | |
| if (!fw->client->ops->is_valid_class(class)) | |
| return -EINVAL; | |
| return 0; | |
| } | |
| enum { | |
| HOST1X_OPCODE_SETCLASS = 0x00, | |
| HOST1X_OPCODE_INCR = 0x01, | |
| HOST1X_OPCODE_NONINCR = 0x02, | |
| HOST1X_OPCODE_MASK = 0x03, | |
| HOST1X_OPCODE_IMM = 0x04, | |
| HOST1X_OPCODE_RESTART = 0x05, | |
| HOST1X_OPCODE_GATHER = 0x06, | |
| HOST1X_OPCODE_SETSTRMID = 0x07, | |
| HOST1X_OPCODE_SETAPPID = 0x08, | |
| HOST1X_OPCODE_SETPYLD = 0x09, | |
| HOST1X_OPCODE_INCR_W = 0x0a, | |
| HOST1X_OPCODE_NONINCR_W = 0x0b, | |
| HOST1X_OPCODE_GATHER_W = 0x0c, | |
| HOST1X_OPCODE_RESTART_W = 0x0d, | |
| HOST1X_OPCODE_EXTEND = 0x0e, | |
| }; | |
| int tegra_drm_fw_validate(struct tegra_drm_client *client, u32 *data, u32 start, | |
| u32 words, struct tegra_drm_submit_data *submit, | |
| u32 *job_class) | |
| { | |
| struct tegra_drm_firewall fw = { | |
| .submit = submit, | |
| .client = client, | |
| .data = data, | |
| .pos = start, | |
| .end = start+words, | |
| .class = *job_class, | |
| }; | |
| bool payload_valid = false; | |
| u32 payload; | |
| int err; | |
| while (fw.pos != fw.end) { | |
| u32 word, opcode, offset, count, mask, class; | |
| err = fw_next(&fw, &word); | |
| if (err) | |
| return err; | |
| opcode = (word & 0xf0000000) >> 28; | |
| switch (opcode) { | |
| case HOST1X_OPCODE_SETCLASS: | |
| offset = word >> 16 & 0xfff; | |
| mask = word & 0x3f; | |
| class = (word >> 6) & 0x3ff; | |
| err = fw_check_class(&fw, class); | |
| fw.class = class; | |
| *job_class = class; | |
| if (!err) | |
| err = fw_check_regs_mask(&fw, offset, mask); | |
| if (err) | |
| dev_warn(client->base.dev, | |
| "illegal SETCLASS(offset=0x%x, mask=0x%x, class=0x%x) at word %u", | |
| offset, mask, class, fw.pos-1); | |
| break; | |
| case HOST1X_OPCODE_INCR: | |
| offset = (word >> 16) & 0xfff; | |
| count = word & 0xffff; | |
| err = fw_check_regs_seq(&fw, offset, count, true); | |
| if (err) | |
| dev_warn(client->base.dev, | |
| "illegal INCR(offset=0x%x, count=%u) in class 0x%x at word %u", | |
| offset, count, fw.class, fw.pos-1); | |
| break; | |
| case HOST1X_OPCODE_NONINCR: | |
| offset = (word >> 16) & 0xfff; | |
| count = word & 0xffff; | |
| err = fw_check_regs_seq(&fw, offset, count, false); | |
| if (err) | |
| dev_warn(client->base.dev, | |
| "illegal NONINCR(offset=0x%x, count=%u) in class 0x%x at word %u", | |
| offset, count, fw.class, fw.pos-1); | |
| break; | |
| case HOST1X_OPCODE_MASK: | |
| offset = (word >> 16) & 0xfff; | |
| mask = word & 0xffff; | |
| err = fw_check_regs_mask(&fw, offset, mask); | |
| if (err) | |
| dev_warn(client->base.dev, | |
| "illegal MASK(offset=0x%x, mask=0x%x) in class 0x%x at word %u", | |
| offset, mask, fw.class, fw.pos-1); | |
| break; | |
| case HOST1X_OPCODE_IMM: | |
| /* IMM cannot reasonably be used to write a pointer */ | |
| offset = (word >> 16) & 0xfff; | |
| err = fw_check_regs_imm(&fw, offset); | |
| if (err) | |
| dev_warn(client->base.dev, | |
| "illegal IMM(offset=0x%x) in class 0x%x at word %u", | |
| offset, fw.class, fw.pos-1); | |
| break; | |
| case HOST1X_OPCODE_SETPYLD: | |
| payload = word & 0xffff; | |
| payload_valid = true; | |
| break; | |
| case HOST1X_OPCODE_INCR_W: | |
| if (!payload_valid) | |
| return -EINVAL; | |
| offset = word & 0x3fffff; | |
| err = fw_check_regs_seq(&fw, offset, payload, true); | |
| if (err) | |
| dev_warn(client->base.dev, | |
| "illegal INCR_W(offset=0x%x) in class 0x%x at word %u", | |
| offset, fw.class, fw.pos-1); | |
| break; | |
| case HOST1X_OPCODE_NONINCR_W: | |
| if (!payload_valid) | |
| return -EINVAL; | |
| offset = word & 0x3fffff; | |
| err = fw_check_regs_seq(&fw, offset, payload, false); | |
| if (err) | |
| dev_warn(client->base.dev, | |
| "illegal NONINCR(offset=0x%x) in class 0x%x at word %u", | |
| offset, fw.class, fw.pos-1); | |
| break; | |
| default: | |
| dev_warn(client->base.dev, "illegal opcode at word %u", | |
| fw.pos-1); | |
| return -EINVAL; | |
| } | |
| if (err) | |
| return err; | |
| } | |
| return 0; | |
| } |