For every dynamic software update patch that will be produced, a file must be provided that describes how the update should be applied. This file describes:
Which functions will be updated.
Which global variables and which datatypes will be updated. Note that a datatype update affects functions that use that datatype.
How program execution should continue after an update is applied.
Runtime update constraints.
The functions that should be updated need to be described to the dynamic software update runtime. Figure 5-1 shows an example describing the updated functions when updating vsFTPd from version 2.0.4 to version 2.0.5.
Figure 5-1. Describing function updates for vsFTPd from 2.0.4 to 2.0.5.
#include "hcu_mappings.h" hcu_mapping_update_description_t mapping_updates_v2[] = { { "main", "main" } }; hcu_mapping_function_update_description_t mapping_function_updates_v2[] = { { "str_locate_text_reverse", "str_locate_text_reverse", 0 }, { "emit_greeting", "emit_greeting", 0 }, { "handle_login", "handle_login", 0 }, { "str_locate_chars", "str_locate_chars", 0 }, { "vsf_privop_do_login", "vsf_privop_do_login", 0 }, { "vsf_remove_uwtmp", "vsf_remove_uwtmp", 0 }, { "handle_retr", "handle_retr", 0 }, { "vsf_sysutil_connect_timeout", "vsf_sysutil_connect_timeout", 0 }, { "handle_upload_common", "handle_upload_common", 0 }, { "handle_user_command", "handle_user_command", 0 }, { "main", "main", 1 }, { "handle_mdtm", "handle_mdtm", 0 }, { "handle_size", "handle_size", 0 }, { "vsf_insert_uwtmp", "vsf_insert_uwtmp", 0 }, { "handle_feat", "handle_feat", 0 }, { "str_locate_text", "str_locate_text", 0 }, { "get_unique_filename", "get_unique_filename", 0 }, { "vsf_sysutil_chroot", "vsf_sysutil_chroot", 0 }, { "vsf_sysdep_check_auth", "vsf_sysdep_check_auth", 0 }, { "vsf_sysutil_tzset", "vsf_sysutil_tzset", 0 }, { "handle_pass_command", "handle_pas_command", 0 }, { "vsf_ls_populate_dir_list", "vsf_ls_populate_dir_list", 0 }, { "calc_num_send", "calc_num_send", 0 }, { "handle_stat", "handle_stat", 0 }, { "vsf_privop_do_file_chown", "vsf_privop_do_file_chown", 0 } };
hcu_mapping_update_description_t
This datatype defines an array of threads that should be updated. One variable declaration of this datatype is required.
Since vsFTPd is a single-threaded program, this array contains only one entry. The entry requests that the thread called main (the thread whose entrypoint function is the function called main()) will have its stack reconstructed, when updated, all the way up to the function called main().
![]() | It would be possible to request for this thread to be have its stack partially reconstructed: to unwind up to one of the callees of the main() function instead of unwinding all the way up to the main() function. This could be useful as an optimization that minimizes the updating latency in a deeply recursive program. But for most programs unwinding the stack up to the thread entrypoint would have little impact in the total updating latency. |
hcu_mapping_function_update_description_t
This datatype defines an array of functions that should be updated. One variable declaration of this datatype is required.
Each definition of a function update consists of three fields. The name of the function that will be updated (from the original source code), the name of the function that will take its place (from the new source code), and a flag indicating whether the original function was a thread entrypoint.
Thread entrypoints are the main() function, functions that are passed as arguments to a pthread_create(), and signal handlers defined with signal() and sigaction().
Since vsFTPd is a single-threaded program, the entry to update the main() function is flagged as a thread entrypoint.
![]() | But wait! How did a user produce the list of function updates? The patch generator, described in Section 5.4, can produce the list of modified functions when invoked with empty variable definitions of the two required datatypes hcu_mapping_update_description_t and hcu_mapping_function_update_description_t. It is expected that a user will first run the patch generator to produce the list of function updates, and then manually produce the update description file. But why? Isn't the patch generator capable of producing the entire update description file? The patch generator can produce the entire update description file, but that would be presumptuous. Producing the entire list of function updates in the file would guarantee that a program is representation consistent: the running version matches the source code. However, the user may not desire an update to be representation consistent for various reasons. A user may want to apply an update to only a small collection of functions. For example, the user may want to apply a security fix or to avoid introducing, as part of the update, additional known defects that are present in the updated version of the program. Conclusion: Allowing users to manually produce a customized update description file separates policy from mechanism in the patch generator. There are plans to enhance the patch generator to produce a template update description file that the user may customize to produce the final file. |
Datatype updates can affect both global variables and local variables in functions that use the datatype. Datatype updates are automatically produced by the patch generator as described in Section 5.4. To complement incomplete datatype updates a user can manually write datatype transformers in the mappings file. Datatype transformer names must contain the special prefix HCU_datatype_transformation__ and to be invoked from the special function HCU_datatype_transformations_function_.
For example, the parseconf_uint_array variable is an array than has had its size extended in the newer version 2.0.5. The values of this array are preserved in the updated parseconf_uint_array_v205 variable for version 2.0.5, as shown in Figure 5-2.
Figure 5-2. Transforming datatypes for vsFTPd from 2.0.4 to 2.0.5.
void HCU_datatype_transformation__struct__parseconf_uint_setting__arraysize17_to_struct__parseconf_uint_setting__arraysize20(struct parseconf_uint_setting (*old)[17] , struct parseconf_uint_setting (*new)[20] ) { long array_counter ; { array_counter = 0; while (1) { HCU_datatype_transformation__struct__parseconf_uint_setting__arraysize17_to_struct__parseconf_uint_setting__arraysize20__element(& (*old)[array_counter], & (*new)[array_counter]); if (array_counter >= 17 - 1) { break; } array_counter ++; } // Must extend the array // NOTE: The following 11 statements are not automatically generated yet ((*new)[16]).p_setting_name = malloc(strlen("delay_failed_login") + 1); memcpy(&((*new)[16]).p_setting_name, "delay_failed_login" "\0", strlen("delay_failed_login") + 1); ((*new)[16]).p_variable = &tunable_delay_failed_login; ((*new)[17]).p_setting_name = malloc(strlen("delay_successful_login") + 1); memcpy(&((*new)[17]).p_setting_name, "delay_successful_login" "\0", strlen("delay_successful_login") + 1); ((*new)[17]).p_variable = &tunable_delay_successful_login; ((*new)[18]).p_setting_name = malloc(strlen("max_login_fails") + 1); memcpy(&((*new)[18]).p_setting_name, "max_login_fails" "\0", strlen("max_login_fails") + 1); ((*new)[18]).p_variable = &tunable_max_login_fails; ((*new)[19]).p_setting_name = 0; ((*new)[19]).p_variable = 0; } } int HCU_datatype_transformations_function() __attribute__((__HCU_ATTRIBUTE_NON_UPDATEABLE__)) ; { HCU_datatype_transformation__struct__parseconf_uint_setting__arraysize17_to_struct__parseconf_uint_setting__arraysize20(& parseconf_uint_array, & parseconf_uint_array_v205); // NOTE: The following 3 statements are not automatically generated yet tunable_delay_failed_login = 1; tunable_delay_successful_login = 0; tunable_max_login_fails = 3; printf("HCU_datatype_transformations_function_v205 executed\n"); return (0); }
UpStare is able to update a running algorithm midstream its execution and resume from a different point (not necessarily the beginning) of another algorithm that is behaviorally equivalent: an algorithm that aims to produce the same results while reusing existing progress. This capability requires user assistance. A user needs to define the mapping of continuation points in the original program to continuation points in the new version.
Continuation points are uniquely identified with an integer enumeration starting from 0 for every program function. This enumeration is embedded in a .cil.c file which contains the updateable source code. The continuation points of each function are enumerated in the beginning of the function (though the function name now includes the postfix _vXXX, where XXX is the version number) as case statements in a big switch statement.
![]() | There are plans to improve the identification (not the selection) of continuation points to use strings rather than numeric ids. This will further minimize the input needed by a user in defining continuation mappings. |
For example, for vsFTPd 2.0.4 the update source code is prepared in vsftpd-2.0.4/vsftpd.cil.c. To view the continuation points of function vsf_standalone_main, a user should look at the big switch statement in the beginning of function vsf_standalone_main_v204, which is shown in part in Figure 5-3.
Figure 5-3. Viewing the execution continuation points of vsf_standalone_main in vsFTPd 2.0.4.
struct vsf_client_launch vsf_standalone_main_v204(void) { ... switch (__cil_tmp20) { case 0: goto vsf_standalone_main_entrypoint; case 1: goto hcu_try_to_update_1_after; case 2: goto vsf_sysutil_get_ipaddr_size_2_after; case 3: goto die_3_after; case 4: goto vsf_sysutil_fork_4_after; ...
Figure 5-4 shows an example describing execution continuation when updating Bubblesort and continuing execution with Heapsort.
Figure 5-4. Describing execution continuation when updating from Bubblesort to Heapsort.
#include "hcu_mappings.h" #include "hcu_headers.h" hcu_mapping_update_description_t mapping_updates_v2[] = { { "main", "main" } }; hcu_mapping_function_update_description_t mapping_function_updates_v2[] = { { "main", "main", 1 } }; hcu_mapping_algorithmic_equivalence_t mapping_equivalence_v2_bubblesort[] = { { "bubbleSort", "heapSort", 1, { { 2, 1 } } } }; struct hcu_stack_local_bubbleSort_v1_s { int i ; int j ; int temp ; struct hcu_stack_frame_fields_s hcu_stack_frame_fields ; }; struct hcu_stack_local_heapSort_v2_s { int i ; int temp ; struct hcu_stack_frame_fields_s hcu_stack_frame_fields ; }; struct hcu_function_formal_heapSort_v2_s { int *numbers ; int array_size ; }; void HCU_stack_transformer__heapSort(void *transform_stack_to , void *transform_stack_from , void *transform_params_to ) { struct hcu_stack_local_heapSort_v2_s *stack_to ; struct hcu_stack_local_bubbleSort_v1_s *stack_from ; struct hcu_function_formal_heapSort_v2_s *params_to ; stack_to = (struct hcu_stack_local_heapSort_v2_s *)transform_stack_to; stack_from = (struct hcu_stack_local_bubbleSort_v1_s *)transform_stack_from; params_to = (struct hcu_function_formal_heapSort_v2_s *)transform_params_to; /* Continue the heapsort algorithm from the current iteration (redo the last iteration). Don't restart it from scratch. Our bubbleSort implementation processes an array from the end. Thus we simply have to shrink the array size to define the new bounds of the array for heapSort. */ params_to->array_size = stack_from->i + 1; hcu_copy_stack_frame_fields(&stack_to->hcu_stack_frame_fields, &stack_from->hcu_stack_frame_fields); }
hcu_mapping_algorithmic_equivalence_t
This datatype defines an array of behaviorally equivalent functions.
In each array element, the first parameter is the name of the function that will be updated, bubbleSort. The second parameter is the name of the behaviorally equivalent function from which execution will continue (heapSort) when it is time for the stack of the original function (bubbleSort) to be reconstructed. The third parameter reports the number of elements in the fourth parameter, which is an array. Each element of this array lists the update point number of the original function (update point 2 from bubbleSort) and a continuation point in the new function (heapSort) from which execution should resume.
void *transform_stack_to
A pointer to the stack of the new function heapSort.
void *transform_stack_from
A pointer to the stack of the old function bubbleSort.
void *transform_params_to
A pointer to a struct variable that groups the formal parameters of the new function heapSort.
hcu_copy_stack_frame_fields( from, to )
Preserves the execution continuation point in the stack of the new function. Executing this function within a stack-state transformer is required.
![]() | So what happens in this example? The stack of the updated function heapSort does not have state preserved from the stack of the old function bubbleSort at all. The stack of the old function is only consulted to change the formal parameters of the new function, and the stack of the new function remains uninitialized. Essentially, the new function continues execution by taking as input a smaller array of numbers to be sorted: it continues sorting from where Bubblesort stopped. |
Defining update constraints can help reduce the amount of state that needs to be mapped from the old version of an application to the new version. Depending on the updating model used, defining update constraints can also help enforce runtime safety.
If the updating model requested is to apply updates lazily, it is often necessary to define constraints that enforce type-safety and transaction-safety. The lazy updating model is enabled either using the HCU_STATIC_REQUEST_UPDATE_LAZY() call or by invoking the tool hcuapply with the command-line parameter--update-model=lazy.
![]() | Defining update constraints for type-safety is not necessary if the updating model requested is to apply updates immediately. However, defining update constraints for transaction-safety can be useful both for applying updates immediately and lazily. |
Figure 5-5 shows an example describing update constraints.
Figure 5-5. Describing update constraints.
#include "hcu_mappings.h" #include "hcu_safety_constraints.h" hcu_safety_constraint_t hcu_safety_constraints_v2[] = { { "main", "functionA", 1, { { -1, -1, 0 } } } };
hcu_safety_constraints_t
This datatype defines an array of update constraints.
For each array element, the first parameter is the name of thread on which the constraints should be enforced. The second parameter is the name of function for the requested thread on which the constraints should be enforced. The third parameter is the number of elements in the fourth parameter. The fourth parameter is an array of update constraints in disjunctive normal form. For example, the constraint:
(a|b)&cshould be expressed as:
c&a|c&bThis means one should declare an array with two elements. In the first element one should add the two items: c a. In the second element one should add the two items: c b. For each constraint definition, there are three parameters
The minimum execution point allowed for a valid update. Execution must have passed this point to match the constraint.
Note that a minimum of -1 is special and means anywhere (starting from the beginning of the function).
The maximum execution point allowed for a valid update. Execution must have not yet encountered this point to match the constraint.
Note that a minimum of -1 is special and means anywhere (until the end of the function).
Whether this constraint is a negation (NOT).
![]() | So what happens in this example? The constraint prohibits updates anywhere inside the function functionA for the main thread. |