diff options
Diffstat (limited to 'src/pluto/kernel_alg.c')
-rw-r--r-- | src/pluto/kernel_alg.c | 775 |
1 files changed, 775 insertions, 0 deletions
diff --git a/src/pluto/kernel_alg.c b/src/pluto/kernel_alg.c new file mode 100644 index 000000000..91dfaff59 --- /dev/null +++ b/src/pluto/kernel_alg.c @@ -0,0 +1,775 @@ +/* Kernel runtime algorithm handling interface + * Author: JuanJo Ciarlante <jjo-ipsec@mendoza.gov.ar> + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * RCSID $Id: kernel_alg.c,v 1.9 2005/08/17 16:31:24 as Exp $ + */ + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <unistd.h> +#include <sys/queue.h> + +#include <pfkeyv2.h> +#include <pfkey.h> + +#include <freeswan.h> +#include <ipsec_policy.h> + +#include "constants.h" +#include "defs.h" +#include "connections.h" +#include "state.h" +#include "packet.h" +#include "spdb.h" +#include "kernel.h" +#include "kernel_alg.h" +#include "alg_info.h" + +#ifndef NO_PLUTO +#include "log.h" +#include "whack.h" +#include "db_ops.h" +#else +/* + * macros/functions for compilation without pluto (eg: spi for manual conns) + */ +extern int debug; +#include <assert.h> +#define passert(x) assert(x) +#define DBG(cond, action) { if (debug) { action ; } } +#define DBG_log(x, args...) fprintf(stderr, x "\n" , ##args); +#define plog(x, args...) fprintf(stderr, x "\n" , ##args); +#endif /* NO_PLUTO */ +/* ALG storage */ +static struct sadb_alg esp_aalg[SADB_AALG_MAX+1]; +static struct sadb_alg esp_ealg[SADB_EALG_MAX+1]; +static int esp_ealg_num = 0; +static int esp_aalg_num = 0; + +#define ESP_EALG_PRESENT(algo) (((algo)<=SADB_EALG_MAX)&&(esp_ealg[(algo)].sadb_alg_id==(algo))) +#define ESP_EALG_FOR_EACH_UPDOWN(algo) \ + for (algo=SADB_EALG_MAX; algo >0 ; algo--) \ + if (ESP_EALG_PRESENT(algo)) +#define ESP_AALG_PRESENT(algo) ((algo<=SADB_AALG_MAX)&&(esp_aalg[(algo)].sadb_alg_id==(algo))) +#define ESP_AALG_FOR_EACH_UPDOWN(algo) \ + for (algo=SADB_AALG_MAX; algo >0 ; algo--) \ + if (ESP_AALG_PRESENT(algo)) + +static struct sadb_alg* +sadb_alg_ptr (int satype, int exttype, int alg_id, int rw) +{ + struct sadb_alg *alg_p = NULL; + + switch (exttype) + { + case SADB_EXT_SUPPORTED_AUTH: + if (alg_id > SADB_AALG_MAX) + return NULL; + break; + case SADB_EXT_SUPPORTED_ENCRYPT: + if (alg_id > SADB_EALG_MAX) + return NULL; + break; + default: + return NULL; + } + + switch (satype) + { + case SADB_SATYPE_ESP: + alg_p = (exttype == SADB_EXT_SUPPORTED_ENCRYPT)? + &esp_ealg[alg_id] : &esp_aalg[alg_id]; + /* get for write: increment elem count */ + if (rw) + { + (exttype == SADB_EXT_SUPPORTED_ENCRYPT)? + esp_ealg_num++ : esp_aalg_num++; + } + break; + case SADB_SATYPE_AH: + default: + return NULL; + } + + return alg_p; +} + +const struct sadb_alg * +kernel_alg_sadb_alg_get(int satype, int exttype, int alg_id) +{ + return sadb_alg_ptr(satype, exttype, alg_id, 0); +} + +/* + * Forget previous registration + */ +static void +kernel_alg_init(void) +{ + DBG(DBG_KLIPS, + DBG_log("alg_init(): memset(%p, 0, %d) memset(%p, 0, %d)", + &esp_aalg, (int)sizeof (esp_aalg), + &esp_ealg, (int)sizeof (esp_ealg)) + ) + memset (&esp_aalg, 0, sizeof (esp_aalg)); + memset (&esp_ealg, 0, sizeof (esp_ealg)); + esp_ealg_num=esp_aalg_num = 0; +} + +static int +kernel_alg_add(int satype, int exttype, const struct sadb_alg *sadb_alg) +{ + struct sadb_alg *alg_p = NULL; + int alg_id = sadb_alg->sadb_alg_id; + + DBG(DBG_KLIPS, + DBG_log("kernel_alg_add(): satype=%d, exttype=%d, alg_id=%d", + satype, exttype, sadb_alg->sadb_alg_id) + ) + if (!(alg_p = sadb_alg_ptr(satype, exttype, alg_id, 1))) + return -1; + + /* This logic "mimics" KLIPS: first algo implementation will be used */ + if (alg_p->sadb_alg_id) + { + DBG(DBG_KLIPS, + DBG_log("kernel_alg_add(): discarding already setup " + "satype=%d, exttype=%d, alg_id=%d", + satype, exttype, sadb_alg->sadb_alg_id) + ) + return 0; + } + *alg_p = *sadb_alg; + return 1; +} + +bool +kernel_alg_esp_enc_ok(u_int alg_id, u_int key_len, + struct alg_info_esp *alg_info __attribute__((unused))) +{ + struct sadb_alg *alg_p = NULL; + + /* + * test #1: encrypt algo must be present + */ + int ret = ESP_EALG_PRESENT(alg_id); + if (!ret) goto out; + + alg_p = &esp_ealg[alg_id]; + + /* + * test #2: if key_len specified, it must be in range + */ + if (key_len + && (key_len < alg_p->sadb_alg_minbits || key_len > alg_p->sadb_alg_maxbits)) + { + plog("kernel_alg_db_add() key_len not in range: alg_id=%d, " + "key_len=%d, alg_minbits=%d, alg_maxbits=%d" + , alg_id, key_len + , alg_p->sadb_alg_minbits + , alg_p->sadb_alg_maxbits); + ret = FALSE; + } + +out: + if (ret) + { + DBG(DBG_KLIPS, + DBG_log("kernel_alg_esp_enc_ok(%d,%d): " + "alg_id=%d, " + "alg_ivlen=%d, alg_minbits=%d, alg_maxbits=%d, " + "res=%d, ret=%d" + , alg_id, key_len + , alg_p->sadb_alg_id + , alg_p->sadb_alg_ivlen + , alg_p->sadb_alg_minbits + , alg_p->sadb_alg_maxbits + , alg_p->sadb_alg_reserved + , ret); + ) + } + else + { + DBG(DBG_KLIPS, + DBG_log("kernel_alg_esp_enc_ok(%d,%d): NO", alg_id, key_len); + ) + } + return ret; +} + +/* + * ML: make F_STRICT logic consider enc,auth algorithms + */ +#ifndef NO_PLUTO +bool +kernel_alg_esp_ok_final(u_int ealg, u_int key_len, u_int aalg, struct alg_info_esp *alg_info) +{ + int ealg_insecure; + + /* + * key_len passed comes from esp_attrs read from peer + * For many older algoritms (eg 3DES) this key_len is fixed + * and get passed as 0. + * ... then get default key_len + */ + if (key_len == 0) + key_len = kernel_alg_esp_enc_keylen(ealg) * BITS_PER_BYTE; + + /* + * simple test to toss low key_len, will accept it only + * if specified in "esp" string + */ + ealg_insecure = (key_len < 128) ; + + if (ealg_insecure + || (alg_info && alg_info->alg_info_flags & ALG_INFO_F_STRICT)) + { + int i; + struct esp_info *esp_info; + + if (alg_info) + { + ALG_INFO_ESP_FOREACH(alg_info, esp_info, i) + { + if (esp_info->esp_ealg_id == ealg + && (esp_info->esp_ealg_keylen == 0 || key_len == 0 + || esp_info->esp_ealg_keylen == key_len) + && esp_info->esp_aalg_id == aalg) + { + if (ealg_insecure) + { + loglog(RC_LOG_SERIOUS + , "You should NOT use insecure ESP algorithms [%s (%d)]!" + , enum_name(&esp_transformid_names, ealg), key_len); + } + return TRUE; + } + } + } + plog("IPSec Transform [%s (%d), %s] refused due to %s", + enum_name(&esp_transformid_names, ealg), key_len, + enum_name(&auth_alg_names, aalg), + ealg_insecure ? "insecure key_len and enc. alg. not listed in \"esp\" string" : "strict flag"); + return FALSE; + } + return TRUE; +} +#endif /* NO_PLUTO */ + +/* + * Load kernel_alg arrays from /proc + * used in manual mode from klips/utils/spi.c + */ +int +kernel_alg_proc_read(void) +{ + int satype; + int supp_exttype; + int alg_id, ivlen, minbits, maxbits; + struct sadb_alg sadb_alg; + int ret; + char buf[128]; + + FILE *fp=fopen("/proc/net/pf_key_supported", "r"); + + if (!fp) + return -1; + + kernel_alg_init(); + + while (fgets(buf, sizeof(buf), fp)) + { + if (buf[0] != ' ') /* skip titles */ + continue; + + sscanf(buf, "%d %d %d %d %d %d" + ,&satype, &supp_exttype + , &alg_id, &ivlen + , &minbits, &maxbits); + + switch (satype) + { + case SADB_SATYPE_ESP: + switch(supp_exttype) + { + case SADB_EXT_SUPPORTED_AUTH: + case SADB_EXT_SUPPORTED_ENCRYPT: + sadb_alg.sadb_alg_id = alg_id; + sadb_alg.sadb_alg_ivlen = ivlen; + sadb_alg.sadb_alg_minbits = minbits; + sadb_alg.sadb_alg_maxbits = maxbits; + ret = kernel_alg_add(satype, supp_exttype, &sadb_alg); + DBG(DBG_CRYPT, + DBG_log("kernel_alg_proc_read() alg_id=%d, " + "alg_ivlen=%d, alg_minbits=%d, alg_maxbits=%d, " + "ret=%d" + , sadb_alg.sadb_alg_id + , sadb_alg.sadb_alg_ivlen + , sadb_alg.sadb_alg_minbits + , sadb_alg.sadb_alg_maxbits + , ret) + ) + } + default: + continue; + } + } + fclose(fp); + return 0; +} + +/* + * Load kernel_alg arrays pluto's SADB_REGISTER + * user by pluto/kernel.c + */ + +void +kernel_alg_register_pfkey(const struct sadb_msg *msg_buf, int buflen) +{ + /* Trick: one 'type-mangle-able' pointer to ease offset/assign */ + union { + const struct sadb_msg *msg; + const struct sadb_supported *supported; + const struct sadb_ext *ext; + const struct sadb_alg *alg; + const char *ch; + } sadb; + + int satype; + int msglen; + int i = 0; + + /* Initialize alg arrays */ + kernel_alg_init(); + satype = msg_buf->sadb_msg_satype; + sadb.msg = msg_buf; + msglen = sadb.msg->sadb_msg_len*IPSEC_PFKEYv2_ALIGN; + msglen -= sizeof(struct sadb_msg); + buflen -= sizeof(struct sadb_msg); + passert(buflen > 0); + + sadb.msg++; + + while(msglen) + { + int supp_exttype = sadb.supported->sadb_supported_exttype; + int supp_len = sadb.supported->sadb_supported_len*IPSEC_PFKEYv2_ALIGN; + + DBG(DBG_KLIPS, + DBG_log("kernel_alg_register_pfkey(): SADB_SATYPE_%s: " + "sadb_msg_len=%d sadb_supported_len=%d" + , satype==SADB_SATYPE_ESP? "ESP" : "AH" + , msg_buf->sadb_msg_len, supp_len) + ) + sadb.supported++; + msglen -= supp_len; + buflen -= supp_len; + passert(buflen >= 0); + + for (supp_len -= sizeof(struct sadb_supported); + supp_len; + supp_len -= sizeof(struct sadb_alg), sadb.alg++,i++) + { + int ret = kernel_alg_add(satype, supp_exttype, sadb.alg); + + DBG(DBG_KLIPS, + DBG_log("kernel_alg_register_pfkey(): SADB_SATYPE_%s: " + "alg[%d], exttype=%d, satype=%d, alg_id=%d, " + "alg_ivlen=%d, alg_minbits=%d, alg_maxbits=%d, " + "res=%d, ret=%d" + , satype==SADB_SATYPE_ESP? "ESP" : "AH" + , i + , supp_exttype + , satype + , sadb.alg->sadb_alg_id + , sadb.alg->sadb_alg_ivlen + , sadb.alg->sadb_alg_minbits + , sadb.alg->sadb_alg_maxbits + , sadb.alg->sadb_alg_reserved + , ret) + ) + } + } +} + +u_int +kernel_alg_esp_enc_keylen(u_int alg_id) +{ + u_int keylen = 0; + + if (!ESP_EALG_PRESENT(alg_id)) + goto none; + + keylen = esp_ealg[alg_id].sadb_alg_maxbits/BITS_PER_BYTE; + + switch (alg_id) + { + /* + * this is veryUgly[TM] + * Peer should have sent KEY_LENGTH attribute for ESP_AES + * but if not do force it to 128 instead of using sadb_alg_maxbits + * from kernel. + */ + case ESP_AES: + keylen = 128/BITS_PER_BYTE; + break; + } + +none: + DBG(DBG_KLIPS, + DBG_log("kernel_alg_esp_enc_keylen():" + "alg_id=%d, keylen=%d", + alg_id, keylen) + ) + return keylen; +} + +struct sadb_alg * +kernel_alg_esp_sadb_alg(u_int alg_id) +{ + struct sadb_alg *sadb_alg = (ESP_EALG_PRESENT(alg_id)) + ? &esp_ealg[alg_id] : NULL; + + DBG(DBG_KLIPS, + DBG_log("kernel_alg_esp_sadb_alg(): alg_id=%d, sadb_alg=%p" + , alg_id, sadb_alg) + ) + return sadb_alg; +} + +#ifndef NO_PLUTO +void kernel_alg_list(void) +{ + u_int sadb_id; + + whack_log(RC_COMMENT, " "); + whack_log(RC_COMMENT, "List of registered ESP Encryption Algorithms:"); + whack_log(RC_COMMENT, " "); + + for (sadb_id = 1; sadb_id <= SADB_EALG_MAX; sadb_id++) + { + if (ESP_EALG_PRESENT(sadb_id)) + { + struct sadb_alg *alg_p = &esp_ealg[sadb_id]; + + whack_log(RC_COMMENT, "#%-5d %s, blocksize: %d, keylen: %d-%d" + , sadb_id + , enum_name(&esp_transformid_names, sadb_id) + , alg_p->sadb_alg_ivlen + , alg_p->sadb_alg_minbits + , alg_p->sadb_alg_maxbits + ); + } + } + + whack_log(RC_COMMENT, " "); + whack_log(RC_COMMENT, "List of registered ESP Authentication Algorithms:"); + whack_log(RC_COMMENT, " "); + + for (sadb_id = 1; sadb_id <= SADB_AALG_MAX; sadb_id++) + { + if (ESP_AALG_PRESENT(sadb_id)) + { + u_int aaid = alg_info_esp_sadb2aa(sadb_id); + struct sadb_alg *alg_p = &esp_aalg[sadb_id]; + + whack_log(RC_COMMENT, "#%-5d %s, keylen: %d-%d" + , aaid + , enum_name(&auth_alg_names, aaid) + , alg_p->sadb_alg_minbits + , alg_p->sadb_alg_maxbits + ); + } + } +} + +void +kernel_alg_show_connection(struct connection *c, const char *instance) +{ + char buf[256]; + struct state *st; + + if (c->alg_info_esp) + { + alg_info_snprint(buf, sizeof(buf), (struct alg_info *)c->alg_info_esp); + whack_log(RC_COMMENT + , "\"%s\"%s: ESP algorithms wanted: %s" + , c->name + , instance + , buf); + } + if (c->alg_info_esp) + { + alg_info_snprint_esp(buf, sizeof(buf), c->alg_info_esp); + whack_log(RC_COMMENT + , "\"%s\"%s: ESP algorithms loaded: %s" + , c->name + , instance + , buf); + } + st = state_with_serialno(c->newest_ipsec_sa); + if (st && st->st_esp.present) + whack_log(RC_COMMENT + , "\"%s\"%s: ESP algorithm newest: %s_%d-%s; pfsgroup=%s" + , c->name + , instance + , enum_show(&esp_transformid_names, st->st_esp.attrs.transid) + +4 /* strlen("ESP_") */ + , st->st_esp.attrs.key_len + , enum_show(&auth_alg_names, st->st_esp.attrs.auth)+ + +15 /* strlen("AUTH_ALGORITHM_") */ + , c->policy & POLICY_PFS ? + c->alg_info_esp->esp_pfsgroup ? + enum_show(&oakley_group_names, + c->alg_info_esp->esp_pfsgroup) + +13 /*strlen("OAKLEY_GROUP_")*/ + : "<Phase1>" + : "<N/A>" + ); +} +#endif /* NO_PLUTO */ + +bool +kernel_alg_esp_auth_ok(u_int auth, + struct alg_info_esp *alg_info __attribute__((unused))) +{ + return ESP_AALG_PRESENT(alg_info_esp_aa2sadb(auth)); +} + +u_int +kernel_alg_esp_auth_keylen(u_int auth) +{ + u_int sadb_aalg = alg_info_esp_aa2sadb(auth); + + u_int a_keylen = (sadb_aalg) + ? esp_aalg[sadb_aalg].sadb_alg_maxbits/BITS_PER_BYTE + : 0; + + DBG(DBG_CONTROL | DBG_CRYPT | DBG_PARSING, + DBG_log("kernel_alg_esp_auth_keylen(auth=%d, sadb_aalg=%d): " + "a_keylen=%d", auth, sadb_aalg, a_keylen) + ) + return a_keylen; +} + +struct esp_info * +kernel_alg_esp_info(int transid, int auth) +{ + int sadb_aalg, sadb_ealg; + static struct esp_info ei_buf; + + sadb_ealg = transid; + sadb_aalg = alg_info_esp_aa2sadb(auth); + + if (!ESP_EALG_PRESENT(sadb_ealg)) + goto none; + if (!ESP_AALG_PRESENT(sadb_aalg)) + goto none; + + memset(&ei_buf, 0, sizeof (ei_buf)); + ei_buf.transid = transid; + ei_buf.auth = auth; + + /* don't return "default" keylen because this value is used from + * setup_half_ipsec_sa() to "validate" keylen + * In effect, enckeylen will be used as "max" value + */ + ei_buf.enckeylen = esp_ealg[sadb_ealg].sadb_alg_maxbits/BITS_PER_BYTE; + ei_buf.authkeylen = esp_aalg[sadb_aalg].sadb_alg_maxbits/BITS_PER_BYTE; + ei_buf.encryptalg = sadb_ealg; + ei_buf.authalg = sadb_aalg; + + DBG(DBG_PARSING, + DBG_log("kernel_alg_esp_info():" + "transid=%d, auth=%d, ei=%p, " + "enckeylen=%d, authkeylen=%d, encryptalg=%d, authalg=%d", + transid, auth, &ei_buf, + (int)ei_buf.enckeylen, (int)ei_buf.authkeylen, + ei_buf.encryptalg, ei_buf.authalg) + ) + return &ei_buf; + +none: + DBG(DBG_PARSING, + DBG_log("kernel_alg_esp_info():" + "transid=%d, auth=%d, ei=NULL", + transid, auth) + ) + return NULL; +} + +#ifndef NO_PLUTO +static void +kernel_alg_policy_algorithms(struct esp_info *esp_info) +{ + u_int ealg_id = esp_info->esp_ealg_id; + + switch(ealg_id) + { + case 0: + case ESP_DES: + case ESP_3DES: + case ESP_NULL: + case ESP_CAST: + break; + default: + if (!esp_info->esp_ealg_keylen) + { + /* algos that need KEY_LENGTH + * + * Note: this is a very dirty hack ;-) + * Idea: Add a key_length_needed attribute to + * esp_ealg ?? + */ + esp_info->esp_ealg_keylen = esp_ealg[ealg_id].sadb_alg_maxbits; + } + } +} + +static bool +kernel_alg_db_add(struct db_context *db_ctx, struct esp_info *esp_info, lset_t policy) +{ + u_int ealg_id, aalg_id; + + ealg_id = esp_info->esp_ealg_id; + + if (!ESP_EALG_PRESENT(ealg_id)) + { + DBG_log("kernel_alg_db_add() kernel enc ealg_id=%d not present", ealg_id); + return FALSE; + } + + if (!(policy & POLICY_AUTHENTICATE)) /* skip ESP auth attrs for AH */ + { + aalg_id = alg_info_esp_aa2sadb(esp_info->esp_aalg_id); + + if (!ESP_AALG_PRESENT(aalg_id)) + { + DBG_log("kernel_alg_db_add() kernel auth " + "aalg_id=%d not present", aalg_id); + return FALSE; + } + } + + /* do algo policy */ + kernel_alg_policy_algorithms(esp_info); + + /* open new transformation */ + db_trans_add(db_ctx, ealg_id); + + /* add ESP auth attr */ + if (!(policy & POLICY_AUTHENTICATE)) + db_attr_add_values(db_ctx, AUTH_ALGORITHM, esp_info->esp_aalg_id); + + /* add keylegth if specified in esp= string */ + if (esp_info->esp_ealg_keylen) + db_attr_add_values(db_ctx, KEY_LENGTH, esp_info->esp_ealg_keylen); + + return TRUE; +} + +/* + * Create proposal with runtime kernel algos, merging + * with passed proposal if not NULL + * + * for now this function does free() previous returned + * malloced pointer (this quirk allows easier spdb.c change) + */ +struct db_context * +kernel_alg_db_new(struct alg_info_esp *alg_info, lset_t policy ) +{ + const struct esp_info *esp_info; + struct esp_info tmp_esp_info; + struct db_context *ctx_new=NULL; + struct db_trans *t; + struct db_prop *prop; + u_int trans_cnt; + int tn = 0; + + if (!(policy & POLICY_ENCRYPT)) /* not possible, I think */ + return NULL; + + trans_cnt = esp_ealg_num * esp_aalg_num; + DBG(DBG_EMITTING, + DBG_log("kernel_alg_db_prop_new() initial trans_cnt=%d" + , trans_cnt) + ) + + /* pass aprox. number of transforms and attributes */ + ctx_new = db_prop_new(PROTO_IPSEC_ESP, trans_cnt, trans_cnt * 2); + + /* + * Loop: for each element (struct esp_info) of alg_info, + * if kernel support is present then build the transform (and attrs) + * if NULL alg_info, propose everything ... + */ + + if (alg_info) + { + int i; + + ALG_INFO_ESP_FOREACH(alg_info, esp_info, i) + { + tmp_esp_info = *esp_info; + kernel_alg_db_add(ctx_new, &tmp_esp_info, policy); + } + } + else + { + u_int ealg_id; + + ESP_EALG_FOR_EACH_UPDOWN(ealg_id) + { + u_int aalg_id; + + tmp_esp_info.esp_ealg_id = ealg_id; + tmp_esp_info.esp_ealg_keylen = 0; + + for (aalg_id = 1; aalg_id <= SADB_AALG_MAX; aalg_id++) + { + if (ESP_AALG_PRESENT(aalg_id)) + { + tmp_esp_info.esp_aalg_id = alg_info_esp_sadb2aa(aalg_id); + tmp_esp_info.esp_aalg_keylen = 0; + kernel_alg_db_add(ctx_new, &tmp_esp_info, policy); + } + } + } + } + + prop = db_prop_get(ctx_new); + + DBG(DBG_CONTROL|DBG_EMITTING, + DBG_log("kernel_alg_db_prop_new() " + "will return p_new->protoid=%d, p_new->trans_cnt=%d" + , prop->protoid, prop->trans_cnt) + ) + + for (t = prop->trans, tn = 0; tn < prop->trans_cnt; tn++) + { + DBG(DBG_CONTROL|DBG_EMITTING, + DBG_log("kernel_alg_db_prop_new() " + " trans[%d]: transid=%d, attr_cnt=%d, " + "attrs[0].type=%d, attrs[0].val=%d" + , tn + , t[tn].transid, t[tn].attr_cnt + , t[tn].attrs[0].type, t[tn].attrs[0].val) + ) + } + return ctx_new; +} +#endif /* NO_PLUTO */ |