linux/drivers/net/wireless/orinoco/hermes_dld.c
David Kilroy 3faa19cd9d orinoco: prevent accessing memory outside the firmware image
Do this by indicating the end of the appropriate regions of memory.

Note that MAX_PDA_SIZE should only apply to the PDA block read from
flash/EEPROM, and has been erronously applied to the pdr elements.
Remove the macro, and use the actual PDA size passed down by the caller.

We also fix up some of the types used, marking as much as possible
const, and using void* for the end pointers.

Signed-off-by: David Kilroy <kilroyd@googlemail.com>
Signed-off-by: John W. Linville <linville@tuxdriver.com>
2009-02-27 14:53:02 -05:00

707 lines
19 KiB
C

/*
* Hermes download helper.
*
* This helper:
* - is capable of writing to the volatile area of the hermes device
* - is currently not capable of writing to non-volatile areas
* - provide helpers to identify and update plugin data
* - is not capable of interpreting a fw image directly. That is up to
* the main card driver.
* - deals with Hermes I devices. It can probably be modified to deal
* with Hermes II devices
*
* Copyright (C) 2007, David Kilroy
*
* Plug data code slightly modified from spectrum_cs driver
* Copyright (C) 2002-2005 Pavel Roskin <proski@gnu.org>
* Portions based on information in wl_lkm_718 Agere driver
* COPYRIGHT (C) 2001-2004 by Agere Systems Inc. All Rights Reserved
*
* The contents of this file are subject to the Mozilla Public License
* Version 1.1 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License
* at http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
* the License for the specific language governing rights and
* limitations under the License.
*
* Alternatively, the contents of this file may be used under the
* terms of the GNU General Public License version 2 (the "GPL"), in
* which case the provisions of the GPL are applicable instead of the
* above. If you wish to allow the use of your version of this file
* only under the terms of the GPL and not to allow others to use your
* version of this file under the MPL, indicate your decision by
* deleting the provisions above and replace them with the notice and
* other provisions required by the GPL. If you do not delete the
* provisions above, a recipient may use your version of this file
* under either the MPL or the GPL.
*/
#include <linux/module.h>
#include <linux/delay.h>
#include "hermes.h"
#include "hermes_dld.h"
#define PFX "hermes_dld: "
/*
* AUX port access. To unlock the AUX port write the access keys to the
* PARAM0-2 registers, then write HERMES_AUX_ENABLE to the HERMES_CONTROL
* register. Then read it and make sure it's HERMES_AUX_ENABLED.
*/
#define HERMES_AUX_ENABLE 0x8000 /* Enable auxiliary port access */
#define HERMES_AUX_DISABLE 0x4000 /* Disable to auxiliary port access */
#define HERMES_AUX_ENABLED 0xC000 /* Auxiliary port is open */
#define HERMES_AUX_DISABLED 0x0000 /* Auxiliary port is closed */
#define HERMES_AUX_PW0 0xFE01
#define HERMES_AUX_PW1 0xDC23
#define HERMES_AUX_PW2 0xBA45
/* HERMES_CMD_DOWNLD */
#define HERMES_PROGRAM_DISABLE (0x0000 | HERMES_CMD_DOWNLD)
#define HERMES_PROGRAM_ENABLE_VOLATILE (0x0100 | HERMES_CMD_DOWNLD)
#define HERMES_PROGRAM_ENABLE_NON_VOLATILE (0x0200 | HERMES_CMD_DOWNLD)
#define HERMES_PROGRAM_NON_VOLATILE (0x0300 | HERMES_CMD_DOWNLD)
/* End markers used in dblocks */
#define PDI_END 0x00000000 /* End of PDA */
#define BLOCK_END 0xFFFFFFFF /* Last image block */
#define TEXT_END 0x1A /* End of text header */
/* Limit the amout we try to download in a single shot.
* Size is in bytes.
*/
#define MAX_DL_SIZE 1024
#define LIMIT_PROGRAM_SIZE 0
/*
* The following structures have little-endian fields denoted by
* the leading underscore. Don't access them directly - use inline
* functions defined below.
*/
/*
* The binary image to be downloaded consists of series of data blocks.
* Each block has the following structure.
*/
struct dblock {
__le32 addr; /* adapter address where to write the block */
__le16 len; /* length of the data only, in bytes */
char data[0]; /* data to be written */
} __attribute__ ((packed));
/*
* Plug Data References are located in in the image after the last data
* block. They refer to areas in the adapter memory where the plug data
* items with matching ID should be written.
*/
struct pdr {
__le32 id; /* record ID */
__le32 addr; /* adapter address where to write the data */
__le32 len; /* expected length of the data, in bytes */
char next[0]; /* next PDR starts here */
} __attribute__ ((packed));
/*
* Plug Data Items are located in the EEPROM read from the adapter by
* primary firmware. They refer to the device-specific data that should
* be plugged into the secondary firmware.
*/
struct pdi {
__le16 len; /* length of ID and data, in words */
__le16 id; /* record ID */
char data[0]; /* plug data */
} __attribute__ ((packed));
/*** FW data block access functions ***/
static inline u32
dblock_addr(const struct dblock *blk)
{
return le32_to_cpu(blk->addr);
}
static inline u32
dblock_len(const struct dblock *blk)
{
return le16_to_cpu(blk->len);
}
/*** PDR Access functions ***/
static inline u32
pdr_id(const struct pdr *pdr)
{
return le32_to_cpu(pdr->id);
}
static inline u32
pdr_addr(const struct pdr *pdr)
{
return le32_to_cpu(pdr->addr);
}
static inline u32
pdr_len(const struct pdr *pdr)
{
return le32_to_cpu(pdr->len);
}
/*** PDI Access functions ***/
static inline u32
pdi_id(const struct pdi *pdi)
{
return le16_to_cpu(pdi->id);
}
/* Return length of the data only, in bytes */
static inline u32
pdi_len(const struct pdi *pdi)
{
return 2 * (le16_to_cpu(pdi->len) - 1);
}
/*** Hermes AUX control ***/
static inline void
hermes_aux_setaddr(hermes_t *hw, u32 addr)
{
hermes_write_reg(hw, HERMES_AUXPAGE, (u16) (addr >> 7));
hermes_write_reg(hw, HERMES_AUXOFFSET, (u16) (addr & 0x7F));
}
static inline int
hermes_aux_control(hermes_t *hw, int enabled)
{
int desired_state = enabled ? HERMES_AUX_ENABLED : HERMES_AUX_DISABLED;
int action = enabled ? HERMES_AUX_ENABLE : HERMES_AUX_DISABLE;
int i;
/* Already open? */
if (hermes_read_reg(hw, HERMES_CONTROL) == desired_state)
return 0;
hermes_write_reg(hw, HERMES_PARAM0, HERMES_AUX_PW0);
hermes_write_reg(hw, HERMES_PARAM1, HERMES_AUX_PW1);
hermes_write_reg(hw, HERMES_PARAM2, HERMES_AUX_PW2);
hermes_write_reg(hw, HERMES_CONTROL, action);
for (i = 0; i < 20; i++) {
udelay(10);
if (hermes_read_reg(hw, HERMES_CONTROL) ==
desired_state)
return 0;
}
return -EBUSY;
}
/*** Plug Data Functions ***/
/*
* Scan PDR for the record with the specified RECORD_ID.
* If it's not found, return NULL.
*/
static const struct pdr *
hermes_find_pdr(const struct pdr *first_pdr, u32 record_id, const void *end)
{
const struct pdr *pdr = first_pdr;
end -= sizeof(struct pdr);
while (((void *) pdr <= end) &&
(pdr_id(pdr) != PDI_END)) {
/*
* PDR area is currently not terminated by PDI_END.
* It's followed by CRC records, which have the type
* field where PDR has length. The type can be 0 or 1.
*/
if (pdr_len(pdr) < 2)
return NULL;
/* If the record ID matches, we are done */
if (pdr_id(pdr) == record_id)
return pdr;
pdr = (struct pdr *) pdr->next;
}
return NULL;
}
/* Scan production data items for a particular entry */
static const struct pdi *
hermes_find_pdi(const struct pdi *first_pdi, u32 record_id, const void *end)
{
const struct pdi *pdi = first_pdi;
end -= sizeof(struct pdi);
while (((void *) pdi <= end) &&
(pdi_id(pdi) != PDI_END)) {
/* If the record ID matches, we are done */
if (pdi_id(pdi) == record_id)
return pdi;
pdi = (struct pdi *) &pdi->data[pdi_len(pdi)];
}
return NULL;
}
/* Process one Plug Data Item - find corresponding PDR and plug it */
static int
hermes_plug_pdi(hermes_t *hw, const struct pdr *first_pdr,
const struct pdi *pdi, const void *pdr_end)
{
const struct pdr *pdr;
/* Find the PDR corresponding to this PDI */
pdr = hermes_find_pdr(first_pdr, pdi_id(pdi), pdr_end);
/* No match is found, safe to ignore */
if (!pdr)
return 0;
/* Lengths of the data in PDI and PDR must match */
if (pdi_len(pdi) != pdr_len(pdr))
return -EINVAL;
/* do the actual plugging */
hermes_aux_setaddr(hw, pdr_addr(pdr));
hermes_write_bytes(hw, HERMES_AUXDATA, pdi->data, pdi_len(pdi));
return 0;
}
/* Read PDA from the adapter */
int hermes_read_pda(hermes_t *hw,
__le16 *pda,
u32 pda_addr,
u16 pda_len,
int use_eeprom) /* can we get this into hw? */
{
int ret;
u16 pda_size;
u16 data_len = pda_len;
__le16 *data = pda;
if (use_eeprom) {
/* PDA of spectrum symbol is in eeprom */
/* Issue command to read EEPROM */
ret = hermes_docmd_wait(hw, HERMES_CMD_READMIF, 0, NULL);
if (ret)
return ret;
} else {
/* wl_lkm does not include PDA size in the PDA area.
* We will pad the information into pda, so other routines
* don't have to be modified */
pda[0] = cpu_to_le16(pda_len - 2);
/* Includes CFG_PROD_DATA but not itself */
pda[1] = cpu_to_le16(0x0800); /* CFG_PROD_DATA */
data_len = pda_len - 4;
data = pda + 2;
}
/* Open auxiliary port */
ret = hermes_aux_control(hw, 1);
printk(KERN_DEBUG PFX "AUX enable returned %d\n", ret);
if (ret)
return ret;
/* read PDA from EEPROM */
hermes_aux_setaddr(hw, pda_addr);
hermes_read_words(hw, HERMES_AUXDATA, data, data_len / 2);
/* Close aux port */
ret = hermes_aux_control(hw, 0);
printk(KERN_DEBUG PFX "AUX disable returned %d\n", ret);
/* Check PDA length */
pda_size = le16_to_cpu(pda[0]);
printk(KERN_DEBUG PFX "Actual PDA length %d, Max allowed %d\n",
pda_size, pda_len);
if (pda_size > pda_len)
return -EINVAL;
return 0;
}
/* Parse PDA and write the records into the adapter
*
* Attempt to write every records that is in the specified pda
* which also has a valid production data record for the firmware.
*/
int hermes_apply_pda(hermes_t *hw,
const char *first_pdr,
const void *pdr_end,
const __le16 *pda,
const void *pda_end)
{
int ret;
const struct pdi *pdi;
const struct pdr *pdr;
pdr = (const struct pdr *) first_pdr;
pda_end -= sizeof(struct pdi);
/* Go through every PDI and plug them into the adapter */
pdi = (const struct pdi *) (pda + 2);
while (((void *) pdi <= pda_end) &&
(pdi_id(pdi) != PDI_END)) {
ret = hermes_plug_pdi(hw, pdr, pdi, pdr_end);
if (ret)
return ret;
/* Increment to the next PDI */
pdi = (const struct pdi *) &pdi->data[pdi_len(pdi)];
}
return 0;
}
/* Identify the total number of bytes in all blocks
* including the header data.
*/
size_t
hermes_blocks_length(const char *first_block, const void *end)
{
const struct dblock *blk = (const struct dblock *) first_block;
int total_len = 0;
int len;
end -= sizeof(*blk);
/* Skip all blocks to locate Plug Data References
* (Spectrum CS) */
while (((void *) blk <= end) &&
(dblock_addr(blk) != BLOCK_END)) {
len = dblock_len(blk);
total_len += sizeof(*blk) + len;
blk = (struct dblock *) &blk->data[len];
}
return total_len;
}
/*** Hermes programming ***/
/* About to start programming data (Hermes I)
* offset is the entry point
*
* Spectrum_cs' Symbol fw does not require this
* wl_lkm Agere fw does
* Don't know about intersil
*/
int hermesi_program_init(hermes_t *hw, u32 offset)
{
int err;
/* Disable interrupts?*/
/*hw->inten = 0x0;*/
/*hermes_write_regn(hw, INTEN, 0);*/
/*hermes_set_irqmask(hw, 0);*/
/* Acknowledge any outstanding command */
hermes_write_regn(hw, EVACK, 0xFFFF);
/* Using doicmd_wait rather than docmd_wait */
err = hermes_doicmd_wait(hw,
0x0100 | HERMES_CMD_INIT,
0, 0, 0, NULL);
if (err)
return err;
err = hermes_doicmd_wait(hw,
0x0000 | HERMES_CMD_INIT,
0, 0, 0, NULL);
if (err)
return err;
err = hermes_aux_control(hw, 1);
printk(KERN_DEBUG PFX "AUX enable returned %d\n", err);
if (err)
return err;
printk(KERN_DEBUG PFX "Enabling volatile, EP 0x%08x\n", offset);
err = hermes_doicmd_wait(hw,
HERMES_PROGRAM_ENABLE_VOLATILE,
offset & 0xFFFFu,
offset >> 16,
0,
NULL);
printk(KERN_DEBUG PFX "PROGRAM_ENABLE returned %d\n",
err);
return err;
}
/* Done programming data (Hermes I)
*
* Spectrum_cs' Symbol fw does not require this
* wl_lkm Agere fw does
* Don't know about intersil
*/
int hermesi_program_end(hermes_t *hw)
{
struct hermes_response resp;
int rc = 0;
int err;
rc = hermes_docmd_wait(hw, HERMES_PROGRAM_DISABLE, 0, &resp);
printk(KERN_DEBUG PFX "PROGRAM_DISABLE returned %d, "
"r0 0x%04x, r1 0x%04x, r2 0x%04x\n",
rc, resp.resp0, resp.resp1, resp.resp2);
if ((rc == 0) &&
((resp.status & HERMES_STATUS_CMDCODE) != HERMES_CMD_DOWNLD))
rc = -EIO;
err = hermes_aux_control(hw, 0);
printk(KERN_DEBUG PFX "AUX disable returned %d\n", err);
/* Acknowledge any outstanding command */
hermes_write_regn(hw, EVACK, 0xFFFF);
/* Reinitialise, ignoring return */
(void) hermes_doicmd_wait(hw, 0x0000 | HERMES_CMD_INIT,
0, 0, 0, NULL);
return rc ? rc : err;
}
/* Program the data blocks */
int hermes_program(hermes_t *hw, const char *first_block, const void *end)
{
const struct dblock *blk;
u32 blkaddr;
u32 blklen;
#if LIMIT_PROGRAM_SIZE
u32 addr;
u32 len;
#endif
blk = (const struct dblock *) first_block;
if ((void *) blk > (end - sizeof(*blk)))
return -EIO;
blkaddr = dblock_addr(blk);
blklen = dblock_len(blk);
while ((blkaddr != BLOCK_END) &&
(((void *) blk + blklen) <= end)) {
printk(KERN_DEBUG PFX
"Programming block of length %d to address 0x%08x\n",
blklen, blkaddr);
#if !LIMIT_PROGRAM_SIZE
/* wl_lkm driver splits this into writes of 2000 bytes */
hermes_aux_setaddr(hw, blkaddr);
hermes_write_bytes(hw, HERMES_AUXDATA, blk->data,
blklen);
#else
len = (blklen < MAX_DL_SIZE) ? blklen : MAX_DL_SIZE;
addr = blkaddr;
while (addr < (blkaddr + blklen)) {
printk(KERN_DEBUG PFX
"Programming subblock of length %d "
"to address 0x%08x. Data @ %p\n",
len, addr, &blk->data[addr - blkaddr]);
hermes_aux_setaddr(hw, addr);
hermes_write_bytes(hw, HERMES_AUXDATA,
&blk->data[addr - blkaddr],
len);
addr += len;
len = ((blkaddr + blklen - addr) < MAX_DL_SIZE) ?
(blkaddr + blklen - addr) : MAX_DL_SIZE;
}
#endif
blk = (const struct dblock *) &blk->data[blklen];
if ((void *) blk > (end - sizeof(*blk)))
return -EIO;
blkaddr = dblock_addr(blk);
blklen = dblock_len(blk);
}
return 0;
}
/*** Default plugging data for Hermes I ***/
/* Values from wl_lkm_718/hcf/dhf.c */
#define DEFINE_DEFAULT_PDR(pid, length, data) \
static const struct { \
__le16 len; \
__le16 id; \
u8 val[length]; \
} __attribute__ ((packed)) default_pdr_data_##pid = { \
cpu_to_le16((sizeof(default_pdr_data_##pid)/ \
sizeof(__le16)) - 1), \
cpu_to_le16(pid), \
data \
}
#define DEFAULT_PDR(pid) default_pdr_data_##pid
/* HWIF Compatiblity */
DEFINE_DEFAULT_PDR(0x0005, 10, "\x00\x00\x06\x00\x01\x00\x01\x00\x01\x00");
/* PPPPSign */
DEFINE_DEFAULT_PDR(0x0108, 4, "\x00\x00\x00\x00");
/* PPPPProf */
DEFINE_DEFAULT_PDR(0x0109, 10, "\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00");
/* Antenna diversity */
DEFINE_DEFAULT_PDR(0x0150, 2, "\x00\x3F");
/* Modem VCO band Set-up */
DEFINE_DEFAULT_PDR(0x0160, 28,
"\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00");
/* Modem Rx Gain Table Values */
DEFINE_DEFAULT_PDR(0x0161, 256,
"\x3F\x01\x3F\01\x3F\x01\x3F\x01"
"\x3F\x01\x3F\01\x3F\x01\x3F\x01"
"\x3F\x01\x3F\01\x3F\x01\x3F\x01"
"\x3F\x01\x3F\01\x3F\x01\x3F\x01"
"\x3F\x01\x3E\01\x3E\x01\x3D\x01"
"\x3D\x01\x3C\01\x3C\x01\x3B\x01"
"\x3B\x01\x3A\01\x3A\x01\x39\x01"
"\x39\x01\x38\01\x38\x01\x37\x01"
"\x37\x01\x36\01\x36\x01\x35\x01"
"\x35\x01\x34\01\x34\x01\x33\x01"
"\x33\x01\x32\x01\x32\x01\x31\x01"
"\x31\x01\x30\x01\x30\x01\x7B\x01"
"\x7B\x01\x7A\x01\x7A\x01\x79\x01"
"\x79\x01\x78\x01\x78\x01\x77\x01"
"\x77\x01\x76\x01\x76\x01\x75\x01"
"\x75\x01\x74\x01\x74\x01\x73\x01"
"\x73\x01\x72\x01\x72\x01\x71\x01"
"\x71\x01\x70\x01\x70\x01\x68\x01"
"\x68\x01\x67\x01\x67\x01\x66\x01"
"\x66\x01\x65\x01\x65\x01\x57\x01"
"\x57\x01\x56\x01\x56\x01\x55\x01"
"\x55\x01\x54\x01\x54\x01\x53\x01"
"\x53\x01\x52\x01\x52\x01\x51\x01"
"\x51\x01\x50\x01\x50\x01\x48\x01"
"\x48\x01\x47\x01\x47\x01\x46\x01"
"\x46\x01\x45\x01\x45\x01\x44\x01"
"\x44\x01\x43\x01\x43\x01\x42\x01"
"\x42\x01\x41\x01\x41\x01\x40\x01"
"\x40\x01\x40\x01\x40\x01\x40\x01"
"\x40\x01\x40\x01\x40\x01\x40\x01"
"\x40\x01\x40\x01\x40\x01\x40\x01"
"\x40\x01\x40\x01\x40\x01\x40\x01");
/* Write PDA according to certain rules.
*
* For every production data record, look for a previous setting in
* the pda, and use that.
*
* For certain records, use defaults if they are not found in pda.
*/
int hermes_apply_pda_with_defaults(hermes_t *hw,
const char *first_pdr,
const void *pdr_end,
const __le16 *pda,
const void *pda_end)
{
const struct pdr *pdr = (const struct pdr *) first_pdr;
const struct pdi *first_pdi = (const struct pdi *) &pda[2];
const struct pdi *pdi;
const struct pdi *default_pdi = NULL;
const struct pdi *outdoor_pdi;
int record_id;
pdr_end -= sizeof(struct pdr);
while (((void *) pdr <= pdr_end) &&
(pdr_id(pdr) != PDI_END)) {
/*
* For spectrum_cs firmwares,
* PDR area is currently not terminated by PDI_END.
* It's followed by CRC records, which have the type
* field where PDR has length. The type can be 0 or 1.
*/
if (pdr_len(pdr) < 2)
break;
record_id = pdr_id(pdr);
pdi = hermes_find_pdi(first_pdi, record_id, pda_end);
if (pdi)
printk(KERN_DEBUG PFX "Found record 0x%04x at %p\n",
record_id, pdi);
switch (record_id) {
case 0x110: /* Modem REFDAC values */
case 0x120: /* Modem VGDAC values */
outdoor_pdi = hermes_find_pdi(first_pdi, record_id + 1,
pda_end);
default_pdi = NULL;
if (outdoor_pdi) {
pdi = outdoor_pdi;
printk(KERN_DEBUG PFX
"Using outdoor record 0x%04x at %p\n",
record_id + 1, pdi);
}
break;
case 0x5: /* HWIF Compatiblity */
default_pdi = (struct pdi *) &DEFAULT_PDR(0x0005);
break;
case 0x108: /* PPPPSign */
default_pdi = (struct pdi *) &DEFAULT_PDR(0x0108);
break;
case 0x109: /* PPPPProf */
default_pdi = (struct pdi *) &DEFAULT_PDR(0x0109);
break;
case 0x150: /* Antenna diversity */
default_pdi = (struct pdi *) &DEFAULT_PDR(0x0150);
break;
case 0x160: /* Modem VCO band Set-up */
default_pdi = (struct pdi *) &DEFAULT_PDR(0x0160);
break;
case 0x161: /* Modem Rx Gain Table Values */
default_pdi = (struct pdi *) &DEFAULT_PDR(0x0161);
break;
default:
default_pdi = NULL;
break;
}
if (!pdi && default_pdi) {
/* Use default */
pdi = default_pdi;
printk(KERN_DEBUG PFX
"Using default record 0x%04x at %p\n",
record_id, pdi);
}
if (pdi) {
/* Lengths of the data in PDI and PDR must match */
if ((pdi_len(pdi) == pdr_len(pdr)) &&
((void *) pdi->data + pdi_len(pdi) < pda_end)) {
/* do the actual plugging */
hermes_aux_setaddr(hw, pdr_addr(pdr));
hermes_write_bytes(hw, HERMES_AUXDATA,
pdi->data, pdi_len(pdi));
}
}
pdr++;
}
return 0;
}